From 2be4a8cd9f694da84060594258b8115a28d18cdd Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 25 Sep 2020 15:28:03 -0700 Subject: [PATCH 01/37] initial commit --- hubconf.py | 4 +--- models/common.py | 31 +++++++++++++++++++++++++++---- models/yolo.py | 29 +++++++++++++++++++---------- 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/hubconf.py b/hubconf.py index 168b40502f25..e439530b7660 100644 --- a/hubconf.py +++ b/hubconf.py @@ -36,9 +36,7 @@ def create(name, pretrained, channels, classes): state_dict = torch.load(ckpt, map_location=torch.device('cpu'))['model'].float().state_dict() # to FP32 state_dict = {k: v for k, v in state_dict.items() if model.state_dict()[k].shape == v.shape} # filter model.load_state_dict(state_dict, strict=False) # load - - model.add_nms() # add NMS module - model.eval() + # model = model.autoshape() # cv2/PIL/np inference: predictions = model(cv2.imread('img.jpg')) return model except Exception as e: diff --git a/models/common.py b/models/common.py index 314c31f91aac..a54050337d14 100644 --- a/models/common.py +++ b/models/common.py @@ -1,9 +1,12 @@ # This file contains modules common to various models import math +import numpy as np import torch import torch.nn as nn -from utils.general import non_max_suppression + +from utils.datasets import letterbox +from utils.general import non_max_suppression, make_divisible, scale_coords def autopad(k, p=None): # kernel, padding @@ -101,17 +104,37 @@ def forward(self, x): class NMS(nn.Module): # Non-Maximum Suppression (NMS) module - conf = 0.3 # confidence threshold - iou = 0.6 # IoU threshold + conf = 0.25 # confidence threshold + iou = 0.45 # IoU threshold classes = None # (optional list) filter by class - def __init__(self, dimension=1): + def __init__(self): super(NMS, self).__init__() def forward(self, x): return non_max_suppression(x[0], conf_thres=self.conf, iou_thres=self.iou, classes=self.classes) +class autoShape(nn.Module): + # auto-reshape image size model wrapper + img_size = 640 # inference size (pixels) + + def __init__(self, model): + super(autoShape, self).__init__() + self.model = model + + def forward(self, x, shape=640, augment=False, profile=False): # x = cv2.imread('img.jpg') + x0shape = x.shape[:2] + p = next(self.model.parameters()) + x, ratio, (dw, dh) = letterbox(x, new_shape=make_divisible(shape or max(x0shape), int(self.stride.max()))) + x1shape = x.shape[:2] + x = np.ascontiguousarray(x[:, :, ::-1].transpose(2, 0, 1)) # BGR to RGB, to 3x640x640 + x = torch.from_numpy(x).to(p.device).type_as(p).unsqueeze(0) / 255. # uint8 to fp16/32 + x = self.model(x, augment, profile) # forward + x[0][:, :4] = scale_coords(x1shape, x[0][:, :4], x0shape) + return x + + class Flatten(nn.Module): # Use after nn.AdaptiveAvgPool2d(1) to remove last 2 dimensions @staticmethod diff --git a/models/yolo.py b/models/yolo.py index 348c418e424e..083a33d30d27 100644 --- a/models/yolo.py +++ b/models/yolo.py @@ -7,11 +7,11 @@ import torch import torch.nn as nn -from models.common import Conv, Bottleneck, SPP, DWConv, Focus, BottleneckCSP, Concat, NMS +from models.common import Conv, Bottleneck, SPP, DWConv, Focus, BottleneckCSP, Concat, NMS, autoShape from models.experimental import MixConv2d, CrossConv, C3 from utils.general import check_anchor_order, make_divisible, check_file, set_logging -from utils.torch_utils import ( - time_synchronized, fuse_conv_and_bn, model_info, scale_img, initialize_weights, select_device) +from utils.torch_utils import time_synchronized, fuse_conv_and_bn, model_info, scale_img, initialize_weights, \ + select_device, copy_attr logger = logging.getLogger(__name__) @@ -138,6 +138,7 @@ def forward_once(self, x, profile=False): return x def _initialize_biases(self, cf=None): # initialize biases into Detect(), cf is class frequency + # https://arxiv.org/abs/1708.02002 section 3.3 # cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1. m = self.model[-1] # Detect() module for mi, s in zip(m.m, m.stride): # from @@ -168,15 +169,27 @@ def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers self.info() return self - def add_nms(self): # fuse model Conv2d() + BatchNorm2d() layers - if type(self.model[-1]) is not NMS: # if missing NMS - print('Adding NMS module... ') + def nms(self, mode=True): # add or remove NMS module + present = type(self.model[-1]) is NMS # last layer is NMS + if mode and not present: + print('Adding NMS... ') m = NMS() # module m.f = -1 # from m.i = self.model[-1].i + 1 # index self.model.add_module(name='%s' % m.i, module=m) # add + self.eval() + elif not mode and present: + print('Removing NMS... ') + self.model = self.model[:-1] # remove return self + def autoshape(self): # add autoShape module + print('Adding autoShape... ') + self.nms() # add NMS + m = autoShape(self) # wrap model + copy_attr(m, self, include=('names', 'stride', 'nc', 'autoshape'), exclude=()) # copy attributes + return m + def info(self, verbose=False): # print model information model_info(self, verbose) @@ -261,10 +274,6 @@ def parse_model(d, ch): # model_dict, input_channels(3) # img = torch.rand(8 if torch.cuda.is_available() else 1, 3, 640, 640).to(device) # y = model(img, profile=True) - # ONNX export - # model.model[-1].export = True - # torch.onnx.export(model, img, opt.cfg.replace('.yaml', '.onnx'), verbose=True, opset_version=11) - # Tensorboard # from torch.utils.tensorboard import SummaryWriter # tb_writer = SummaryWriter() From c9ef2690f22aa85af098ad8bc65373995ac64898 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 26 Sep 2020 18:32:36 -0700 Subject: [PATCH 02/37] batch inference update --- models/common.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/models/common.py b/models/common.py index a54050337d14..e543d129d907 100644 --- a/models/common.py +++ b/models/common.py @@ -116,22 +116,38 @@ def forward(self, x): class autoShape(nn.Module): - # auto-reshape image size model wrapper + # auto-reshape input image model wrapper img_size = 640 # inference size (pixels) def __init__(self, model): super(autoShape, self).__init__() self.model = model - def forward(self, x, shape=640, augment=False, profile=False): # x = cv2.imread('img.jpg') - x0shape = x.shape[:2] - p = next(self.model.parameters()) - x, ratio, (dw, dh) = letterbox(x, new_shape=make_divisible(shape or max(x0shape), int(self.stride.max()))) - x1shape = x.shape[:2] - x = np.ascontiguousarray(x[:, :, ::-1].transpose(2, 0, 1)) # BGR to RGB, to 3x640x640 - x = torch.from_numpy(x).to(p.device).type_as(p).unsqueeze(0) / 255. # uint8 to fp16/32 + def forward(self, x, shape=640, augment=False, profile=False): + # x is cv2/np/PIL RGB image, or list of images for batched inference, i.e. x = Image.open('image.jpg') + p = next(self.model.parameters()) # for device and type + if not isinstance(x, list): + x = [x] + batch = range(len(x)) # batch size + + shape0, shape1 = [], [] # image and inference shapes + for i in batch: + x[i] = np.array(x[i])[:, :, :3] # up to 3 channels if png + s = x[i].shape[:2] # HWC + shape0.append(s) # image shape + g = (shape / max(s)) # gain + shape1.append([y * g for y in s]) + shape1 = [make_divisible(x, int(self.stride.max())) for x in np.stack(shape1, 0).max(0)] # inference shape + + x = [letterbox(x[i], new_shape=shape1, auto=False)[0] for i in batch] # pad + x = np.stack(x, 0) if batch[-1] else x[0][None] # stack + x = np.ascontiguousarray(x.transpose((0, 3, 1, 2))) # BHWC to BCHW + x = torch.from_numpy(x).to(p.device).type_as(p) / 255. # uint8 to fp16/32 + x = self.model(x, augment, profile) # forward - x[0][:, :4] = scale_coords(x1shape, x[0][:, :4], x0shape) + + for i in batch: + x[i][:, :4] = scale_coords(shape1, x[i][:, :4], shape0[i]) # postprocess return x From c599075a4bb96ccb391f5df286864628f4618323 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 25 Sep 2020 15:28:03 -0700 Subject: [PATCH 03/37] initial commit --- hubconf.py | 4 +--- models/common.py | 31 +++++++++++++++++++++++++++---- models/yolo.py | 29 +++++++++++++++++++---------- 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/hubconf.py b/hubconf.py index 168b40502f25..e439530b7660 100644 --- a/hubconf.py +++ b/hubconf.py @@ -36,9 +36,7 @@ def create(name, pretrained, channels, classes): state_dict = torch.load(ckpt, map_location=torch.device('cpu'))['model'].float().state_dict() # to FP32 state_dict = {k: v for k, v in state_dict.items() if model.state_dict()[k].shape == v.shape} # filter model.load_state_dict(state_dict, strict=False) # load - - model.add_nms() # add NMS module - model.eval() + # model = model.autoshape() # cv2/PIL/np inference: predictions = model(cv2.imread('img.jpg')) return model except Exception as e: diff --git a/models/common.py b/models/common.py index 314c31f91aac..a54050337d14 100644 --- a/models/common.py +++ b/models/common.py @@ -1,9 +1,12 @@ # This file contains modules common to various models import math +import numpy as np import torch import torch.nn as nn -from utils.general import non_max_suppression + +from utils.datasets import letterbox +from utils.general import non_max_suppression, make_divisible, scale_coords def autopad(k, p=None): # kernel, padding @@ -101,17 +104,37 @@ def forward(self, x): class NMS(nn.Module): # Non-Maximum Suppression (NMS) module - conf = 0.3 # confidence threshold - iou = 0.6 # IoU threshold + conf = 0.25 # confidence threshold + iou = 0.45 # IoU threshold classes = None # (optional list) filter by class - def __init__(self, dimension=1): + def __init__(self): super(NMS, self).__init__() def forward(self, x): return non_max_suppression(x[0], conf_thres=self.conf, iou_thres=self.iou, classes=self.classes) +class autoShape(nn.Module): + # auto-reshape image size model wrapper + img_size = 640 # inference size (pixels) + + def __init__(self, model): + super(autoShape, self).__init__() + self.model = model + + def forward(self, x, shape=640, augment=False, profile=False): # x = cv2.imread('img.jpg') + x0shape = x.shape[:2] + p = next(self.model.parameters()) + x, ratio, (dw, dh) = letterbox(x, new_shape=make_divisible(shape or max(x0shape), int(self.stride.max()))) + x1shape = x.shape[:2] + x = np.ascontiguousarray(x[:, :, ::-1].transpose(2, 0, 1)) # BGR to RGB, to 3x640x640 + x = torch.from_numpy(x).to(p.device).type_as(p).unsqueeze(0) / 255. # uint8 to fp16/32 + x = self.model(x, augment, profile) # forward + x[0][:, :4] = scale_coords(x1shape, x[0][:, :4], x0shape) + return x + + class Flatten(nn.Module): # Use after nn.AdaptiveAvgPool2d(1) to remove last 2 dimensions @staticmethod diff --git a/models/yolo.py b/models/yolo.py index 46f1375e50aa..e7f9c8e99134 100644 --- a/models/yolo.py +++ b/models/yolo.py @@ -11,11 +11,11 @@ import torch import torch.nn as nn -from models.common import Conv, Bottleneck, SPP, DWConv, Focus, BottleneckCSP, Concat, NMS +from models.common import Conv, Bottleneck, SPP, DWConv, Focus, BottleneckCSP, Concat, NMS, autoShape from models.experimental import MixConv2d, CrossConv, C3 from utils.general import check_anchor_order, make_divisible, check_file, set_logging -from utils.torch_utils import ( - time_synchronized, fuse_conv_and_bn, model_info, scale_img, initialize_weights, select_device) +from utils.torch_utils import time_synchronized, fuse_conv_and_bn, model_info, scale_img, initialize_weights, \ + select_device, copy_attr class Detect(nn.Module): @@ -140,6 +140,7 @@ def forward_once(self, x, profile=False): return x def _initialize_biases(self, cf=None): # initialize biases into Detect(), cf is class frequency + # https://arxiv.org/abs/1708.02002 section 3.3 # cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1. m = self.model[-1] # Detect() module for mi, s in zip(m.m, m.stride): # from @@ -170,15 +171,27 @@ def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers self.info() return self - def add_nms(self): # fuse model Conv2d() + BatchNorm2d() layers - if type(self.model[-1]) is not NMS: # if missing NMS - print('Adding NMS module... ') + def nms(self, mode=True): # add or remove NMS module + present = type(self.model[-1]) is NMS # last layer is NMS + if mode and not present: + print('Adding NMS... ') m = NMS() # module m.f = -1 # from m.i = self.model[-1].i + 1 # index self.model.add_module(name='%s' % m.i, module=m) # add + self.eval() + elif not mode and present: + print('Removing NMS... ') + self.model = self.model[:-1] # remove return self + def autoshape(self): # add autoShape module + print('Adding autoShape... ') + self.nms() # add NMS + m = autoShape(self) # wrap model + copy_attr(m, self, include=('names', 'stride', 'nc', 'autoshape'), exclude=()) # copy attributes + return m + def info(self, verbose=False): # print model information model_info(self, verbose) @@ -263,10 +276,6 @@ def parse_model(d, ch): # model_dict, input_channels(3) # img = torch.rand(8 if torch.cuda.is_available() else 1, 3, 640, 640).to(device) # y = model(img, profile=True) - # ONNX export - # model.model[-1].export = True - # torch.onnx.export(model, img, opt.cfg.replace('.yaml', '.onnx'), verbose=True, opset_version=11) - # Tensorboard # from torch.utils.tensorboard import SummaryWriter # tb_writer = SummaryWriter() From 68c78a0738d577a6b6f847c748202c4bbc5d1c4b Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 26 Sep 2020 18:32:36 -0700 Subject: [PATCH 04/37] batch inference update --- models/common.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/models/common.py b/models/common.py index a54050337d14..e543d129d907 100644 --- a/models/common.py +++ b/models/common.py @@ -116,22 +116,38 @@ def forward(self, x): class autoShape(nn.Module): - # auto-reshape image size model wrapper + # auto-reshape input image model wrapper img_size = 640 # inference size (pixels) def __init__(self, model): super(autoShape, self).__init__() self.model = model - def forward(self, x, shape=640, augment=False, profile=False): # x = cv2.imread('img.jpg') - x0shape = x.shape[:2] - p = next(self.model.parameters()) - x, ratio, (dw, dh) = letterbox(x, new_shape=make_divisible(shape or max(x0shape), int(self.stride.max()))) - x1shape = x.shape[:2] - x = np.ascontiguousarray(x[:, :, ::-1].transpose(2, 0, 1)) # BGR to RGB, to 3x640x640 - x = torch.from_numpy(x).to(p.device).type_as(p).unsqueeze(0) / 255. # uint8 to fp16/32 + def forward(self, x, shape=640, augment=False, profile=False): + # x is cv2/np/PIL RGB image, or list of images for batched inference, i.e. x = Image.open('image.jpg') + p = next(self.model.parameters()) # for device and type + if not isinstance(x, list): + x = [x] + batch = range(len(x)) # batch size + + shape0, shape1 = [], [] # image and inference shapes + for i in batch: + x[i] = np.array(x[i])[:, :, :3] # up to 3 channels if png + s = x[i].shape[:2] # HWC + shape0.append(s) # image shape + g = (shape / max(s)) # gain + shape1.append([y * g for y in s]) + shape1 = [make_divisible(x, int(self.stride.max())) for x in np.stack(shape1, 0).max(0)] # inference shape + + x = [letterbox(x[i], new_shape=shape1, auto=False)[0] for i in batch] # pad + x = np.stack(x, 0) if batch[-1] else x[0][None] # stack + x = np.ascontiguousarray(x.transpose((0, 3, 1, 2))) # BHWC to BCHW + x = torch.from_numpy(x).to(p.device).type_as(p) / 255. # uint8 to fp16/32 + x = self.model(x, augment, profile) # forward - x[0][:, :4] = scale_coords(x1shape, x[0][:, :4], x0shape) + + for i in batch: + x[i][:, :4] = scale_coords(shape1, x[i][:, :4], shape0[i]) # postprocess return x From 623c5686b1aff3448b162a01b67156c382639710 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 10 Oct 2020 15:13:31 +0200 Subject: [PATCH 05/37] add torch capability --- models/common.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/models/common.py b/models/common.py index e543d129d907..90ec74f8f0ca 100644 --- a/models/common.py +++ b/models/common.py @@ -116,7 +116,7 @@ def forward(self, x): class autoShape(nn.Module): - # auto-reshape input image model wrapper + # input-robust model wrapper for passing cv2/np/PIL/torch inputs. Includes preprocessing, inference and NMS img_size = 640 # inference size (pixels) def __init__(self, model): @@ -124,13 +124,22 @@ def __init__(self, model): self.model = model def forward(self, x, shape=640, augment=False, profile=False): - # x is cv2/np/PIL RGB image, or list of images for batched inference, i.e. x = Image.open('image.jpg') + # supports inference from various sources. For height=720, width=1280, RGB color images example inputs are: + # opencv: x = cv2.imread('image.jpg')[:,:,::-1] # HWC BGR to RGB x(720,1280,3) + # PIL: x = Image.open('image.jpg') # HWC x(720,1280,3) + # numpy: x = np.zeros((720,1280,3)) # HWC + # torch: x = torch.zeros(16,3,720,1280) # BCHW + # multiple: x = [Image.open('image1.jpg'), Image.open('image2.jpg'), ...] # list of images + p = next(self.model.parameters()) # for device and type + if isinstance(x, torch.Tensor): # torch + return self.model(x.to(p.device).type_as(p), augment, profile) # inference + + # Pre-process if not isinstance(x, list): x = [x] - batch = range(len(x)) # batch size - shape0, shape1 = [], [] # image and inference shapes + batch = range(len(x)) # batch size for i in batch: x[i] = np.array(x[i])[:, :, :3] # up to 3 channels if png s = x[i].shape[:2] # HWC @@ -138,16 +147,17 @@ def forward(self, x, shape=640, augment=False, profile=False): g = (shape / max(s)) # gain shape1.append([y * g for y in s]) shape1 = [make_divisible(x, int(self.stride.max())) for x in np.stack(shape1, 0).max(0)] # inference shape - x = [letterbox(x[i], new_shape=shape1, auto=False)[0] for i in batch] # pad x = np.stack(x, 0) if batch[-1] else x[0][None] # stack x = np.ascontiguousarray(x.transpose((0, 3, 1, 2))) # BHWC to BCHW x = torch.from_numpy(x).to(p.device).type_as(p) / 255. # uint8 to fp16/32 - x = self.model(x, augment, profile) # forward + # Inference + x = self.model(x, augment, profile) # optional NMS() + # Post-process for i in batch: - x[i][:, :4] = scale_coords(shape1, x[i][:, :4], shape0[i]) # postprocess + x[i][:, :4] = scale_coords(shape1, x[i][:, :4], shape0[i]) return x From 097aca27577ff918ab526d0cc7a86e9e80e4bac3 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 10 Oct 2020 16:02:03 +0200 Subject: [PATCH 06/37] empty image bug fix --- models/common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/models/common.py b/models/common.py index 90ec74f8f0ca..4915bf302d6d 100644 --- a/models/common.py +++ b/models/common.py @@ -1,4 +1,5 @@ # This file contains modules common to various models + import math import numpy as np @@ -157,7 +158,8 @@ def forward(self, x, shape=640, augment=False, profile=False): # Post-process for i in batch: - x[i][:, :4] = scale_coords(shape1, x[i][:, :4], shape0[i]) + if x[i] is not None: + x[i][:, :4] = scale_coords(shape1, x[i][:, :4], shape0[i]) return x From 9896ce01c21874b5018ae669f3d842d47e8805fa Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 10 Oct 2020 16:02:19 +0200 Subject: [PATCH 07/37] comment update --- hubconf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hubconf.py b/hubconf.py index e439530b7660..94208e064784 100644 --- a/hubconf.py +++ b/hubconf.py @@ -10,7 +10,6 @@ import torch -from models.common import NMS from models.yolo import Model from utils.google_utils import attempt_download @@ -36,7 +35,7 @@ def create(name, pretrained, channels, classes): state_dict = torch.load(ckpt, map_location=torch.device('cpu'))['model'].float().state_dict() # to FP32 state_dict = {k: v for k, v in state_dict.items() if model.state_dict()[k].shape == v.shape} # filter model.load_state_dict(state_dict, strict=False) # load - # model = model.autoshape() # cv2/PIL/np inference: predictions = model(cv2.imread('img.jpg')) + # model = model.autoshape() # cv2/PIL/np/torch inference: predictions = model(Image.open('image.jpg')) return model except Exception as e: From 11ea3584687b5c317cf1084d7e847d9a78a89b51 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 10 Oct 2020 16:56:23 +0200 Subject: [PATCH 08/37] extract NMS to allow for augment --- models/common.py | 12 ++++++++---- models/yolo.py | 1 - 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/models/common.py b/models/common.py index 4915bf302d6d..6182814102ce 100644 --- a/models/common.py +++ b/models/common.py @@ -119,13 +119,16 @@ def forward(self, x): class autoShape(nn.Module): # input-robust model wrapper for passing cv2/np/PIL/torch inputs. Includes preprocessing, inference and NMS img_size = 640 # inference size (pixels) + conf = 0.25 # NMS confidence threshold + iou = 0.45 # NMS IoU threshold + classes = None # (optional list) filter by class def __init__(self, model): super(autoShape, self).__init__() self.model = model - def forward(self, x, shape=640, augment=False, profile=False): - # supports inference from various sources. For height=720, width=1280, RGB color images example inputs are: + def forward(self, x, size=640, augment=False, profile=False): + # supports inference from various sources. For height=720, width=1280, RGB images example inputs are: # opencv: x = cv2.imread('image.jpg')[:,:,::-1] # HWC BGR to RGB x(720,1280,3) # PIL: x = Image.open('image.jpg') # HWC x(720,1280,3) # numpy: x = np.zeros((720,1280,3)) # HWC @@ -145,7 +148,7 @@ def forward(self, x, shape=640, augment=False, profile=False): x[i] = np.array(x[i])[:, :, :3] # up to 3 channels if png s = x[i].shape[:2] # HWC shape0.append(s) # image shape - g = (shape / max(s)) # gain + g = (size / max(s)) # gain shape1.append([y * g for y in s]) shape1 = [make_divisible(x, int(self.stride.max())) for x in np.stack(shape1, 0).max(0)] # inference shape x = [letterbox(x[i], new_shape=shape1, auto=False)[0] for i in batch] # pad @@ -154,7 +157,8 @@ def forward(self, x, shape=640, augment=False, profile=False): x = torch.from_numpy(x).to(p.device).type_as(p) / 255. # uint8 to fp16/32 # Inference - x = self.model(x, augment, profile) # optional NMS() + x = self.model(x, augment, profile) # forward + x = non_max_suppression(x[0], conf_thres=self.conf, iou_thres=self.iou, classes=self.classes) # NMS # Post-process for i in batch: diff --git a/models/yolo.py b/models/yolo.py index e7f9c8e99134..0a13de8afc47 100644 --- a/models/yolo.py +++ b/models/yolo.py @@ -187,7 +187,6 @@ def nms(self, mode=True): # add or remove NMS module def autoshape(self): # add autoShape module print('Adding autoShape... ') - self.nms() # add NMS m = autoShape(self) # wrap model copy_attr(m, self, include=('names', 'stride', 'nc', 'autoshape'), exclude=()) # copy attributes return m From 82e865a10d31a1118e5cc8bee4028403ca0d525d Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 10 Oct 2020 17:29:36 +0200 Subject: [PATCH 09/37] update NMS thresholds to CoreML defaults --- detect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/detect.py b/detect.py index 67e4248a9509..4f4e0f6fe54d 100644 --- a/detect.py +++ b/detect.py @@ -150,8 +150,8 @@ def detect(save_img=False): parser.add_argument('--source', type=str, default='inference/images', help='source') # file/folder, 0 for webcam parser.add_argument('--output', type=str, default='inference/output', help='output folder') # output folder parser.add_argument('--img-size', type=int, default=640, help='inference size (pixels)') - parser.add_argument('--conf-thres', type=float, default=0.4, help='object confidence threshold') - parser.add_argument('--iou-thres', type=float, default=0.5, help='IOU threshold for NMS') + parser.add_argument('--conf-thres', type=float, default=0.25, help='object confidence threshold') + parser.add_argument('--iou-thres', type=float, default=0.45, help='IOU threshold for NMS') parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument('--view-img', action='store_true', help='display results') parser.add_argument('--save-txt', action='store_true', help='save results to *.txt') From c2403d785c5c215f5721446bff9b096e07d5cf84 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sun, 11 Oct 2020 00:42:54 +0200 Subject: [PATCH 10/37] fuse() bug fix --- models/yolo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/yolo.py b/models/yolo.py index 46f1375e50aa..a9dc539bf29f 100644 --- a/models/yolo.py +++ b/models/yolo.py @@ -162,7 +162,7 @@ def _print_biases(self): def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers print('Fusing layers... ') for m in self.model.modules(): - if type(m) is Conv and hasattr(Conv, 'bn'): + if type(m) is Conv and hasattr(m, 'bn'): m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatability m.conv = fuse_conv_and_bn(m.conv, m.bn) # update conv delattr(m, 'bn') # remove batchnorm From d87cf7ecb9024e286c33bf41796f1173a46e7a20 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sun, 11 Oct 2020 16:21:39 +0200 Subject: [PATCH 11/37] Update requirements.txt coremltools==4.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2ed5f48e94f6..0871ed666685 100755 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ tqdm>=4.41.0 # export -------------------------------------- # packaging # for coremltools -# coremltools==4.0b4 +# coremltools==4.0 # onnx>=1.7.0 # scikit-learn==0.19.2 # for coreml quantization From d45e349a1e65b2d3877e1339497ac0549cdecd4a Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sun, 11 Oct 2020 16:23:36 +0200 Subject: [PATCH 12/37] Rearrange export input after checks (#1118) img size checks are warnings rather than errors, so current implementation allows improperly formed model inputs. --- models/export.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/export.py b/models/export.py index 5eb885ddbcde..c5e96f13cee8 100644 --- a/models/export.py +++ b/models/export.py @@ -29,9 +29,6 @@ set_logging() t = time.time() - # Input - img = torch.zeros((opt.batch_size, 3, *opt.img_size)) # image size(1,3,320,192) iDetection - # Load PyTorch model model = attempt_load(opt.weights, map_location=torch.device('cpu')) # load FP32 model labels = model.names @@ -40,6 +37,9 @@ gs = int(max(model.stride)) # grid size (max stride) opt.img_size = [check_img_size(x, gs) for x in opt.img_size] # verify img_size are gs-multiples + # Input + img = torch.zeros(opt.batch_size, 3, *opt.img_size) # image size(1,3,320,192) iDetection + # Update model for k, m in model.named_modules(): m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatibility From 10c85bf4ebf51cdf7d974ce0212bcb420e0a66bb Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sun, 11 Oct 2020 16:25:14 +0200 Subject: [PATCH 13/37] FROM nvcr.io/nvidia/pytorch:20.09-py3 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8840596483fc..59b1a03d210c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Start FROM Nvidia PyTorch image https://ngc.nvidia.com/catalog/containers/nvidia:pytorch -FROM nvcr.io/nvidia/pytorch:20.08-py3 +FROM nvcr.io/nvidia/pytorch:20.09-py3 # Install dependencies RUN pip install --upgrade pip From 0ada058f6359b9c76569e9b501a5da7da0a11d74 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sun, 11 Oct 2020 17:25:17 +0200 Subject: [PATCH 14/37] Generalized regression criterion renaming (#1120) --- data/hyp.finetune.yaml | 2 +- data/hyp.scratch.yaml | 2 +- sotabench.py | 2 +- test.py | 2 +- train.py | 18 +++++++++--------- utils/general.py | 18 +++++++++--------- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/data/hyp.finetune.yaml b/data/hyp.finetune.yaml index fe9cd55019f7..1b84cff95c2c 100644 --- a/data/hyp.finetune.yaml +++ b/data/hyp.finetune.yaml @@ -15,7 +15,7 @@ weight_decay: 0.00036 warmup_epochs: 2.0 warmup_momentum: 0.5 warmup_bias_lr: 0.05 -giou: 0.0296 +box: 0.0296 cls: 0.243 cls_pw: 0.631 obj: 0.301 diff --git a/data/hyp.scratch.yaml b/data/hyp.scratch.yaml index 9f53e86dd3ab..43354316c095 100644 --- a/data/hyp.scratch.yaml +++ b/data/hyp.scratch.yaml @@ -10,7 +10,7 @@ weight_decay: 0.0005 # optimizer weight decay 5e-4 warmup_epochs: 3.0 # warmup epochs (fractions ok) warmup_momentum: 0.8 # warmup initial momentum warmup_bias_lr: 0.1 # warmup initial bias lr -giou: 0.05 # box loss gain +box: 0.05 # box loss gain cls: 0.5 # cls loss gain cls_pw: 1.0 # cls BCELoss positive_weight obj: 1.0 # obj loss gain (scale with pixels) diff --git a/sotabench.py b/sotabench.py index daef5168b213..96ea6bffcbb0 100644 --- a/sotabench.py +++ b/sotabench.py @@ -113,7 +113,7 @@ def test(data, # Compute loss if training: # if model has loss hyperparameters - loss += compute_loss([x.float() for x in train_out], targets, model)[1][:3] # GIoU, obj, cls + loss += compute_loss([x.float() for x in train_out], targets, model)[1][:3] # box, obj, cls # Run NMS t = time_synchronized() diff --git a/test.py b/test.py index e0bb7726f7d1..9e79a769f884 100644 --- a/test.py +++ b/test.py @@ -106,7 +106,7 @@ def test(data, # Compute loss if training: # if model has loss hyperparameters - loss += compute_loss([x.float() for x in train_out], targets, model)[1][:3] # GIoU, obj, cls + loss += compute_loss([x.float() for x in train_out], targets, model)[1][:3] # box, obj, cls # Run NMS t = time_synchronized() diff --git a/train.py b/train.py index 4060a5701a8b..bbb69c7e2b53 100644 --- a/train.py +++ b/train.py @@ -195,7 +195,7 @@ def train(hyp, opt, device, tb_writer=None): hyp['cls'] *= nc / 80. # scale coco-tuned hyp['cls'] to current dataset model.nc = nc # attach number of classes to model model.hyp = hyp # attach hyperparameters to model - model.gr = 1.0 # giou loss ratio (obj_loss = 1.0 or giou) + model.gr = 1.0 # iou loss ratio (obj_loss = 1.0 or iou) model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) # attach class weights model.names = names @@ -204,7 +204,7 @@ def train(hyp, opt, device, tb_writer=None): nw = max(round(hyp['warmup_epochs'] * nb), 1e3) # number of warmup iterations, max(3 epochs, 1k iterations) # nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training maps = np.zeros(nc) # mAP per class - results = (0, 0, 0, 0, 0, 0, 0) # 'P', 'R', 'mAP', 'F1', 'val GIoU', 'val Objectness', 'val Classification' + results = (0, 0, 0, 0, 0, 0, 0) # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls) scheduler.last_epoch = start_epoch - 1 # do not move scaler = amp.GradScaler(enabled=cuda) logger.info('Image sizes %g train, %g test\nUsing %g dataloader workers\nLogging results to %s\n' @@ -234,7 +234,7 @@ def train(hyp, opt, device, tb_writer=None): if rank != -1: dataloader.sampler.set_epoch(epoch) pbar = enumerate(dataloader) - logger.info(('\n' + '%10s' * 8) % ('Epoch', 'gpu_mem', 'GIoU', 'obj', 'cls', 'total', 'targets', 'img_size')) + logger.info(('\n' + '%10s' * 8) % ('Epoch', 'gpu_mem', 'box', 'obj', 'cls', 'total', 'targets', 'img_size')) if rank in [-1, 0]: pbar = tqdm(pbar, total=nb) # progress bar optimizer.zero_grad() @@ -245,7 +245,7 @@ def train(hyp, opt, device, tb_writer=None): # Warmup if ni <= nw: xi = [0, nw] # x interp - # model.gr = np.interp(ni, xi, [0.0, 1.0]) # giou loss ratio (obj_loss = 1.0 or giou) + # model.gr = np.interp(ni, xi, [0.0, 1.0]) # iou loss ratio (obj_loss = 1.0 or iou) accumulate = max(1, np.interp(ni, xi, [1, nbs / total_batch_size]).round()) for j, x in enumerate(optimizer.param_groups): # bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0 @@ -319,21 +319,21 @@ def train(hyp, opt, device, tb_writer=None): # Write with open(results_file, 'a') as f: - f.write(s + '%10.4g' * 7 % results + '\n') # P, R, mAP, F1, test_losses=(GIoU, obj, cls) + f.write(s + '%10.4g' * 7 % results + '\n') # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls) if len(opt.name) and opt.bucket: os.system('gsutil cp %s gs://%s/results/results%s.txt' % (results_file, opt.bucket, opt.name)) # Tensorboard if tb_writer: - tags = ['train/giou_loss', 'train/obj_loss', 'train/cls_loss', # train loss + tags = ['train/box_loss', 'train/obj_loss', 'train/cls_loss', # train loss 'metrics/precision', 'metrics/recall', 'metrics/mAP_0.5', 'metrics/mAP_0.5:0.95', - 'val/giou_loss', 'val/obj_loss', 'val/cls_loss', # val loss + 'val/box_loss', 'val/obj_loss', 'val/cls_loss', # val loss 'x/lr0', 'x/lr1', 'x/lr2'] # params for x, tag in zip(list(mloss[:-1]) + list(results) + lr, tags): tb_writer.add_scalar(tag, x, epoch) # Update best mAP - fi = fitness(np.array(results).reshape(1, -1)) # fitness_i = weighted combination of [P, R, mAP, F1] + fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95] if fi > best_fitness: best_fitness = fi @@ -463,7 +463,7 @@ def train(hyp, opt, device, tb_writer=None): 'warmup_epochs': (1, 0.0, 5.0), # warmup epochs (fractions ok) 'warmup_momentum': (1, 0.0, 0.95), # warmup initial momentum 'warmup_bias_lr': (1, 0.0, 0.2), # warmup initial bias lr - 'giou': (1, 0.02, 0.2), # GIoU loss gain + 'box': (1, 0.02, 0.2), # box loss gain 'cls': (1, 0.2, 4.0), # cls loss gain 'cls_pw': (1, 0.5, 2.0), # cls BCELoss positive_weight 'obj': (1, 0.2, 4.0), # obj loss gain (scale with pixels) diff --git a/utils/general.py b/utils/general.py index da016631e589..2530b10efb11 100755 --- a/utils/general.py +++ b/utils/general.py @@ -509,11 +509,11 @@ def compute_loss(p, targets, model): # predictions, targets, model pxy = ps[:, :2].sigmoid() * 2. - 0.5 pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i] pbox = torch.cat((pxy, pwh), 1).to(device) # predicted box - giou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True) # giou(prediction, target) - lbox += (1.0 - giou).mean() # giou loss + iou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True) # iou(prediction, target) + lbox += (1.0 - iou).mean() # iou loss # Objectness - tobj[b, a, gj, gi] = (1.0 - model.gr) + model.gr * giou.detach().clamp(0).type(tobj.dtype) # giou ratio + tobj[b, a, gj, gi] = (1.0 - model.gr) + model.gr * iou.detach().clamp(0).type(tobj.dtype) # iou ratio # Classification if model.nc > 1: # cls loss (only if multiple classes) @@ -528,7 +528,7 @@ def compute_loss(p, targets, model): # predictions, targets, model lobj += BCEobj(pi[..., 4], tobj) * balance[i] # obj loss s = 3 / np # output count scaling - lbox *= h['giou'] * s + lbox *= h['box'] * s lobj *= h['obj'] * s * (1.4 if np == 4 else 1.) lcls *= h['cls'] * s bs = tobj.shape[0] # batch size @@ -1234,7 +1234,7 @@ def plot_evolution(yaml_file='data/hyp.finetune.yaml'): # from utils.general im def plot_results_overlay(start=0, stop=0): # from utils.general import *; plot_results_overlay() # Plot training 'results*.txt', overlaying train and val losses s = ['train', 'train', 'train', 'Precision', 'mAP@0.5', 'val', 'val', 'val', 'Recall', 'mAP@0.5:0.95'] # legends - t = ['GIoU', 'Objectness', 'Classification', 'P-R', 'mAP-F1'] # titles + t = ['Box', 'Objectness', 'Classification', 'P-R', 'mAP-F1'] # titles for f in sorted(glob.glob('results*.txt') + glob.glob('../../Downloads/results*.txt')): results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T n = results.shape[1] # number of rows @@ -1254,13 +1254,13 @@ def plot_results_overlay(start=0, stop=0): # from utils.general import *; plot_ fig.savefig(f.replace('.txt', '.png'), dpi=200) -def plot_results(start=0, stop=0, bucket='', id=(), labels=(), - save_dir=''): # from utils.general import *; plot_results() +def plot_results(start=0, stop=0, bucket='', id=(), labels=(), save_dir=''): + # from utils.general import *; plot_results() # Plot training 'results*.txt' as seen in https://github.com/ultralytics/yolov5#reproduce-our-training fig, ax = plt.subplots(2, 5, figsize=(12, 6)) ax = ax.ravel() - s = ['GIoU', 'Objectness', 'Classification', 'Precision', 'Recall', - 'val GIoU', 'val Objectness', 'val Classification', 'mAP@0.5', 'mAP@0.5:0.95'] + s = ['Box', 'Objectness', 'Classification', 'Precision', 'Recall', + 'val Box', 'val Objectness', 'val Classification', 'mAP@0.5', 'mAP@0.5:0.95'] if bucket: # os.system('rm -rf storage.googleapis.com') # files = ['https://storage.googleapis.com/%s/results%g.txt' % (bucket, x) for x in id] From 00917a62250c16748bdf7491b9262b6d57c00e7b Mon Sep 17 00:00:00 2001 From: Jirka Borovec Date: Tue, 13 Oct 2020 14:10:21 +0200 Subject: [PATCH 15/37] update expt name comment and folder parsing for training (#978) * comment * fix parsing * fix evolve * folder * tqdm * Update train.py * Update train.py * reinstate anchors into meta dict anchor evolution is working correctly now * reinstate logger prefer the single line readout for concise logging, which helps simplify notebook and tutorials etc. Co-authored-by: Glenn Jocher --- train.py | 13 +++++++------ utils/general.py | 10 +++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/train.py b/train.py index bbb69c7e2b53..357c2c6434ad 100644 --- a/train.py +++ b/train.py @@ -207,7 +207,8 @@ def train(hyp, opt, device, tb_writer=None): results = (0, 0, 0, 0, 0, 0, 0) # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls) scheduler.last_epoch = start_epoch - 1 # do not move scaler = amp.GradScaler(enabled=cuda) - logger.info('Image sizes %g train, %g test\nUsing %g dataloader workers\nLogging results to %s\n' + logger.info('Image sizes %g train, %g test\n' + 'Using %g dataloader workers\nLogging results to %s\n' 'Starting training for %g epochs...' % (imgsz, imgsz_test, dataloader.num_workers, log_dir, epochs)) for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------ model.train() @@ -393,7 +394,7 @@ def train(hyp, opt, device, tb_writer=None): parser.add_argument('--bucket', type=str, default='', help='gsutil bucket') parser.add_argument('--cache-images', action='store_true', help='cache images for faster training') parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training') - parser.add_argument('--name', default='', help='renames results.txt to results_name.txt if supplied') + parser.add_argument('--name', default='', help='renames experiment folder exp{N} to exp{N}_{name} if supplied') parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%') parser.add_argument('--single-cls', action='store_true', help='train as single-class dataset') @@ -448,7 +449,7 @@ def train(hyp, opt, device, tb_writer=None): if not opt.evolve: tb_writer = None if opt.global_rank in [-1, 0]: - logger.info('Start Tensorboard with "tensorboard --logdir %s", view at http://localhost:6006/' % opt.logdir) + logger.info(f'Start Tensorboard with "tensorboard --logdir {opt.logdir}", view at http://localhost:6006/') tb_writer = SummaryWriter(log_dir=log_dir) # runs/exp0 train(hyp, opt, device, tb_writer) @@ -488,7 +489,7 @@ def train(hyp, opt, device, tb_writer=None): assert opt.local_rank == -1, 'DDP mode not implemented for --evolve' opt.notest, opt.nosave = True, True # only test/save final epoch # ei = [isinstance(x, (int, float)) for x in hyp.values()] # evolvable indices - yaml_file = Path('runs/evolve/hyp_evolved.yaml') # save best result here + yaml_file = Path(opt.logdir) / 'evolve' / 'hyp_evolved.yaml' # save best result here if opt.bucket: os.system('gsutil cp gs://%s/evolve.txt .' % opt.bucket) # download evolve.txt if exists @@ -532,5 +533,5 @@ def train(hyp, opt, device, tb_writer=None): # Plot results plot_evolution(yaml_file) - print('Hyperparameter evolution complete. Best results saved as: %s\nCommand to train a new model with these ' - 'hyperparameters: $ python train.py --hyp %s' % (yaml_file, yaml_file)) + print(f'Hyperparameter evolution complete. Best results saved as: {yaml_file}\n' + f'Command to train a new model with these hyperparameters: $ python train.py --hyp {yaml_file}') diff --git a/utils/general.py b/utils/general.py index 2530b10efb11..f58f7850be73 100755 --- a/utils/general.py +++ b/utils/general.py @@ -7,6 +7,7 @@ import shutil import subprocess import time +import re from contextlib import contextmanager from copy import copy from pathlib import Path @@ -952,9 +953,12 @@ def increment_dir(dir, comment=''): # Increments a directory runs/exp1 --> runs/exp2_comment n = 0 # number dir = str(Path(dir)) # os-agnostic - d = sorted(glob.glob(dir + '*')) # directories - if len(d): - n = max([int(x[len(dir):x.rfind('_') if '_' in Path(x).name else None]) for x in d]) + 1 # increment + dirs = sorted(glob.glob(dir + '*')) # directories + if dirs: + matches = [re.search(r"exp(\d+)", d) for d in dirs] + idxs = [int(m.groups()[0]) for m in matches if m] + if idxs: + n = max(idxs) + 1 # increment return dir + str(n) + ('_' + comment if comment else '') From 4346b13a40efd1c13f9bbdf3541893706f7deb23 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Tue, 13 Oct 2020 15:58:03 +0200 Subject: [PATCH 16/37] Dataset download bash script updates (#1132) --- data/scripts/get_coco.sh | 17 +++++---- data/scripts/get_voc.sh | 80 ++++++---------------------------------- 2 files changed, 22 insertions(+), 75 deletions(-) diff --git a/data/scripts/get_coco.sh b/data/scripts/get_coco.sh index 7f86377070a5..157a0b04cf86 100755 --- a/data/scripts/get_coco.sh +++ b/data/scripts/get_coco.sh @@ -8,14 +8,17 @@ # /yolov5 # Download/unzip labels -echo 'Downloading COCO 2017 labels ...' d='../' # unzip directory -f='coco2017labels.zip' && curl -L https://github.com/ultralytics/yolov5/releases/download/v1.0/$f -o $f -unzip -q $f -d $d && rm $f +url=https://github.com/ultralytics/yolov5/releases/download/v1.0/ +f='coco2017labels.zip' # 68 MB +echo 'Downloading' $url$f ' ...' && curl -L $url$f -o $f && unzip -q $f -d $d && rm $f # download, unzip, remove # Download/unzip images -echo 'Downloading COCO 2017 images ...' d='../coco/images' # unzip directory -f='train2017.zip' && curl http://images.cocodataset.org/zips/$f -o $f && unzip -q $f -d $d && rm $f # 19G, 118k images -f='val2017.zip' && curl http://images.cocodataset.org/zips/$f -o $f && unzip -q $f -d $d && rm $f # 1G, 5k images -# f='test2017.zip' && curl http://images.cocodataset.org/zips/$f -o $f && unzip -q $f -d $d && rm $f # 7G, 41k images +url=http://images.cocodataset.org/zips/ +f1='train2017.zip' # 19G, 118k images +f2='val2017.zip' # 1G, 5k images +f3='test2017.zip' # 7G, 41k images (optional) +for f in $f1 $f2; do + echo 'Downloading' $url$f ' ...' && curl -L $url$f -o $f && unzip -q $f -d $d && rm $f # download, unzip, remove +done diff --git a/data/scripts/get_voc.sh b/data/scripts/get_voc.sh index 5658864f2251..5e488c827c59 100644 --- a/data/scripts/get_voc.sh +++ b/data/scripts/get_voc.sh @@ -8,79 +8,23 @@ # /yolov5 start=$(date +%s) - -# handle optional download dir -if [ -z "$1" ]; then - # navigate to ~/tmp - echo "navigating to ../tmp/ ..." - mkdir -p ../tmp - cd ../tmp/ -else - # check if is valid directory - if [ ! -d $1 ]; then - echo $1 "is not a valid directory" - exit 0 - fi - echo "navigating to" $1 "..." - cd $1 -fi - -echo "Downloading VOC2007 trainval ..." -# Download data -curl -LO http://pjreddie.com/media/files/VOCtrainval_06-Nov-2007.tar -echo "Downloading VOC2007 test data ..." -curl -LO http://pjreddie.com/media/files/VOCtest_06-Nov-2007.tar -echo "Done downloading." - -# Extract data -echo "Extracting trainval ..." -tar -xf VOCtrainval_06-Nov-2007.tar -echo "Extracting test ..." -tar -xf VOCtest_06-Nov-2007.tar -echo "removing tars ..." -rm VOCtrainval_06-Nov-2007.tar -rm VOCtest_06-Nov-2007.tar - -end=$(date +%s) -runtime=$((end - start)) - -echo "Completed in" $runtime "seconds" - -start=$(date +%s) - -# handle optional download dir -if [ -z "$1" ]; then - # navigate to ~/tmp - echo "navigating to ../tmp/ ..." - mkdir -p ../tmp - cd ../tmp/ -else - # check if is valid directory - if [ ! -d $1 ]; then - echo $1 "is not a valid directory" - exit 0 - fi - echo "navigating to" $1 "..." - cd $1 -fi - -echo "Downloading VOC2012 trainval ..." -# Download data -curl -LO http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar -echo "Done downloading." - -# Extract data -echo "Extracting trainval ..." -tar -xf VOCtrainval_11-May-2012.tar -echo "removing tar ..." -rm VOCtrainval_11-May-2012.tar +mkdir -p ../tmp +cd ../tmp/ + +# Download/unzip images and labels +d='.' # unzip directory +url=https://github.com/ultralytics/yolov5/releases/download/v1.0/ +f1=VOCtrainval_06-Nov-2007.zip # 446MB, 5012 images +f2=VOCtest_06-Nov-2007.zip # 438MB, 4953 images +f3=VOCtrainval_11-May-2012.zip # 1.95GB, 17126 images +for f in $f1 $f2 $f3; do + echo 'Downloading' $url$f ' ...' && curl -L $url$f -o $f && unzip -q $f -d $d && rm $f # download, unzip, remove +done end=$(date +%s) runtime=$((end - start)) - echo "Completed in" $runtime "seconds" -cd ../tmp echo "Spliting dataset..." python3 - "$@" < Date: Tue, 13 Oct 2020 17:24:27 +0200 Subject: [PATCH 17/37] Minor import and spelling updates (#1133) --- detect.py | 1 - train.py | 2 +- utils/general.py | 12 ++++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/detect.py b/detect.py index 67e4248a9509..eee5f0208244 100644 --- a/detect.py +++ b/detect.py @@ -1,6 +1,5 @@ import argparse import os -import platform import shutil import time from pathlib import Path diff --git a/train.py b/train.py index 357c2c6434ad..80d9eec1e823 100644 --- a/train.py +++ b/train.py @@ -1,12 +1,12 @@ import argparse import logging -import math import os import random import shutil import time from pathlib import Path +import math import numpy as np import torch.distributed as dist import torch.nn.functional as F diff --git a/utils/general.py b/utils/general.py index f58f7850be73..3513d65cb4c6 100755 --- a/utils/general.py +++ b/utils/general.py @@ -143,7 +143,7 @@ def check_dataset(dict): if val and len(val): val = [os.path.abspath(x) for x in (val if isinstance(val, list) else [val])] # val path if not all(os.path.exists(x) for x in val): - print('\nWARNING: Dataset not found, nonexistant paths: %s' % [*val]) + print('\nWARNING: Dataset not found, nonexistent paths: %s' % [*val]) if s and len(s): # download script print('Downloading %s ...' % s) if s.startswith('http') and s.endswith('.zip'): # URL @@ -158,7 +158,7 @@ def check_dataset(dict): def make_divisible(x, divisor): - # Returns x evenly divisble by divisor + # Returns x evenly divisible by divisor return math.ceil(x / divisor) * divisor @@ -169,9 +169,9 @@ def labels_to_class_weights(labels, nc=80): labels = np.concatenate(labels, 0) # labels.shape = (866643, 5) for COCO classes = labels[:, 0].astype(np.int) # labels = [class xywh] - weights = np.bincount(classes, minlength=nc) # occurences per class + weights = np.bincount(classes, minlength=nc) # occurrences per class - # Prepend gridpoint count (for uCE trianing) + # Prepend gridpoint count (for uCE training) # gpi = ((320 / 32 * np.array([1, 2, 4])) ** 2 * 3).sum() # gridpoints per image # weights = np.hstack([gpi * len(labels) - weights.sum() * 9, weights * 9]) ** 0.5 # prepend gridpoints to start @@ -820,7 +820,7 @@ def print_results(k): k, dist = kmeans(wh / s, n, iter=30) # points, mean distance k *= s wh = torch.tensor(wh, dtype=torch.float32) # filtered - wh0 = torch.tensor(wh0, dtype=torch.float32) # unflitered + wh0 = torch.tensor(wh0, dtype=torch.float32) # unfiltered k = print_results(k) # Plot @@ -1281,7 +1281,7 @@ def plot_results(start=0, stop=0, bucket='', id=(), labels=(), save_dir=''): for i in range(10): y = results[i, x] if i in [0, 1, 2, 5, 6, 7]: - y[y == 0] = np.nan # dont show zero loss values + y[y == 0] = np.nan # don't show zero loss values # y /= y[0] # normalize label = labels[fi] if len(labels) else Path(f).stem ax[i].plot(x, y, marker='.', label=label, linewidth=1, markersize=6) From c67e72200e8b5caab6b2b2fa560cdb647b46f004 Mon Sep 17 00:00:00 2001 From: Jirka Borovec Date: Thu, 15 Oct 2020 15:05:58 +0200 Subject: [PATCH 18/37] fix compatibility for hyper config (#1146) * fix/hyper * Hyp giou check to train.py * restore general.py * train.py overwrite fix * restore general.py and pep8 update Co-authored-by: Glenn Jocher --- train.py | 11 ++++++++--- utils/general.py | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/train.py b/train.py index 80d9eec1e823..42774b880fc6 100644 --- a/train.py +++ b/train.py @@ -5,6 +5,7 @@ import shutil import time from pathlib import Path +from warnings import warn import math import numpy as np @@ -430,9 +431,8 @@ def train(hyp, opt, device, tb_writer=None): opt.img_size.extend([opt.img_size[-1]] * (2 - len(opt.img_size))) # extend to 2 sizes (train, test) log_dir = increment_dir(Path(opt.logdir) / 'exp', opt.name) # runs/exp1 - device = select_device(opt.device, batch_size=opt.batch_size) - # DDP mode + device = select_device(opt.device, batch_size=opt.batch_size) if opt.local_rank != -1: assert torch.cuda.device_count() > opt.local_rank torch.cuda.set_device(opt.local_rank) @@ -441,11 +441,16 @@ def train(hyp, opt, device, tb_writer=None): assert opt.batch_size % opt.world_size == 0, '--batch-size must be multiple of CUDA device count' opt.batch_size = opt.total_batch_size // opt.world_size - logger.info(opt) + # Hyperparameters with open(opt.hyp) as f: hyp = yaml.load(f, Loader=yaml.FullLoader) # load hyps + if 'box' not in hyp: + warn('Compatibility: %s missing "box" which was renamed from "giou" in %s' % + (opt.hyp, 'https://github.com/ultralytics/yolov5/pull/1120')) + hyp['box'] = hyp.pop('giou') # Train + logger.info(opt) if not opt.evolve: tb_writer = None if opt.global_rank in [-1, 0]: diff --git a/utils/general.py b/utils/general.py index 3513d65cb4c6..f8415feef999 100755 --- a/utils/general.py +++ b/utils/general.py @@ -1,18 +1,18 @@ import glob import logging -import math import os import platform import random +import re import shutil import subprocess import time -import re from contextlib import contextmanager from copy import copy from pathlib import Path import cv2 +import math import matplotlib import matplotlib.pyplot as plt import numpy as np From fe1d90a4d7e08effc92328a586fe0a95604d6c94 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sun, 11 Oct 2020 00:42:54 +0200 Subject: [PATCH 19/37] fuse() bug fix --- models/yolo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/yolo.py b/models/yolo.py index 0a13de8afc47..8774bea2863f 100644 --- a/models/yolo.py +++ b/models/yolo.py @@ -163,7 +163,7 @@ def _print_biases(self): def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers print('Fusing layers... ') for m in self.model.modules(): - if type(m) is Conv and hasattr(Conv, 'bn'): + if type(m) is Conv and hasattr(m, 'bn'): m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatability m.conv = fuse_conv_and_bn(m.conv, m.bn) # update conv delattr(m, 'bn') # remove batchnorm From 70432a55ec897ee51375a659e9c05db9b8216e9a Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sun, 11 Oct 2020 16:21:39 +0200 Subject: [PATCH 20/37] Update requirements.txt coremltools==4.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2ed5f48e94f6..0871ed666685 100755 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ tqdm>=4.41.0 # export -------------------------------------- # packaging # for coremltools -# coremltools==4.0b4 +# coremltools==4.0 # onnx>=1.7.0 # scikit-learn==0.19.2 # for coreml quantization From e63bf4dc4db5f3196101358d297f45c475b936b5 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sun, 11 Oct 2020 16:23:36 +0200 Subject: [PATCH 21/37] Rearrange export input after checks (#1118) img size checks are warnings rather than errors, so current implementation allows improperly formed model inputs. --- models/export.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/export.py b/models/export.py index 5eb885ddbcde..c5e96f13cee8 100644 --- a/models/export.py +++ b/models/export.py @@ -29,9 +29,6 @@ set_logging() t = time.time() - # Input - img = torch.zeros((opt.batch_size, 3, *opt.img_size)) # image size(1,3,320,192) iDetection - # Load PyTorch model model = attempt_load(opt.weights, map_location=torch.device('cpu')) # load FP32 model labels = model.names @@ -40,6 +37,9 @@ gs = int(max(model.stride)) # grid size (max stride) opt.img_size = [check_img_size(x, gs) for x in opt.img_size] # verify img_size are gs-multiples + # Input + img = torch.zeros(opt.batch_size, 3, *opt.img_size) # image size(1,3,320,192) iDetection + # Update model for k, m in model.named_modules(): m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatibility From bfa2f89f1d80a4b938ebe5693c743ee778f560e7 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sun, 11 Oct 2020 16:25:14 +0200 Subject: [PATCH 22/37] FROM nvcr.io/nvidia/pytorch:20.09-py3 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8840596483fc..59b1a03d210c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Start FROM Nvidia PyTorch image https://ngc.nvidia.com/catalog/containers/nvidia:pytorch -FROM nvcr.io/nvidia/pytorch:20.08-py3 +FROM nvcr.io/nvidia/pytorch:20.09-py3 # Install dependencies RUN pip install --upgrade pip From 402095a177a2d76747d2af8b39010643058d43bd Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sun, 11 Oct 2020 17:25:17 +0200 Subject: [PATCH 23/37] Generalized regression criterion renaming (#1120) --- data/hyp.finetune.yaml | 2 +- data/hyp.scratch.yaml | 2 +- sotabench.py | 2 +- test.py | 2 +- train.py | 18 +++++++++--------- utils/general.py | 18 +++++++++--------- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/data/hyp.finetune.yaml b/data/hyp.finetune.yaml index fe9cd55019f7..1b84cff95c2c 100644 --- a/data/hyp.finetune.yaml +++ b/data/hyp.finetune.yaml @@ -15,7 +15,7 @@ weight_decay: 0.00036 warmup_epochs: 2.0 warmup_momentum: 0.5 warmup_bias_lr: 0.05 -giou: 0.0296 +box: 0.0296 cls: 0.243 cls_pw: 0.631 obj: 0.301 diff --git a/data/hyp.scratch.yaml b/data/hyp.scratch.yaml index 9f53e86dd3ab..43354316c095 100644 --- a/data/hyp.scratch.yaml +++ b/data/hyp.scratch.yaml @@ -10,7 +10,7 @@ weight_decay: 0.0005 # optimizer weight decay 5e-4 warmup_epochs: 3.0 # warmup epochs (fractions ok) warmup_momentum: 0.8 # warmup initial momentum warmup_bias_lr: 0.1 # warmup initial bias lr -giou: 0.05 # box loss gain +box: 0.05 # box loss gain cls: 0.5 # cls loss gain cls_pw: 1.0 # cls BCELoss positive_weight obj: 1.0 # obj loss gain (scale with pixels) diff --git a/sotabench.py b/sotabench.py index daef5168b213..96ea6bffcbb0 100644 --- a/sotabench.py +++ b/sotabench.py @@ -113,7 +113,7 @@ def test(data, # Compute loss if training: # if model has loss hyperparameters - loss += compute_loss([x.float() for x in train_out], targets, model)[1][:3] # GIoU, obj, cls + loss += compute_loss([x.float() for x in train_out], targets, model)[1][:3] # box, obj, cls # Run NMS t = time_synchronized() diff --git a/test.py b/test.py index e0bb7726f7d1..9e79a769f884 100644 --- a/test.py +++ b/test.py @@ -106,7 +106,7 @@ def test(data, # Compute loss if training: # if model has loss hyperparameters - loss += compute_loss([x.float() for x in train_out], targets, model)[1][:3] # GIoU, obj, cls + loss += compute_loss([x.float() for x in train_out], targets, model)[1][:3] # box, obj, cls # Run NMS t = time_synchronized() diff --git a/train.py b/train.py index 4060a5701a8b..bbb69c7e2b53 100644 --- a/train.py +++ b/train.py @@ -195,7 +195,7 @@ def train(hyp, opt, device, tb_writer=None): hyp['cls'] *= nc / 80. # scale coco-tuned hyp['cls'] to current dataset model.nc = nc # attach number of classes to model model.hyp = hyp # attach hyperparameters to model - model.gr = 1.0 # giou loss ratio (obj_loss = 1.0 or giou) + model.gr = 1.0 # iou loss ratio (obj_loss = 1.0 or iou) model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) # attach class weights model.names = names @@ -204,7 +204,7 @@ def train(hyp, opt, device, tb_writer=None): nw = max(round(hyp['warmup_epochs'] * nb), 1e3) # number of warmup iterations, max(3 epochs, 1k iterations) # nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training maps = np.zeros(nc) # mAP per class - results = (0, 0, 0, 0, 0, 0, 0) # 'P', 'R', 'mAP', 'F1', 'val GIoU', 'val Objectness', 'val Classification' + results = (0, 0, 0, 0, 0, 0, 0) # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls) scheduler.last_epoch = start_epoch - 1 # do not move scaler = amp.GradScaler(enabled=cuda) logger.info('Image sizes %g train, %g test\nUsing %g dataloader workers\nLogging results to %s\n' @@ -234,7 +234,7 @@ def train(hyp, opt, device, tb_writer=None): if rank != -1: dataloader.sampler.set_epoch(epoch) pbar = enumerate(dataloader) - logger.info(('\n' + '%10s' * 8) % ('Epoch', 'gpu_mem', 'GIoU', 'obj', 'cls', 'total', 'targets', 'img_size')) + logger.info(('\n' + '%10s' * 8) % ('Epoch', 'gpu_mem', 'box', 'obj', 'cls', 'total', 'targets', 'img_size')) if rank in [-1, 0]: pbar = tqdm(pbar, total=nb) # progress bar optimizer.zero_grad() @@ -245,7 +245,7 @@ def train(hyp, opt, device, tb_writer=None): # Warmup if ni <= nw: xi = [0, nw] # x interp - # model.gr = np.interp(ni, xi, [0.0, 1.0]) # giou loss ratio (obj_loss = 1.0 or giou) + # model.gr = np.interp(ni, xi, [0.0, 1.0]) # iou loss ratio (obj_loss = 1.0 or iou) accumulate = max(1, np.interp(ni, xi, [1, nbs / total_batch_size]).round()) for j, x in enumerate(optimizer.param_groups): # bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0 @@ -319,21 +319,21 @@ def train(hyp, opt, device, tb_writer=None): # Write with open(results_file, 'a') as f: - f.write(s + '%10.4g' * 7 % results + '\n') # P, R, mAP, F1, test_losses=(GIoU, obj, cls) + f.write(s + '%10.4g' * 7 % results + '\n') # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls) if len(opt.name) and opt.bucket: os.system('gsutil cp %s gs://%s/results/results%s.txt' % (results_file, opt.bucket, opt.name)) # Tensorboard if tb_writer: - tags = ['train/giou_loss', 'train/obj_loss', 'train/cls_loss', # train loss + tags = ['train/box_loss', 'train/obj_loss', 'train/cls_loss', # train loss 'metrics/precision', 'metrics/recall', 'metrics/mAP_0.5', 'metrics/mAP_0.5:0.95', - 'val/giou_loss', 'val/obj_loss', 'val/cls_loss', # val loss + 'val/box_loss', 'val/obj_loss', 'val/cls_loss', # val loss 'x/lr0', 'x/lr1', 'x/lr2'] # params for x, tag in zip(list(mloss[:-1]) + list(results) + lr, tags): tb_writer.add_scalar(tag, x, epoch) # Update best mAP - fi = fitness(np.array(results).reshape(1, -1)) # fitness_i = weighted combination of [P, R, mAP, F1] + fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95] if fi > best_fitness: best_fitness = fi @@ -463,7 +463,7 @@ def train(hyp, opt, device, tb_writer=None): 'warmup_epochs': (1, 0.0, 5.0), # warmup epochs (fractions ok) 'warmup_momentum': (1, 0.0, 0.95), # warmup initial momentum 'warmup_bias_lr': (1, 0.0, 0.2), # warmup initial bias lr - 'giou': (1, 0.02, 0.2), # GIoU loss gain + 'box': (1, 0.02, 0.2), # box loss gain 'cls': (1, 0.2, 4.0), # cls loss gain 'cls_pw': (1, 0.5, 2.0), # cls BCELoss positive_weight 'obj': (1, 0.2, 4.0), # obj loss gain (scale with pixels) diff --git a/utils/general.py b/utils/general.py index da016631e589..2530b10efb11 100755 --- a/utils/general.py +++ b/utils/general.py @@ -509,11 +509,11 @@ def compute_loss(p, targets, model): # predictions, targets, model pxy = ps[:, :2].sigmoid() * 2. - 0.5 pwh = (ps[:, 2:4].sigmoid() * 2) ** 2 * anchors[i] pbox = torch.cat((pxy, pwh), 1).to(device) # predicted box - giou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True) # giou(prediction, target) - lbox += (1.0 - giou).mean() # giou loss + iou = bbox_iou(pbox.T, tbox[i], x1y1x2y2=False, CIoU=True) # iou(prediction, target) + lbox += (1.0 - iou).mean() # iou loss # Objectness - tobj[b, a, gj, gi] = (1.0 - model.gr) + model.gr * giou.detach().clamp(0).type(tobj.dtype) # giou ratio + tobj[b, a, gj, gi] = (1.0 - model.gr) + model.gr * iou.detach().clamp(0).type(tobj.dtype) # iou ratio # Classification if model.nc > 1: # cls loss (only if multiple classes) @@ -528,7 +528,7 @@ def compute_loss(p, targets, model): # predictions, targets, model lobj += BCEobj(pi[..., 4], tobj) * balance[i] # obj loss s = 3 / np # output count scaling - lbox *= h['giou'] * s + lbox *= h['box'] * s lobj *= h['obj'] * s * (1.4 if np == 4 else 1.) lcls *= h['cls'] * s bs = tobj.shape[0] # batch size @@ -1234,7 +1234,7 @@ def plot_evolution(yaml_file='data/hyp.finetune.yaml'): # from utils.general im def plot_results_overlay(start=0, stop=0): # from utils.general import *; plot_results_overlay() # Plot training 'results*.txt', overlaying train and val losses s = ['train', 'train', 'train', 'Precision', 'mAP@0.5', 'val', 'val', 'val', 'Recall', 'mAP@0.5:0.95'] # legends - t = ['GIoU', 'Objectness', 'Classification', 'P-R', 'mAP-F1'] # titles + t = ['Box', 'Objectness', 'Classification', 'P-R', 'mAP-F1'] # titles for f in sorted(glob.glob('results*.txt') + glob.glob('../../Downloads/results*.txt')): results = np.loadtxt(f, usecols=[2, 3, 4, 8, 9, 12, 13, 14, 10, 11], ndmin=2).T n = results.shape[1] # number of rows @@ -1254,13 +1254,13 @@ def plot_results_overlay(start=0, stop=0): # from utils.general import *; plot_ fig.savefig(f.replace('.txt', '.png'), dpi=200) -def plot_results(start=0, stop=0, bucket='', id=(), labels=(), - save_dir=''): # from utils.general import *; plot_results() +def plot_results(start=0, stop=0, bucket='', id=(), labels=(), save_dir=''): + # from utils.general import *; plot_results() # Plot training 'results*.txt' as seen in https://github.com/ultralytics/yolov5#reproduce-our-training fig, ax = plt.subplots(2, 5, figsize=(12, 6)) ax = ax.ravel() - s = ['GIoU', 'Objectness', 'Classification', 'Precision', 'Recall', - 'val GIoU', 'val Objectness', 'val Classification', 'mAP@0.5', 'mAP@0.5:0.95'] + s = ['Box', 'Objectness', 'Classification', 'Precision', 'Recall', + 'val Box', 'val Objectness', 'val Classification', 'mAP@0.5', 'mAP@0.5:0.95'] if bucket: # os.system('rm -rf storage.googleapis.com') # files = ['https://storage.googleapis.com/%s/results%g.txt' % (bucket, x) for x in id] From 73638720ee00c3dff2ae3fc6623e8e710fb8cef4 Mon Sep 17 00:00:00 2001 From: Jirka Borovec Date: Tue, 13 Oct 2020 14:10:21 +0200 Subject: [PATCH 24/37] update expt name comment and folder parsing for training (#978) * comment * fix parsing * fix evolve * folder * tqdm * Update train.py * Update train.py * reinstate anchors into meta dict anchor evolution is working correctly now * reinstate logger prefer the single line readout for concise logging, which helps simplify notebook and tutorials etc. Co-authored-by: Glenn Jocher --- train.py | 13 +++++++------ utils/general.py | 10 +++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/train.py b/train.py index bbb69c7e2b53..357c2c6434ad 100644 --- a/train.py +++ b/train.py @@ -207,7 +207,8 @@ def train(hyp, opt, device, tb_writer=None): results = (0, 0, 0, 0, 0, 0, 0) # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls) scheduler.last_epoch = start_epoch - 1 # do not move scaler = amp.GradScaler(enabled=cuda) - logger.info('Image sizes %g train, %g test\nUsing %g dataloader workers\nLogging results to %s\n' + logger.info('Image sizes %g train, %g test\n' + 'Using %g dataloader workers\nLogging results to %s\n' 'Starting training for %g epochs...' % (imgsz, imgsz_test, dataloader.num_workers, log_dir, epochs)) for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------ model.train() @@ -393,7 +394,7 @@ def train(hyp, opt, device, tb_writer=None): parser.add_argument('--bucket', type=str, default='', help='gsutil bucket') parser.add_argument('--cache-images', action='store_true', help='cache images for faster training') parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training') - parser.add_argument('--name', default='', help='renames results.txt to results_name.txt if supplied') + parser.add_argument('--name', default='', help='renames experiment folder exp{N} to exp{N}_{name} if supplied') parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%') parser.add_argument('--single-cls', action='store_true', help='train as single-class dataset') @@ -448,7 +449,7 @@ def train(hyp, opt, device, tb_writer=None): if not opt.evolve: tb_writer = None if opt.global_rank in [-1, 0]: - logger.info('Start Tensorboard with "tensorboard --logdir %s", view at http://localhost:6006/' % opt.logdir) + logger.info(f'Start Tensorboard with "tensorboard --logdir {opt.logdir}", view at http://localhost:6006/') tb_writer = SummaryWriter(log_dir=log_dir) # runs/exp0 train(hyp, opt, device, tb_writer) @@ -488,7 +489,7 @@ def train(hyp, opt, device, tb_writer=None): assert opt.local_rank == -1, 'DDP mode not implemented for --evolve' opt.notest, opt.nosave = True, True # only test/save final epoch # ei = [isinstance(x, (int, float)) for x in hyp.values()] # evolvable indices - yaml_file = Path('runs/evolve/hyp_evolved.yaml') # save best result here + yaml_file = Path(opt.logdir) / 'evolve' / 'hyp_evolved.yaml' # save best result here if opt.bucket: os.system('gsutil cp gs://%s/evolve.txt .' % opt.bucket) # download evolve.txt if exists @@ -532,5 +533,5 @@ def train(hyp, opt, device, tb_writer=None): # Plot results plot_evolution(yaml_file) - print('Hyperparameter evolution complete. Best results saved as: %s\nCommand to train a new model with these ' - 'hyperparameters: $ python train.py --hyp %s' % (yaml_file, yaml_file)) + print(f'Hyperparameter evolution complete. Best results saved as: {yaml_file}\n' + f'Command to train a new model with these hyperparameters: $ python train.py --hyp {yaml_file}') diff --git a/utils/general.py b/utils/general.py index 2530b10efb11..f58f7850be73 100755 --- a/utils/general.py +++ b/utils/general.py @@ -7,6 +7,7 @@ import shutil import subprocess import time +import re from contextlib import contextmanager from copy import copy from pathlib import Path @@ -952,9 +953,12 @@ def increment_dir(dir, comment=''): # Increments a directory runs/exp1 --> runs/exp2_comment n = 0 # number dir = str(Path(dir)) # os-agnostic - d = sorted(glob.glob(dir + '*')) # directories - if len(d): - n = max([int(x[len(dir):x.rfind('_') if '_' in Path(x).name else None]) for x in d]) + 1 # increment + dirs = sorted(glob.glob(dir + '*')) # directories + if dirs: + matches = [re.search(r"exp(\d+)", d) for d in dirs] + idxs = [int(m.groups()[0]) for m in matches if m] + if idxs: + n = max(idxs) + 1 # increment return dir + str(n) + ('_' + comment if comment else '') From 6088171f1698895f027ad5ab8284ec0a771d615b Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Tue, 13 Oct 2020 15:58:03 +0200 Subject: [PATCH 25/37] Dataset download bash script updates (#1132) --- data/scripts/get_coco.sh | 17 +++++---- data/scripts/get_voc.sh | 80 ++++++---------------------------------- 2 files changed, 22 insertions(+), 75 deletions(-) diff --git a/data/scripts/get_coco.sh b/data/scripts/get_coco.sh index 7f86377070a5..157a0b04cf86 100755 --- a/data/scripts/get_coco.sh +++ b/data/scripts/get_coco.sh @@ -8,14 +8,17 @@ # /yolov5 # Download/unzip labels -echo 'Downloading COCO 2017 labels ...' d='../' # unzip directory -f='coco2017labels.zip' && curl -L https://github.com/ultralytics/yolov5/releases/download/v1.0/$f -o $f -unzip -q $f -d $d && rm $f +url=https://github.com/ultralytics/yolov5/releases/download/v1.0/ +f='coco2017labels.zip' # 68 MB +echo 'Downloading' $url$f ' ...' && curl -L $url$f -o $f && unzip -q $f -d $d && rm $f # download, unzip, remove # Download/unzip images -echo 'Downloading COCO 2017 images ...' d='../coco/images' # unzip directory -f='train2017.zip' && curl http://images.cocodataset.org/zips/$f -o $f && unzip -q $f -d $d && rm $f # 19G, 118k images -f='val2017.zip' && curl http://images.cocodataset.org/zips/$f -o $f && unzip -q $f -d $d && rm $f # 1G, 5k images -# f='test2017.zip' && curl http://images.cocodataset.org/zips/$f -o $f && unzip -q $f -d $d && rm $f # 7G, 41k images +url=http://images.cocodataset.org/zips/ +f1='train2017.zip' # 19G, 118k images +f2='val2017.zip' # 1G, 5k images +f3='test2017.zip' # 7G, 41k images (optional) +for f in $f1 $f2; do + echo 'Downloading' $url$f ' ...' && curl -L $url$f -o $f && unzip -q $f -d $d && rm $f # download, unzip, remove +done diff --git a/data/scripts/get_voc.sh b/data/scripts/get_voc.sh index 5658864f2251..5e488c827c59 100644 --- a/data/scripts/get_voc.sh +++ b/data/scripts/get_voc.sh @@ -8,79 +8,23 @@ # /yolov5 start=$(date +%s) - -# handle optional download dir -if [ -z "$1" ]; then - # navigate to ~/tmp - echo "navigating to ../tmp/ ..." - mkdir -p ../tmp - cd ../tmp/ -else - # check if is valid directory - if [ ! -d $1 ]; then - echo $1 "is not a valid directory" - exit 0 - fi - echo "navigating to" $1 "..." - cd $1 -fi - -echo "Downloading VOC2007 trainval ..." -# Download data -curl -LO http://pjreddie.com/media/files/VOCtrainval_06-Nov-2007.tar -echo "Downloading VOC2007 test data ..." -curl -LO http://pjreddie.com/media/files/VOCtest_06-Nov-2007.tar -echo "Done downloading." - -# Extract data -echo "Extracting trainval ..." -tar -xf VOCtrainval_06-Nov-2007.tar -echo "Extracting test ..." -tar -xf VOCtest_06-Nov-2007.tar -echo "removing tars ..." -rm VOCtrainval_06-Nov-2007.tar -rm VOCtest_06-Nov-2007.tar - -end=$(date +%s) -runtime=$((end - start)) - -echo "Completed in" $runtime "seconds" - -start=$(date +%s) - -# handle optional download dir -if [ -z "$1" ]; then - # navigate to ~/tmp - echo "navigating to ../tmp/ ..." - mkdir -p ../tmp - cd ../tmp/ -else - # check if is valid directory - if [ ! -d $1 ]; then - echo $1 "is not a valid directory" - exit 0 - fi - echo "navigating to" $1 "..." - cd $1 -fi - -echo "Downloading VOC2012 trainval ..." -# Download data -curl -LO http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar -echo "Done downloading." - -# Extract data -echo "Extracting trainval ..." -tar -xf VOCtrainval_11-May-2012.tar -echo "removing tar ..." -rm VOCtrainval_11-May-2012.tar +mkdir -p ../tmp +cd ../tmp/ + +# Download/unzip images and labels +d='.' # unzip directory +url=https://github.com/ultralytics/yolov5/releases/download/v1.0/ +f1=VOCtrainval_06-Nov-2007.zip # 446MB, 5012 images +f2=VOCtest_06-Nov-2007.zip # 438MB, 4953 images +f3=VOCtrainval_11-May-2012.zip # 1.95GB, 17126 images +for f in $f1 $f2 $f3; do + echo 'Downloading' $url$f ' ...' && curl -L $url$f -o $f && unzip -q $f -d $d && rm $f # download, unzip, remove +done end=$(date +%s) runtime=$((end - start)) - echo "Completed in" $runtime "seconds" -cd ../tmp echo "Spliting dataset..." python3 - "$@" < Date: Tue, 13 Oct 2020 17:24:27 +0200 Subject: [PATCH 26/37] Minor import and spelling updates (#1133) --- detect.py | 1 - train.py | 2 +- utils/general.py | 12 ++++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/detect.py b/detect.py index 4f4e0f6fe54d..a98fe7394854 100644 --- a/detect.py +++ b/detect.py @@ -1,6 +1,5 @@ import argparse import os -import platform import shutil import time from pathlib import Path diff --git a/train.py b/train.py index 357c2c6434ad..80d9eec1e823 100644 --- a/train.py +++ b/train.py @@ -1,12 +1,12 @@ import argparse import logging -import math import os import random import shutil import time from pathlib import Path +import math import numpy as np import torch.distributed as dist import torch.nn.functional as F diff --git a/utils/general.py b/utils/general.py index f58f7850be73..3513d65cb4c6 100755 --- a/utils/general.py +++ b/utils/general.py @@ -143,7 +143,7 @@ def check_dataset(dict): if val and len(val): val = [os.path.abspath(x) for x in (val if isinstance(val, list) else [val])] # val path if not all(os.path.exists(x) for x in val): - print('\nWARNING: Dataset not found, nonexistant paths: %s' % [*val]) + print('\nWARNING: Dataset not found, nonexistent paths: %s' % [*val]) if s and len(s): # download script print('Downloading %s ...' % s) if s.startswith('http') and s.endswith('.zip'): # URL @@ -158,7 +158,7 @@ def check_dataset(dict): def make_divisible(x, divisor): - # Returns x evenly divisble by divisor + # Returns x evenly divisible by divisor return math.ceil(x / divisor) * divisor @@ -169,9 +169,9 @@ def labels_to_class_weights(labels, nc=80): labels = np.concatenate(labels, 0) # labels.shape = (866643, 5) for COCO classes = labels[:, 0].astype(np.int) # labels = [class xywh] - weights = np.bincount(classes, minlength=nc) # occurences per class + weights = np.bincount(classes, minlength=nc) # occurrences per class - # Prepend gridpoint count (for uCE trianing) + # Prepend gridpoint count (for uCE training) # gpi = ((320 / 32 * np.array([1, 2, 4])) ** 2 * 3).sum() # gridpoints per image # weights = np.hstack([gpi * len(labels) - weights.sum() * 9, weights * 9]) ** 0.5 # prepend gridpoints to start @@ -820,7 +820,7 @@ def print_results(k): k, dist = kmeans(wh / s, n, iter=30) # points, mean distance k *= s wh = torch.tensor(wh, dtype=torch.float32) # filtered - wh0 = torch.tensor(wh0, dtype=torch.float32) # unflitered + wh0 = torch.tensor(wh0, dtype=torch.float32) # unfiltered k = print_results(k) # Plot @@ -1281,7 +1281,7 @@ def plot_results(start=0, stop=0, bucket='', id=(), labels=(), save_dir=''): for i in range(10): y = results[i, x] if i in [0, 1, 2, 5, 6, 7]: - y[y == 0] = np.nan # dont show zero loss values + y[y == 0] = np.nan # don't show zero loss values # y /= y[0] # normalize label = labels[fi] if len(labels) else Path(f).stem ax[i].plot(x, y, marker='.', label=label, linewidth=1, markersize=6) From d7e6f4dbfe00b0372b6bde15589808f75c5aa964 Mon Sep 17 00:00:00 2001 From: Jirka Borovec Date: Thu, 15 Oct 2020 15:05:58 +0200 Subject: [PATCH 27/37] fix compatibility for hyper config (#1146) * fix/hyper * Hyp giou check to train.py * restore general.py * train.py overwrite fix * restore general.py and pep8 update Co-authored-by: Glenn Jocher --- train.py | 11 ++++++++--- utils/general.py | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/train.py b/train.py index 80d9eec1e823..42774b880fc6 100644 --- a/train.py +++ b/train.py @@ -5,6 +5,7 @@ import shutil import time from pathlib import Path +from warnings import warn import math import numpy as np @@ -430,9 +431,8 @@ def train(hyp, opt, device, tb_writer=None): opt.img_size.extend([opt.img_size[-1]] * (2 - len(opt.img_size))) # extend to 2 sizes (train, test) log_dir = increment_dir(Path(opt.logdir) / 'exp', opt.name) # runs/exp1 - device = select_device(opt.device, batch_size=opt.batch_size) - # DDP mode + device = select_device(opt.device, batch_size=opt.batch_size) if opt.local_rank != -1: assert torch.cuda.device_count() > opt.local_rank torch.cuda.set_device(opt.local_rank) @@ -441,11 +441,16 @@ def train(hyp, opt, device, tb_writer=None): assert opt.batch_size % opt.world_size == 0, '--batch-size must be multiple of CUDA device count' opt.batch_size = opt.total_batch_size // opt.world_size - logger.info(opt) + # Hyperparameters with open(opt.hyp) as f: hyp = yaml.load(f, Loader=yaml.FullLoader) # load hyps + if 'box' not in hyp: + warn('Compatibility: %s missing "box" which was renamed from "giou" in %s' % + (opt.hyp, 'https://github.com/ultralytics/yolov5/pull/1120')) + hyp['box'] = hyp.pop('giou') # Train + logger.info(opt) if not opt.evolve: tb_writer = None if opt.global_rank in [-1, 0]: diff --git a/utils/general.py b/utils/general.py index 3513d65cb4c6..f8415feef999 100755 --- a/utils/general.py +++ b/utils/general.py @@ -1,18 +1,18 @@ import glob import logging -import math import os import platform import random +import re import shutil import subprocess import time -import re from contextlib import contextmanager from copy import copy from pathlib import Path import cv2 +import math import matplotlib import matplotlib.pyplot as plt import numpy as np From 34282b0350282e28a9125665810b878faff7b41c Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Thu, 15 Oct 2020 19:07:15 +0200 Subject: [PATCH 28/37] update copied attributes --- models/yolo.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/models/yolo.py b/models/yolo.py index 8774bea2863f..0d46054ed21c 100644 --- a/models/yolo.py +++ b/models/yolo.py @@ -1,10 +1,11 @@ import argparse import logging -import math import sys from copy import deepcopy from pathlib import Path +import math + sys.path.append('./') # to run '$ python *.py' files in subdirectories logger = logging.getLogger(__name__) @@ -188,7 +189,7 @@ def nms(self, mode=True): # add or remove NMS module def autoshape(self): # add autoShape module print('Adding autoShape... ') m = autoShape(self) # wrap model - copy_attr(m, self, include=('names', 'stride', 'nc', 'autoshape'), exclude=()) # copy attributes + copy_attr(m, self, include=('yaml', 'nc', 'hyp', 'names', 'stride'), exclude=()) # copy attributes return m def info(self, verbose=False): # print model information From 8668c2bdd0cd46bcae0795f1ef0183531dcb80ab Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Thu, 15 Oct 2020 19:35:31 +0200 Subject: [PATCH 29/37] optimize imports --- models/common.py | 1 - sotabench.py | 9 +++------ utils/datasets.py | 2 +- utils/torch_utils.py | 2 +- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/models/common.py b/models/common.py index 6182814102ce..022ad00ba43e 100644 --- a/models/common.py +++ b/models/common.py @@ -1,7 +1,6 @@ # This file contains modules common to various models import math - import numpy as np import torch import torch.nn as nn diff --git a/sotabench.py b/sotabench.py index 96ea6bffcbb0..9507d0754e95 100644 --- a/sotabench.py +++ b/sotabench.py @@ -1,6 +1,5 @@ import argparse import glob -import json import os import shutil from pathlib import Path @@ -8,19 +7,17 @@ import numpy as np import torch import yaml +from sotabencheval.object_detection import COCOEvaluator +from sotabencheval.utils import is_server from tqdm import tqdm from models.experimental import attempt_load from utils.datasets import create_dataloader from utils.general import ( coco80_to_coco91_class, check_dataset, check_file, check_img_size, compute_loss, non_max_suppression, scale_coords, - xyxy2xywh, clip_coords, plot_images, xywh2xyxy, box_iou, output_to_target, ap_per_class, set_logging) + xyxy2xywh, clip_coords, set_logging) from utils.torch_utils import select_device, time_synchronized - -from sotabencheval.object_detection import COCOEvaluator -from sotabencheval.utils import is_server - DATA_ROOT = './.data/vision/coco' if is_server() else '../coco' # sotabench data dir diff --git a/utils/datasets.py b/utils/datasets.py index 29ee4b051e85..9192dec4b7d9 100755 --- a/utils/datasets.py +++ b/utils/datasets.py @@ -1,5 +1,4 @@ import glob -import math import os import random import shutil @@ -8,6 +7,7 @@ from threading import Thread import cv2 +import math import numpy as np import torch from PIL import Image, ExifTags diff --git a/utils/torch_utils.py b/utils/torch_utils.py index c587617b821c..f6818238452f 100644 --- a/utils/torch_utils.py +++ b/utils/torch_utils.py @@ -1,9 +1,9 @@ import logging -import math import os import time from copy import deepcopy +import math import torch import torch.backends.cudnn as cudnn import torch.nn as nn From 91a029aee809c0a64ca562246d3c2eabc681f18f Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 25 Sep 2020 15:28:03 -0700 Subject: [PATCH 30/37] initial commit --- hubconf.py | 4 +--- models/common.py | 31 +++++++++++++++++++++++++++---- models/yolo.py | 29 +++++++++++++++++++---------- 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/hubconf.py b/hubconf.py index 168b40502f25..e439530b7660 100644 --- a/hubconf.py +++ b/hubconf.py @@ -36,9 +36,7 @@ def create(name, pretrained, channels, classes): state_dict = torch.load(ckpt, map_location=torch.device('cpu'))['model'].float().state_dict() # to FP32 state_dict = {k: v for k, v in state_dict.items() if model.state_dict()[k].shape == v.shape} # filter model.load_state_dict(state_dict, strict=False) # load - - model.add_nms() # add NMS module - model.eval() + # model = model.autoshape() # cv2/PIL/np inference: predictions = model(cv2.imread('img.jpg')) return model except Exception as e: diff --git a/models/common.py b/models/common.py index 314c31f91aac..a54050337d14 100644 --- a/models/common.py +++ b/models/common.py @@ -1,9 +1,12 @@ # This file contains modules common to various models import math +import numpy as np import torch import torch.nn as nn -from utils.general import non_max_suppression + +from utils.datasets import letterbox +from utils.general import non_max_suppression, make_divisible, scale_coords def autopad(k, p=None): # kernel, padding @@ -101,17 +104,37 @@ def forward(self, x): class NMS(nn.Module): # Non-Maximum Suppression (NMS) module - conf = 0.3 # confidence threshold - iou = 0.6 # IoU threshold + conf = 0.25 # confidence threshold + iou = 0.45 # IoU threshold classes = None # (optional list) filter by class - def __init__(self, dimension=1): + def __init__(self): super(NMS, self).__init__() def forward(self, x): return non_max_suppression(x[0], conf_thres=self.conf, iou_thres=self.iou, classes=self.classes) +class autoShape(nn.Module): + # auto-reshape image size model wrapper + img_size = 640 # inference size (pixels) + + def __init__(self, model): + super(autoShape, self).__init__() + self.model = model + + def forward(self, x, shape=640, augment=False, profile=False): # x = cv2.imread('img.jpg') + x0shape = x.shape[:2] + p = next(self.model.parameters()) + x, ratio, (dw, dh) = letterbox(x, new_shape=make_divisible(shape or max(x0shape), int(self.stride.max()))) + x1shape = x.shape[:2] + x = np.ascontiguousarray(x[:, :, ::-1].transpose(2, 0, 1)) # BGR to RGB, to 3x640x640 + x = torch.from_numpy(x).to(p.device).type_as(p).unsqueeze(0) / 255. # uint8 to fp16/32 + x = self.model(x, augment, profile) # forward + x[0][:, :4] = scale_coords(x1shape, x[0][:, :4], x0shape) + return x + + class Flatten(nn.Module): # Use after nn.AdaptiveAvgPool2d(1) to remove last 2 dimensions @staticmethod diff --git a/models/yolo.py b/models/yolo.py index a9dc539bf29f..927615c6d8e1 100644 --- a/models/yolo.py +++ b/models/yolo.py @@ -11,11 +11,11 @@ import torch import torch.nn as nn -from models.common import Conv, Bottleneck, SPP, DWConv, Focus, BottleneckCSP, Concat, NMS +from models.common import Conv, Bottleneck, SPP, DWConv, Focus, BottleneckCSP, Concat, NMS, autoShape from models.experimental import MixConv2d, CrossConv, C3 from utils.general import check_anchor_order, make_divisible, check_file, set_logging -from utils.torch_utils import ( - time_synchronized, fuse_conv_and_bn, model_info, scale_img, initialize_weights, select_device) +from utils.torch_utils import time_synchronized, fuse_conv_and_bn, model_info, scale_img, initialize_weights, \ + select_device, copy_attr class Detect(nn.Module): @@ -140,6 +140,7 @@ def forward_once(self, x, profile=False): return x def _initialize_biases(self, cf=None): # initialize biases into Detect(), cf is class frequency + # https://arxiv.org/abs/1708.02002 section 3.3 # cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1. m = self.model[-1] # Detect() module for mi, s in zip(m.m, m.stride): # from @@ -170,15 +171,27 @@ def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers self.info() return self - def add_nms(self): # fuse model Conv2d() + BatchNorm2d() layers - if type(self.model[-1]) is not NMS: # if missing NMS - print('Adding NMS module... ') + def nms(self, mode=True): # add or remove NMS module + present = type(self.model[-1]) is NMS # last layer is NMS + if mode and not present: + print('Adding NMS... ') m = NMS() # module m.f = -1 # from m.i = self.model[-1].i + 1 # index self.model.add_module(name='%s' % m.i, module=m) # add + self.eval() + elif not mode and present: + print('Removing NMS... ') + self.model = self.model[:-1] # remove return self + def autoshape(self): # add autoShape module + print('Adding autoShape... ') + self.nms() # add NMS + m = autoShape(self) # wrap model + copy_attr(m, self, include=('names', 'stride', 'nc', 'autoshape'), exclude=()) # copy attributes + return m + def info(self, verbose=False): # print model information model_info(self, verbose) @@ -263,10 +276,6 @@ def parse_model(d, ch): # model_dict, input_channels(3) # img = torch.rand(8 if torch.cuda.is_available() else 1, 3, 640, 640).to(device) # y = model(img, profile=True) - # ONNX export - # model.model[-1].export = True - # torch.onnx.export(model, img, opt.cfg.replace('.yaml', '.onnx'), verbose=True, opset_version=11) - # Tensorboard # from torch.utils.tensorboard import SummaryWriter # tb_writer = SummaryWriter() From a34b35b4b496d0e7ca15a900e1e664b47e115598 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 26 Sep 2020 18:32:36 -0700 Subject: [PATCH 31/37] batch inference update --- models/common.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/models/common.py b/models/common.py index a54050337d14..e543d129d907 100644 --- a/models/common.py +++ b/models/common.py @@ -116,22 +116,38 @@ def forward(self, x): class autoShape(nn.Module): - # auto-reshape image size model wrapper + # auto-reshape input image model wrapper img_size = 640 # inference size (pixels) def __init__(self, model): super(autoShape, self).__init__() self.model = model - def forward(self, x, shape=640, augment=False, profile=False): # x = cv2.imread('img.jpg') - x0shape = x.shape[:2] - p = next(self.model.parameters()) - x, ratio, (dw, dh) = letterbox(x, new_shape=make_divisible(shape or max(x0shape), int(self.stride.max()))) - x1shape = x.shape[:2] - x = np.ascontiguousarray(x[:, :, ::-1].transpose(2, 0, 1)) # BGR to RGB, to 3x640x640 - x = torch.from_numpy(x).to(p.device).type_as(p).unsqueeze(0) / 255. # uint8 to fp16/32 + def forward(self, x, shape=640, augment=False, profile=False): + # x is cv2/np/PIL RGB image, or list of images for batched inference, i.e. x = Image.open('image.jpg') + p = next(self.model.parameters()) # for device and type + if not isinstance(x, list): + x = [x] + batch = range(len(x)) # batch size + + shape0, shape1 = [], [] # image and inference shapes + for i in batch: + x[i] = np.array(x[i])[:, :, :3] # up to 3 channels if png + s = x[i].shape[:2] # HWC + shape0.append(s) # image shape + g = (shape / max(s)) # gain + shape1.append([y * g for y in s]) + shape1 = [make_divisible(x, int(self.stride.max())) for x in np.stack(shape1, 0).max(0)] # inference shape + + x = [letterbox(x[i], new_shape=shape1, auto=False)[0] for i in batch] # pad + x = np.stack(x, 0) if batch[-1] else x[0][None] # stack + x = np.ascontiguousarray(x.transpose((0, 3, 1, 2))) # BHWC to BCHW + x = torch.from_numpy(x).to(p.device).type_as(p) / 255. # uint8 to fp16/32 + x = self.model(x, augment, profile) # forward - x[0][:, :4] = scale_coords(x1shape, x[0][:, :4], x0shape) + + for i in batch: + x[i][:, :4] = scale_coords(shape1, x[i][:, :4], shape0[i]) # postprocess return x From a9db87b91f23d601927549e05a29331aee64f090 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Fri, 25 Sep 2020 15:28:03 -0700 Subject: [PATCH 32/37] initial commit --- models/common.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/models/common.py b/models/common.py index e543d129d907..022ad00ba43e 100644 --- a/models/common.py +++ b/models/common.py @@ -1,6 +1,6 @@ # This file contains modules common to various models -import math +import math import numpy as np import torch import torch.nn as nn @@ -116,38 +116,53 @@ def forward(self, x): class autoShape(nn.Module): - # auto-reshape input image model wrapper + # input-robust model wrapper for passing cv2/np/PIL/torch inputs. Includes preprocessing, inference and NMS img_size = 640 # inference size (pixels) + conf = 0.25 # NMS confidence threshold + iou = 0.45 # NMS IoU threshold + classes = None # (optional list) filter by class def __init__(self, model): super(autoShape, self).__init__() self.model = model - def forward(self, x, shape=640, augment=False, profile=False): - # x is cv2/np/PIL RGB image, or list of images for batched inference, i.e. x = Image.open('image.jpg') + def forward(self, x, size=640, augment=False, profile=False): + # supports inference from various sources. For height=720, width=1280, RGB images example inputs are: + # opencv: x = cv2.imread('image.jpg')[:,:,::-1] # HWC BGR to RGB x(720,1280,3) + # PIL: x = Image.open('image.jpg') # HWC x(720,1280,3) + # numpy: x = np.zeros((720,1280,3)) # HWC + # torch: x = torch.zeros(16,3,720,1280) # BCHW + # multiple: x = [Image.open('image1.jpg'), Image.open('image2.jpg'), ...] # list of images + p = next(self.model.parameters()) # for device and type + if isinstance(x, torch.Tensor): # torch + return self.model(x.to(p.device).type_as(p), augment, profile) # inference + + # Pre-process if not isinstance(x, list): x = [x] - batch = range(len(x)) # batch size - shape0, shape1 = [], [] # image and inference shapes + batch = range(len(x)) # batch size for i in batch: x[i] = np.array(x[i])[:, :, :3] # up to 3 channels if png s = x[i].shape[:2] # HWC shape0.append(s) # image shape - g = (shape / max(s)) # gain + g = (size / max(s)) # gain shape1.append([y * g for y in s]) shape1 = [make_divisible(x, int(self.stride.max())) for x in np.stack(shape1, 0).max(0)] # inference shape - x = [letterbox(x[i], new_shape=shape1, auto=False)[0] for i in batch] # pad x = np.stack(x, 0) if batch[-1] else x[0][None] # stack x = np.ascontiguousarray(x.transpose((0, 3, 1, 2))) # BHWC to BCHW x = torch.from_numpy(x).to(p.device).type_as(p) / 255. # uint8 to fp16/32 + # Inference x = self.model(x, augment, profile) # forward + x = non_max_suppression(x[0], conf_thres=self.conf, iou_thres=self.iou, classes=self.classes) # NMS + # Post-process for i in batch: - x[i][:, :4] = scale_coords(shape1, x[i][:, :4], shape0[i]) # postprocess + if x[i] is not None: + x[i][:, :4] = scale_coords(shape1, x[i][:, :4], shape0[i]) return x From 37a07a255f363510689ad2d0d209695606e82915 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 10 Oct 2020 16:02:19 +0200 Subject: [PATCH 33/37] comment update --- hubconf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hubconf.py b/hubconf.py index e439530b7660..94208e064784 100644 --- a/hubconf.py +++ b/hubconf.py @@ -10,7 +10,6 @@ import torch -from models.common import NMS from models.yolo import Model from utils.google_utils import attempt_download @@ -36,7 +35,7 @@ def create(name, pretrained, channels, classes): state_dict = torch.load(ckpt, map_location=torch.device('cpu'))['model'].float().state_dict() # to FP32 state_dict = {k: v for k, v in state_dict.items() if model.state_dict()[k].shape == v.shape} # filter model.load_state_dict(state_dict, strict=False) # load - # model = model.autoshape() # cv2/PIL/np inference: predictions = model(cv2.imread('img.jpg')) + # model = model.autoshape() # cv2/PIL/np/torch inference: predictions = model(Image.open('image.jpg')) return model except Exception as e: From 5159d102634c6db66167dedff73b4462fdc87aa6 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 10 Oct 2020 16:56:23 +0200 Subject: [PATCH 34/37] extract NMS to allow for augment --- models/yolo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/models/yolo.py b/models/yolo.py index 927615c6d8e1..8774bea2863f 100644 --- a/models/yolo.py +++ b/models/yolo.py @@ -187,7 +187,6 @@ def nms(self, mode=True): # add or remove NMS module def autoshape(self): # add autoShape module print('Adding autoShape... ') - self.nms() # add NMS m = autoShape(self) # wrap model copy_attr(m, self, include=('names', 'stride', 'nc', 'autoshape'), exclude=()) # copy attributes return m From 0be772e87e8fe720544b829770c8a381fe75a643 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Sat, 10 Oct 2020 17:29:36 +0200 Subject: [PATCH 35/37] update NMS thresholds to CoreML defaults --- detect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/detect.py b/detect.py index eee5f0208244..a98fe7394854 100644 --- a/detect.py +++ b/detect.py @@ -149,8 +149,8 @@ def detect(save_img=False): parser.add_argument('--source', type=str, default='inference/images', help='source') # file/folder, 0 for webcam parser.add_argument('--output', type=str, default='inference/output', help='output folder') # output folder parser.add_argument('--img-size', type=int, default=640, help='inference size (pixels)') - parser.add_argument('--conf-thres', type=float, default=0.4, help='object confidence threshold') - parser.add_argument('--iou-thres', type=float, default=0.5, help='IOU threshold for NMS') + parser.add_argument('--conf-thres', type=float, default=0.25, help='object confidence threshold') + parser.add_argument('--iou-thres', type=float, default=0.45, help='IOU threshold for NMS') parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument('--view-img', action='store_true', help='display results') parser.add_argument('--save-txt', action='store_true', help='save results to *.txt') From 81444368323eac5920a0875b39a41c5f0ce4e617 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Thu, 15 Oct 2020 19:07:15 +0200 Subject: [PATCH 36/37] update copied attributes --- models/yolo.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/models/yolo.py b/models/yolo.py index 8774bea2863f..0d46054ed21c 100644 --- a/models/yolo.py +++ b/models/yolo.py @@ -1,10 +1,11 @@ import argparse import logging -import math import sys from copy import deepcopy from pathlib import Path +import math + sys.path.append('./') # to run '$ python *.py' files in subdirectories logger = logging.getLogger(__name__) @@ -188,7 +189,7 @@ def nms(self, mode=True): # add or remove NMS module def autoshape(self): # add autoShape module print('Adding autoShape... ') m = autoShape(self) # wrap model - copy_attr(m, self, include=('names', 'stride', 'nc', 'autoshape'), exclude=()) # copy attributes + copy_attr(m, self, include=('yaml', 'nc', 'hyp', 'names', 'stride'), exclude=()) # copy attributes return m def info(self, verbose=False): # print model information From dc5311032a7d0769f3f16f92650d061790dc6097 Mon Sep 17 00:00:00 2001 From: Glenn Jocher Date: Thu, 15 Oct 2020 19:35:31 +0200 Subject: [PATCH 37/37] optimize imports --- sotabench.py | 9 +++------ utils/datasets.py | 2 +- utils/torch_utils.py | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/sotabench.py b/sotabench.py index 96ea6bffcbb0..9507d0754e95 100644 --- a/sotabench.py +++ b/sotabench.py @@ -1,6 +1,5 @@ import argparse import glob -import json import os import shutil from pathlib import Path @@ -8,19 +7,17 @@ import numpy as np import torch import yaml +from sotabencheval.object_detection import COCOEvaluator +from sotabencheval.utils import is_server from tqdm import tqdm from models.experimental import attempt_load from utils.datasets import create_dataloader from utils.general import ( coco80_to_coco91_class, check_dataset, check_file, check_img_size, compute_loss, non_max_suppression, scale_coords, - xyxy2xywh, clip_coords, plot_images, xywh2xyxy, box_iou, output_to_target, ap_per_class, set_logging) + xyxy2xywh, clip_coords, set_logging) from utils.torch_utils import select_device, time_synchronized - -from sotabencheval.object_detection import COCOEvaluator -from sotabencheval.utils import is_server - DATA_ROOT = './.data/vision/coco' if is_server() else '../coco' # sotabench data dir diff --git a/utils/datasets.py b/utils/datasets.py index 29ee4b051e85..9192dec4b7d9 100755 --- a/utils/datasets.py +++ b/utils/datasets.py @@ -1,5 +1,4 @@ import glob -import math import os import random import shutil @@ -8,6 +7,7 @@ from threading import Thread import cv2 +import math import numpy as np import torch from PIL import Image, ExifTags diff --git a/utils/torch_utils.py b/utils/torch_utils.py index c587617b821c..f6818238452f 100644 --- a/utils/torch_utils.py +++ b/utils/torch_utils.py @@ -1,9 +1,9 @@ import logging -import math import os import time from copy import deepcopy +import math import torch import torch.backends.cudnn as cudnn import torch.nn as nn