From 1ac56a2ecf3a28ced294f15a0f35eed95029c276 Mon Sep 17 00:00:00 2001 From: Kacper Szufnarowski Date: Mon, 21 Feb 2022 13:48:32 +0100 Subject: [PATCH 01/23] Added grayscale --- models/yolo.py | 4 +++- models/yolov5m.yaml | 1 + train.py | 2 +- utils/augmentations.py | 12 ++---------- utils/datasets.py | 13 ++++++++----- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/models/yolo.py b/models/yolo.py index 497a0e9c24e6..3360a05ce2e1 100644 --- a/models/yolo.py +++ b/models/yolo.py @@ -82,7 +82,7 @@ def _make_grid(self, nx=20, ny=20, i=0): class Model(nn.Module): - def __init__(self, cfg='yolov5s.yaml', ch=3, nc=None, anchors=None): # model, input channels, number of classes + def __init__(self, cfg='yolov5s.yaml', ch=1, nc=None, anchors=None): # model, input channels, number of classes super().__init__() if isinstance(cfg, dict): self.yaml = cfg # model dict @@ -106,6 +106,7 @@ def __init__(self, cfg='yolov5s.yaml', ch=3, nc=None, anchors=None): # model, i # Build strides, anchors m = self.model[-1] # Detect() + ch = 1 if isinstance(m, Detect): s = 256 # 2x min stride m.inplace = self.inplace @@ -247,6 +248,7 @@ def _apply(self, fn): def parse_model(d, ch): # model_dict, input_channels(3) + ch = [1] #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! LOGGER.info('\n%3s%18s%3s%10s %-40s%-30s' % ('', 'from', 'n', 'params', 'module', 'arguments')) anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple'] na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors diff --git a/models/yolov5m.yaml b/models/yolov5m.yaml index ad13ab370ff6..06c0e0fbf5ad 100644 --- a/models/yolov5m.yaml +++ b/models/yolov5m.yaml @@ -46,3 +46,4 @@ head: [[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5) ] +ch: 1 \ No newline at end of file diff --git a/train.py b/train.py index 29ae43e3bd37..867b7eab7b19 100644 --- a/train.py +++ b/train.py @@ -114,7 +114,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary with torch_distributed_zero_first(LOCAL_RANK): weights = attempt_download(weights) # download if not found locally ckpt = torch.load(weights, map_location=device) # load checkpoint - model = Model(cfg or ckpt['model'].yaml, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create + model = Model(cfg or ckpt['model'].yaml, ch=1, nc=nc, anchors=hyp.get('anchors')).to(device) # create exclude = ['anchor'] if (cfg or hyp.get('anchors')) and not resume else [] # exclude keys csd = ckpt['model'].float().state_dict() # checkpoint state_dict as FP32 csd = intersect_dicts(csd, model.state_dict(), exclude=exclude) # intersect diff --git a/utils/augmentations.py b/utils/augmentations.py index 04192d1ec5cd..5c790e035a08 100644 --- a/utils/augmentations.py +++ b/utils/augmentations.py @@ -22,15 +22,7 @@ def __init__(self): import albumentations as A check_version(A.__version__, '1.0.3') # version requirement - self.transform = A.Compose([ - A.Blur(p=0.01), - A.MedianBlur(p=0.01), - A.ToGray(p=0.01), - A.CLAHE(p=0.01), - A.RandomBrightnessContrast(p=0.0), - A.RandomGamma(p=0.0), - A.ImageCompression(quality_lower=75, p=0.0)], - bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels'])) + self.transform = A.Compose() logging.info(colorstr('albumentations: ') + ', '.join(f'{x}' for x in self.transform.transforms if x.p)) except ImportError: # package not installed, skip @@ -89,7 +81,7 @@ def replicate(im, labels): return im, labels -def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32): +def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=False, scaleFill=False, scaleup=True, stride=32): # Resize and pad image while meeting stride-multiple constraints shape = im.shape[:2] # current shape [height, width] if isinstance(new_shape, int): diff --git a/utils/datasets.py b/utils/datasets.py index a086e6bfb782..b25feb0d994c 100755 --- a/utils/datasets.py +++ b/utils/datasets.py @@ -215,7 +215,8 @@ def __next__(self): else: # Read image self.count += 1 - img0 = cv2.imread(path) # BGR + img0 = cv2.imread(path,cv2.IMREAD_GRAYSCALE) # BGR + img0 = img0.reshape(img0.shape[0],img0.shape[1],1) assert img0 is not None, 'Image Not Found ' + path print(f'image {self.count}/{self.nf} {path}: ', end='') @@ -584,7 +585,7 @@ def __getitem__(self, index): nl = len(labels) # update after albumentations # HSV color-space - augment_hsv(img, hgain=hyp['hsv_h'], sgain=hyp['hsv_s'], vgain=hyp['hsv_v']) + # augment_hsv(img, hgain=hyp['hsv_h'], sgain=hyp['hsv_s'], vgain=hyp['hsv_v']) # Flip up-down if random.random() < hyp['flipud']: @@ -606,7 +607,8 @@ def __getitem__(self, index): labels_out[:, 1:] = torch.from_numpy(labels) # Convert - img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB + #img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB + img = img.reshape(1,img.shape[0], img.shape[1]) img = np.ascontiguousarray(img) return torch.from_numpy(img), labels_out, self.img_files[index], shapes @@ -655,13 +657,15 @@ def load_image(self, i): im = np.load(npy) else: # read image path = self.img_files[i] - im = cv2.imread(path) # BGR + im = cv2.imread(path,cv2.IMREAD_GRAYSCALE) # BGR + im = im.reshape(im.shape[0],im.shape[1],1) assert im is not None, 'Image Not Found ' + path h0, w0 = im.shape[:2] # orig hw r = self.img_size / max(h0, w0) # ratio if r != 1: # if sizes are not equal im = cv2.resize(im, (int(w0 * r), int(h0 * r)), interpolation=cv2.INTER_AREA if r < 1 and not self.augment else cv2.INTER_LINEAR) + im = im.reshape(im.shape[0],im.shape[1],1) return im, (h0, w0), im.shape[:2] # im, hw_original, hw_resized else: return self.imgs[i], self.img_hw0[i], self.img_hw[i] # im, hw_original, hw_resized @@ -677,7 +681,6 @@ def load_mosaic(self, index): for i, index in enumerate(indices): # Load image img, _, (h, w) = load_image(self, index) - # place img in img4 if i == 0: # top left img4 = np.full((s * 2, s * 2, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles From 83913726184c8d5f9376342e22e5c70223b69cab Mon Sep 17 00:00:00 2001 From: Kacper Szufnarowski Date: Mon, 4 Apr 2022 11:53:56 +0200 Subject: [PATCH 02/23] Changed train and val plots limit to 16 --- utils/loggers/__init__.py | 2 +- val.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/loggers/__init__.py b/utils/loggers/__init__.py index 0750be6c8828..9bc1d88ca297 100644 --- a/utils/loggers/__init__.py +++ b/utils/loggers/__init__.py @@ -77,7 +77,7 @@ def on_train_batch_end(self, ni, model, imgs, targets, paths, plots, sync_bn): with warnings.catch_warnings(): warnings.simplefilter('ignore') # suppress jit trace warning self.tb.add_graph(torch.jit.trace(de_parallel(model), imgs[0:1], strict=False), []) - if ni < 3: + if ni < 16: f = self.save_dir / f'train_batch{ni}.jpg' # filename Thread(target=plot_images, args=(imgs, targets, paths, f), daemon=True).start() if self.wandb and ni == 10: diff --git a/val.py b/val.py index 2fc547322a0a..be6f62870c5a 100644 --- a/val.py +++ b/val.py @@ -224,7 +224,7 @@ def run(data, callbacks.run('on_val_image_end', pred, predn, path, names, img[si]) # Plot images - if plots and batch_i < 3: + if plots and batch_i < 16: f = save_dir / f'val_batch{batch_i}_labels.jpg' # labels Thread(target=plot_images, args=(img, targets, paths, f, names), daemon=True).start() f = save_dir / f'val_batch{batch_i}_pred.jpg' # predictions From 9020f08949cd9cd40280ba640f50f211d8eb28ea Mon Sep 17 00:00:00 2001 From: Adam Polerowicz Date: Fri, 29 Jul 2022 16:34:52 +0200 Subject: [PATCH 03/23] Many validations sets, albumentations json, mosaic drop --- data/hyps/hyp.scratch-low.yaml | 3 +- train.py | 108 ++++++++++++++++++++------------- utils/datasets.py | 11 ++-- 3 files changed, 75 insertions(+), 47 deletions(-) diff --git a/data/hyps/hyp.scratch-low.yaml b/data/hyps/hyp.scratch-low.yaml index b093a95ac53b..e12a1d36ccaa 100644 --- a/data/hyps/hyp.scratch-low.yaml +++ b/data/hyps/hyp.scratch-low.yaml @@ -31,4 +31,5 @@ flipud: 0.0 # image flip up-down (probability) fliplr: 0.5 # image flip left-right (probability) mosaic: 1.0 # image mosaic (probability) mixup: 0.0 # image mixup (probability) -copy_paste: 0.0 # segment copy-paste (probability) \ No newline at end of file +copy_paste: 0.0 # segment copy-paste (probability) +area_threshold: 0.5 # area threshold for random perspective diff --git a/train.py b/train.py index 867b7eab7b19..940e58759c74 100644 --- a/train.py +++ b/train.py @@ -13,6 +13,7 @@ import random import sys import time +import json from copy import deepcopy from pathlib import Path @@ -69,7 +70,6 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary w = save_dir / 'weights' # weights dir (w.parent if evolve else w).mkdir(parents=True, exist_ok=True) # make dir last, best = w / 'last.pt', w / 'best.pt' - # Hyperparameters if isinstance(hyp, str): with open(hyp, errors='ignore') as f: @@ -101,7 +101,11 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary init_seeds(1 + RANK) with torch_distributed_zero_first(LOCAL_RANK): data_dict = data_dict or check_dataset(data) # check if None - train_path, val_path = data_dict['train'], data_dict['val'] + multi_val = False + train_path, val_paths = data_dict['train'], data_dict['val'] + if type(val_paths) == list: + print('More than one validation set detected') + multi_val = True nc = 1 if single_cls else int(data_dict['nc']) # number of classes names = ['item'] if single_cls and len(data_dict['names']) != 1 else data_dict['names'] # class names assert len(names) == nc, f'{len(names)} names found for nc={nc} dataset in {data}' # check @@ -211,17 +215,32 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary hyp=hyp, augment=True, cache=opt.cache, rect=opt.rect, rank=LOCAL_RANK, workers=workers, image_weights=opt.image_weights, quad=opt.quad, prefix=colorstr('train: ')) + + augemntations = {str(number+1):str(transform) for number,transform in enumerate(dataset.albumentations.transform.transforms) if transform.p} + augemntations_to_copy = 'self.transform = A.Compose([' + for i, transform in enumerate(augemntations.values()): + augemntations_to_copy += 'A.' + transform + ',' + augemntations_to_copy = augemntations_to_copy[:-1] + augemntations_to_copy += '], bbox_params=A.BboxParams(format=\'yolo\', label_fields=[\'class_labels\']))' + augemntations['Whole composition (to copy)'] = augemntations_to_copy + + # Save augemntations + with open(save_dir / "augmentations.json", "w") as file: + json.dump(augemntations , file) + mlc = int(np.concatenate(dataset.labels, 0)[:, 0].max()) # max label class nb = len(train_loader) # number of batches assert mlc < nc, f'Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}' # Process 0 if RANK in [-1, 0]: - val_loader = create_dataloader(val_path, imgsz, batch_size // WORLD_SIZE * 2, gs, single_cls, + val_loaders = [] + for val_path in (val_paths if multi_val else [val_paths]): + val_loaders.append(create_dataloader(val_path, imgsz, batch_size // WORLD_SIZE * 2, gs, single_cls, hyp=hyp, cache=None if noval else opt.cache, rect=True, rank=-1, workers=workers, pad=0.5, - prefix=colorstr('val: '))[0] - + prefix=colorstr('val: '))[0]) + if not resume: labels = np.concatenate(dataset.labels, 0) # c = torch.tensor(labels[:, 0]) # classes @@ -349,43 +368,47 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary callbacks.run('on_train_epoch_end', epoch=epoch) ema.update_attr(model, include=['yaml', 'nc', 'hyp', 'names', 'stride', 'class_weights']) final_epoch = (epoch + 1 == epochs) or stopper.possible_stop - if not noval or final_epoch: # Calculate mAP - results, maps, _ = val.run(data_dict, - batch_size=batch_size // WORLD_SIZE * 2, - imgsz=imgsz, - model=ema.ema, - single_cls=single_cls, - dataloader=val_loader, - save_dir=save_dir, - plots=False, - callbacks=callbacks, - compute_loss=compute_loss) - - # Update best mAP - 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 - log_vals = list(mloss) + list(results) + lr - callbacks.run('on_fit_epoch_end', log_vals, epoch, best_fitness, fi) - - # Save model - if (not nosave) or (final_epoch and not evolve): # if save - ckpt = {'epoch': epoch, - 'best_fitness': best_fitness, - 'model': deepcopy(de_parallel(model)).half(), - 'ema': deepcopy(ema.ema).half(), - 'updates': ema.updates, - 'optimizer': optimizer.state_dict(), - 'wandb_id': loggers.wandb.wandb_run.id if loggers.wandb else None} - - # Save last, best and delete - torch.save(ckpt, last) - if best_fitness == fi: - torch.save(ckpt, best) - if (epoch > 0) and (opt.save_period > 0) and (epoch % opt.save_period == 0): - torch.save(ckpt, w / f'epoch{epoch}.pt') - del ckpt - callbacks.run('on_model_save', last, epoch, final_epoch, best_fitness, fi) + + for val_index, val_loader in enumerate(val_loaders): + if not noval or final_epoch: # Calculate mAP + results, maps, _ = val.run(data_dict, + batch_size=batch_size // WORLD_SIZE * 2, + imgsz=imgsz, + model=ema.ema, + single_cls=single_cls, + dataloader=val_loader, + save_dir=save_dir, + plots=False, + callbacks=callbacks, + compute_loss=compute_loss) + + # Update best mAP + 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 + log_vals = list(mloss) + list(results) + lr + callbacks.run('on_fit_epoch_end', log_vals, epoch, best_fitness, fi) + + # Save model + if (not nosave) or (final_epoch and not evolve): # if save + ckpt = {'epoch': epoch, + 'best_fitness': best_fitness, + 'model': deepcopy(de_parallel(model)).half(), + 'ema': deepcopy(ema.ema).half(), + 'updates': ema.updates, + 'optimizer': optimizer.state_dict(), + 'wandb_id': loggers.wandb.wandb_run.id if loggers.wandb else None} + + # Save last, best and delete + torch.save(ckpt, last) + val_loader_str = ''.join([i for i in val_paths[val_index] if i.isalpha()]) + new_best = best.parent.joinpath(Path(best.stem + '_'+val_loader_str[:2]+'_'+str(val_index)+best.suffix)) + if best_fitness == fi: + torch.save(ckpt, new_best) + if (epoch > 0) and (opt.save_period > 0) and (epoch % opt.save_period == 0): + torch.save(ckpt, w / f'epoch{epoch}.pt') + del ckpt + callbacks.run('on_model_save', last, epoch, final_epoch, best_fitness, fi) # Stop Single-GPU if RANK == -1 and stopper(epoch=epoch, fitness=fi): @@ -618,3 +641,4 @@ def run(**kwargs): if __name__ == "__main__": opt = parse_opt() main(opt) + diff --git a/utils/datasets.py b/utils/datasets.py index b25feb0d994c..d090bd515f0d 100755 --- a/utils/datasets.py +++ b/utils/datasets.py @@ -390,7 +390,6 @@ def __init__(self, path, img_size=640, batch_size=16, augment=False, hyp=None, r self.stride = stride self.path = path self.albumentations = Albumentations() if augment else None - try: f = [] # image files for p in path if isinstance(path, list) else [path]: @@ -573,7 +572,8 @@ def __getitem__(self, index): translate=hyp['translate'], scale=hyp['scale'], shear=hyp['shear'], - perspective=hyp['perspective']) + perspective=hyp['perspective'], + area_threshold=hyp['area_threshold']) nl = len(labels) # number of labels if nl: @@ -722,7 +722,8 @@ def load_mosaic(self, index): scale=self.hyp['scale'], shear=self.hyp['shear'], perspective=self.hyp['perspective'], - border=self.mosaic_border) # border to remove + border=self.mosaic_border, + area_threshold=self.hyp['area_threshold']) # border to remove return img4, labels4 @@ -796,7 +797,8 @@ def load_mosaic9(self, index): scale=self.hyp['scale'], shear=self.hyp['shear'], perspective=self.hyp['perspective'], - border=self.mosaic_border) # border to remove + border=self.mosaic_border, + area_threshold=self.hyp['area_threshold']) # border to remove return img9, labels9 @@ -1020,3 +1022,4 @@ def hub_ops(f, max_dim=1920): if verbose: print(json.dumps(stats, indent=2, sort_keys=False)) return stats + From cd1f194910a3834ff7414cc2861c9f489520cf7f Mon Sep 17 00:00:00 2001 From: Adam Polerowicz Date: Tue, 2 Aug 2022 16:25:41 +0200 Subject: [PATCH 04/23] mAP plots --- train.py | 93 +++++++++++++++++++++++---------------- utils/augmentations.py | 17 +++++-- utils/loggers/__init__.py | 6 ++- utils/metrics.py | 2 +- utils/plots.py | 3 +- val.py | 59 +++++++++++++++++++++++-- 6 files changed, 131 insertions(+), 49 deletions(-) diff --git a/train.py b/train.py index 940e58759c74..9bbbadaa2d5a 100644 --- a/train.py +++ b/train.py @@ -69,7 +69,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary # Directories w = save_dir / 'weights' # weights dir (w.parent if evolve else w).mkdir(parents=True, exist_ok=True) # make dir - last, best = w / 'last.pt', w / 'best.pt' + last, temp_best = w / 'last.pt', w / 'best.pt' # Hyperparameters if isinstance(hyp, str): with open(hyp, errors='ignore') as f: @@ -102,10 +102,27 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary with torch_distributed_zero_first(LOCAL_RANK): data_dict = data_dict or check_dataset(data) # check if None multi_val = False + best = [] + validation_paths = [] + best_fitnesses = [] train_path, val_paths = data_dict['train'], data_dict['val'] if type(val_paths) == list: print('More than one validation set detected') multi_val = True + for index, val_path in enumerate(val_paths if type(val_paths) == list else [val_paths]): + temp_val_path = ''.join([c for c in val_path if c.isalpha()]).lower() + if 'validyolo' in temp_val_path: + temp_val_path = val_path[:len(val_path)-11] + elif len(temp_val_path) >= 3: + temp_val_path = temp_val_path[:3] + else: + temp_val_path = str(index) + best.append(w.joinpath('best_' + str(Path(temp_val_path).stem) + '.pt')) + temp_val_path = Path(save_dir.joinpath('results_' + temp_val_path)) + temp_val_path.mkdir(exist_ok=True) + temp_val_path.joinpath('classes').mkdir(exist_ok=True) + validation_paths.append(temp_val_path) + nc = 1 if single_cls else int(data_dict['nc']) # number of classes names = ['item'] if single_cls and len(data_dict['names']) != 1 else data_dict['names'] # class names assert len(names) == nc, f'{len(names)} names found for nc={nc} dataset in {data}' # check @@ -216,17 +233,18 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary workers=workers, image_weights=opt.image_weights, quad=opt.quad, prefix=colorstr('train: ')) - augemntations = {str(number+1):str(transform) for number,transform in enumerate(dataset.albumentations.transform.transforms) if transform.p} - augemntations_to_copy = 'self.transform = A.Compose([' - for i, transform in enumerate(augemntations.values()): - augemntations_to_copy += 'A.' + transform + ',' - augemntations_to_copy = augemntations_to_copy[:-1] - augemntations_to_copy += '], bbox_params=A.BboxParams(format=\'yolo\', label_fields=[\'class_labels\']))' - augemntations['Whole composition (to copy)'] = augemntations_to_copy - - # Save augemntations - with open(save_dir / "augmentations.json", "w") as file: - json.dump(augemntations , file) + if dataset.albumentations.transform is not None: + augemntations = {str(number+1):str(transform) for number,transform in enumerate(dataset.albumentations.transform.transforms) if transform.p} + augemntations_to_copy = 'self.transform = A.Compose([' + for i, transform in enumerate(augemntations.values()): + augemntations_to_copy += 'A.' + transform + ',' + augemntations_to_copy = augemntations_to_copy[:-1] + augemntations_to_copy += '], bbox_params=A.BboxParams(format=\'yolo\', label_fields=[\'class_labels\']))' + augemntations['Whole composition (to copy)'] = augemntations_to_copy + + # Save augemntations + with open(save_dir / "augmentations.json", "w") as file: + json.dump(augemntations , file) mlc = int(np.concatenate(dataset.labels, 0)[:, 0].max()) # max label class nb = len(train_loader) # number of batches @@ -377,11 +395,11 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary model=ema.ema, single_cls=single_cls, dataloader=val_loader, - save_dir=save_dir, + save_dir=validation_paths[val_index], plots=False, callbacks=callbacks, compute_loss=compute_loss) - + # Update best mAP fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95] if fi > best_fitness: @@ -401,10 +419,8 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary # Save last, best and delete torch.save(ckpt, last) - val_loader_str = ''.join([i for i in val_paths[val_index] if i.isalpha()]) - new_best = best.parent.joinpath(Path(best.stem + '_'+val_loader_str[:2]+'_'+str(val_index)+best.suffix)) if best_fitness == fi: - torch.save(ckpt, new_best) + torch.save(ckpt, best[val_index]) if (epoch > 0) and (opt.save_period > 0) and (epoch % opt.save_period == 0): torch.save(ckpt, w / f'epoch{epoch}.pt') del ckpt @@ -428,27 +444,28 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary # end training ----------------------------------------------------------------------------------------------------- if RANK in [-1, 0]: LOGGER.info(f'\n{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.') - for f in last, best: - if f.exists(): - strip_optimizer(f) # strip optimizers - if f is best: - LOGGER.info(f'\nValidating {f}...') - results, _, _ = val.run(data_dict, - batch_size=batch_size // WORLD_SIZE * 2, - imgsz=imgsz, - model=attempt_load(f, device).half(), - iou_thres=0.65 if is_coco else 0.60, # best pycocotools results at 0.65 - single_cls=single_cls, - dataloader=val_loader, - save_dir=save_dir, - save_json=is_coco, - verbose=True, - plots=True, - callbacks=callbacks, - compute_loss=compute_loss) # val best model with plots - - callbacks.run('on_train_end', last, best, plots, epoch) - LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}") + for best_index, best_model in enumerate(best): + for f in last, best_model: + if f.exists(): + strip_optimizer(f) # strip optimizers + if f is best_model: + LOGGER.info(f'\nValidating {f}...') + results, _, _ = val.run(data_dict, + batch_size=batch_size // WORLD_SIZE * 2, + imgsz=imgsz, + model=attempt_load(f, device).half(), + iou_thres=0.65 if is_coco else 0.60, # best pycocotools results at 0.65 + single_cls=single_cls, + dataloader=val_loader, + save_dir=validation_paths[best_index], + save_json=is_coco, + verbose=True, + plots=True, + callbacks=callbacks, + compute_loss=compute_loss) # val best model with plots + + callbacks.run('on_train_end', last, best_model, plots, epoch) + LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}") torch.cuda.empty_cache() return results diff --git a/utils/augmentations.py b/utils/augmentations.py index 5c790e035a08..9147c13cc887 100644 --- a/utils/augmentations.py +++ b/utils/augmentations.py @@ -21,8 +21,17 @@ def __init__(self): try: import albumentations as A check_version(A.__version__, '1.0.3') # version requirement - - self.transform = A.Compose() + self.transform = A.Compose([ + A.RandomBrightnessContrast(always_apply=False, p=0.7, brightness_limit=(-0.1, 0.1), contrast_limit=(-0.1, 0.1), brightness_by_max=True), + A.JpegCompression(always_apply=False, p=0.7, quality_lower=40, quality_upper=100), + A.GaussNoise(always_apply=False, p=0.6, mean=-21.0, var_limit=(40.0, 150)), + A.OneOf([ + A.Blur(always_apply=False, p=0.5, blur_limit=(3, 5)), + A.MotionBlur(always_apply=False, p=0.5, blur_limit=(3, 5)), + ], p=0.7), + A.Cutout(always_apply=False, p=0.7, num_holes=32, max_h_size=8, max_w_size=8) + ], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels'])) +# self.transform = A.Compose() logging.info(colorstr('albumentations: ') + ', '.join(f'{x}' for x in self.transform.transforms if x.p)) except ImportError: # package not installed, skip @@ -115,7 +124,7 @@ def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=False, scale def random_perspective(im, targets=(), segments=(), degrees=10, translate=.1, scale=.1, shear=10, perspective=0.0, - border=(0, 0)): + border=(0, 0), area_threshold=0.01): # torchvision.transforms.RandomAffine(degrees=(-10, 10), translate=(.1, .1), scale=(.9, 1.1), shear=(-10, 10)) # targets = [cls, xyxy] @@ -196,7 +205,7 @@ def random_perspective(im, targets=(), segments=(), degrees=10, translate=.1, sc new[:, [1, 3]] = new[:, [1, 3]].clip(0, height) # filter candidates - i = box_candidates(box1=targets[:, 1:5].T * s, box2=new.T, area_thr=0.01 if use_segments else 0.10) + i = box_candidates(box1=targets[:, 1:5].T * s, box2=new.T, area_thr=area_threshold if use_segments else area_threshold) targets = targets[i] targets[:, 1:5] = new[i] diff --git a/utils/loggers/__init__.py b/utils/loggers/__init__.py index 9bc1d88ca297..8440a84efa7d 100644 --- a/utils/loggers/__init__.py +++ b/utils/loggers/__init__.py @@ -78,7 +78,9 @@ def on_train_batch_end(self, ni, model, imgs, targets, paths, plots, sync_bn): warnings.simplefilter('ignore') # suppress jit trace warning self.tb.add_graph(torch.jit.trace(de_parallel(model), imgs[0:1], strict=False), []) if ni < 16: - f = self.save_dir / f'train_batch{ni}.jpg' # filename + new_save_dir = self.save_dir.joinpath('train_batches') + new_save_dir.mkdir(exist_ok = True) + f = new_save_dir / f'train_batch{ni}.jpg' # filename Thread(target=plot_images, args=(imgs, targets, paths, f), daemon=True).start() if self.wandb and ni == 10: files = sorted(self.save_dir.glob('train*.jpg')) @@ -127,7 +129,7 @@ def on_model_save(self, last, epoch, final_epoch, best_fitness, fi): def on_train_end(self, last, best, plots, epoch): # Callback runs on training end if plots: - plot_results(file=self.save_dir / 'results.csv') # save results.png + plot_results(file=self.save_dir / 'results.csv', best=best) # save results.png files = ['results.png', 'confusion_matrix.png', *[f'{x}_curve.png' for x in ('F1', 'PR', 'P', 'R')]] files = [(self.save_dir / f) for f in files if (self.save_dir / f).exists()] # filter diff --git a/utils/metrics.py b/utils/metrics.py index 4f1b5e2d2c2d..b3d0922e09f2 100644 --- a/utils/metrics.py +++ b/utils/metrics.py @@ -58,7 +58,7 @@ def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names # Recall recall = tpc / (n_l + 1e-16) # recall curve r[ci] = np.interp(-px, -conf[i], recall[:, 0], left=0) # negative x, xp because xp decreases - + # Precision precision = tpc / (tpc + fpc) # precision curve p[ci] = np.interp(-px, -conf[i], precision[:, 0], left=1) # p at pr_score diff --git a/utils/plots.py b/utils/plots.py index 00b8f88811e2..dbaeafaa839a 100644 --- a/utils/plots.py +++ b/utils/plots.py @@ -394,7 +394,7 @@ def plot_evolve(evolve_csv='path/to/evolve.csv'): # from utils.plots import *; print(f'Saved {f}') -def plot_results(file='path/to/results.csv', dir=''): +def plot_results(file='path/to/results.csv', best='', dir=''): # Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv') save_dir = Path(file).parent if file else Path(dir) fig, ax = plt.subplots(2, 5, figsize=(12, 6), tight_layout=True) @@ -404,6 +404,7 @@ def plot_results(file='path/to/results.csv', dir=''): for fi, f in enumerate(files): try: data = pd.read_csv(f) + max_epoch = max(np.array(data)[:,0]) s = [x.strip() for x in data.columns] x = data.values[:, 0] for i, j in enumerate([1, 2, 3, 4, 5, 8, 9, 10, 6, 7]): diff --git a/val.py b/val.py index be6f62870c5a..f98fa8922d74 100644 --- a/val.py +++ b/val.py @@ -13,9 +13,11 @@ from pathlib import Path from threading import Thread +import pandas as pd import numpy as np import torch from tqdm import tqdm +import matplotlib.pyplot as plt FILE = Path(__file__).resolve() ROOT = FILE.parents[0] # YOLOv5 root directory @@ -225,9 +227,13 @@ def run(data, # Plot images if plots and batch_i < 16: - f = save_dir / f'val_batch{batch_i}_labels.jpg' # labels + valid_labels_path = save_dir.joinpath('labels') + valid_predictions_path = save_dir.joinpath('pred') + valid_labels_path.mkdir(exist_ok=True) + valid_predictions_path.mkdir(exist_ok=True) + f = valid_labels_path / f'val_batch{batch_i}_labels.jpg' # labels Thread(target=plot_images, args=(img, targets, paths, f, names), daemon=True).start() - f = save_dir / f'val_batch{batch_i}_pred.jpg' # predictions + f = valid_predictions_path / f'val_batch{batch_i}_pred.jpg' # predictions Thread(target=plot_images, args=(img, output_to_target(out), paths, f, names), daemon=True).start() # Compute statistics @@ -245,10 +251,57 @@ def run(data, print(pf % ('all', seen, nt.sum(), mp, mr, map50, map)) # Print results per class + if training and (nc < 50 and nc > 1 and len(stats)): + for i, c in enumerate(ap_class): + save_path = save_dir.joinpath('classes').joinpath(names[c]+'.csv') + df = pd.DataFrame({'mAP.5': [ap50[i]], + 'mAP': [ap[i]]}) + if not save_path.exists(): + df.to_csv(str(save_path), mode='a', index=False, header=True) + else: + df.to_csv(str(save_path), mode='a', index=False, header=False) + if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats): + last_results = [] for i, c in enumerate(ap_class): print(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i])) - + load_path = save_dir.joinpath('classes').joinpath(names[c]+'.csv') + df = pd.read_csv(str(load_path)) + last_results.append((names[c], np.array(df.tail(1)))) + df.plot(y=["mAP.5", "mAP"]) + plt.savefig(load_path.parent.joinpath(str(names[c]))) + plt.figure().clear() + plt.close() + plt.cla() + plt.clf() + + class_names = [item[0] for item in last_results] + mAP5s = np.multiply([item[1][0][0] for item in last_results], 100) + mAPs = np.multiply([item[1][0][1] for item in last_results], 100) + + x = np.arange(len(names)) # the label locations + width = 0.35 # the width of the bars + + fig, ax = plt.subplots(figsize=(20, 10)) + fig.set_dpi(150) + rects1 = ax.bar(x - width/2, mAP5s, width, label='mAP.5') + rects2 = ax.bar(x + width/2, mAPs, width, label='mAP') + + # Add some text for labels, title and custom x-axis tick labels, etc. + ax.set_ylabel('Percentage') + ax.set_title('mAP by class') + plt.xticks([r + (width * 0.1) for r in range(len(class_names))], class_names) + ax.legend() + + ax.bar_label(rects1, padding=3) + ax.bar_label(rects2, padding=3) + plt.savefig(save_dir.joinpath('mAP_summary.png')) + plt.figure().clear() + plt.close() + plt.cla() + plt.clf() + + # Print speeds t = tuple(x / seen * 1E3 for x in dt) # speeds per image if not training: From 90bca0714de5b5bb5c35091169f78c6693656918 Mon Sep 17 00:00:00 2001 From: Adam Polerowicz Date: Wed, 3 Aug 2022 14:12:28 +0200 Subject: [PATCH 05/23] separate results polts --- train.py | 2 +- utils/plots.py | 42 ++++++++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/train.py b/train.py index 9bbbadaa2d5a..69760bbbcafb 100644 --- a/train.py +++ b/train.py @@ -465,7 +465,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary compute_loss=compute_loss) # val best model with plots callbacks.run('on_train_end', last, best_model, plots, epoch) - LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}") + LOGGER.info(f"Results saved to {colorstr('bold', validation_paths[best_index])}") torch.cuda.empty_cache() return results diff --git a/utils/plots.py b/utils/plots.py index dbaeafaa839a..775a5de6bc32 100644 --- a/utils/plots.py +++ b/utils/plots.py @@ -219,7 +219,7 @@ def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=''): plt.plot(y, '.-', label='LR') plt.xlabel('epoch') plt.ylabel('LR') - plt.grid() +# plt.grid() plt.xlim(0, epochs) plt.ylim(0) plt.savefig(Path(save_dir) / 'LR.png', dpi=200) @@ -281,7 +281,7 @@ def plot_val_study(file='', dir='', x=None): # from utils.plots import *; plot_ ax2.plot(1E3 / np.array([209, 140, 97, 58, 35, 18]), [34.6, 40.5, 43.0, 47.5, 49.7, 51.5], 'k.-', linewidth=2, markersize=8, alpha=.25, label='EfficientDet') - ax2.grid(alpha=0.2) +# ax2.grid(alpha=0.2) ax2.set_yticks(np.arange(20, 60, 5)) ax2.set_xlim(0, 57) ax2.set_ylim(25, 55) @@ -397,29 +397,35 @@ def plot_evolve(evolve_csv='path/to/evolve.csv'): # from utils.plots import *; def plot_results(file='path/to/results.csv', best='', dir=''): # Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv') save_dir = Path(file).parent if file else Path(dir) - fig, ax = plt.subplots(2, 5, figsize=(12, 6), tight_layout=True) - ax = ax.ravel() files = list(save_dir.glob('results*.csv')) + names = [name.stem[8:] for name in list(save_dir.glob('results*[!.csv!.png]'))] assert len(files), f'No results.csv files found in {save_dir.resolve()}, nothing to plot.' for fi, f in enumerate(files): try: data = pd.read_csv(f) - max_epoch = max(np.array(data)[:,0]) - s = [x.strip() for x in data.columns] - x = data.values[:, 0] - for i, j in enumerate([1, 2, 3, 4, 5, 8, 9, 10, 6, 7]): - y = data.values[:, j] - # y[y == 0] = np.nan # don't show zero values - ax[i].plot(x, y, marker='.', label=f.stem, linewidth=2, markersize=8) - ax[i].set_title(s[j], fontsize=12) - # if j in [8, 9, 10]: # share train and val loss y axes - # ax[i].get_shared_y_axes().join(ax[i], ax[i - 5]) + first_column = np.array(data)[:,0] + max_epochs = max(first_column) + 1 + number_of_sets = math.floor(len(first_column) / max_epochs) + print('Number of sets: ',number_of_sets) + + for sets in range(number_of_sets): + fig, ax = plt.subplots(2, 5, figsize=(12, 6), tight_layout=True) + ax = ax.ravel() + s = [x.strip() for x in data.columns] + x = data.values[:, 0] + for i, j in enumerate([1, 2, 3, 4, 5, 8, 9, 10, 6, 7]): + y = [] + for index, y_value in enumerate(data.values[:, j]): + if index % number_of_sets == sets: + y.append(y_value) + # y[y == 0] = np.nan # don't show zero values + ax[i].plot([x for x in range(len(y))], y, marker='.', label=f.stem, linewidth=2, markersize=8) + ax[i].set_title(s[j], fontsize=12) + ax[1].legend() + fig.savefig(save_dir / 'results_{}.png'.format(names[sets]), dpi=200) + plt.close() except Exception as e: print(f'Warning: Plotting error for {f}: {e}') - ax[1].legend() - fig.savefig(save_dir / 'results.png', dpi=200) - plt.close() - def feature_visualization(x, module_type, stage, n=32, save_dir=Path('runs/detect/exp')): """ From 77565bedfaaab7a3d481766fa2d6e6d649116a8a Mon Sep 17 00:00:00 2001 From: Adam Polerowicz Date: Thu, 4 Aug 2022 12:14:49 +0200 Subject: [PATCH 06/23] All class plots & epoch to csv fix --- train.py | 2 +- utils/loggers/__init__.py | 1 + utils/plots.py | 1 + val.py | 44 ++++++++++++++++++++------------------- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/train.py b/train.py index 69760bbbcafb..ed26d5369197 100644 --- a/train.py +++ b/train.py @@ -456,7 +456,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary model=attempt_load(f, device).half(), iou_thres=0.65 if is_coco else 0.60, # best pycocotools results at 0.65 single_cls=single_cls, - dataloader=val_loader, + dataloader=val_loaders[best_index], save_dir=validation_paths[best_index], save_json=is_coco, verbose=True, diff --git a/utils/loggers/__init__.py b/utils/loggers/__init__.py index 8440a84efa7d..7ad4d873fadf 100644 --- a/utils/loggers/__init__.py +++ b/utils/loggers/__init__.py @@ -109,6 +109,7 @@ def on_fit_epoch_end(self, vals, epoch, best_fitness, fi): file = self.save_dir / 'results.csv' n = len(x) + 1 # number of cols s = '' if file.exists() else (('%20s,' * n % tuple(['epoch'] + self.keys)).rstrip(',') + '\n') # add header + print(('%20.5g,' * n % tuple([epoch] + vals)).rstrip(',') + '\n') with open(file, 'a') as f: f.write(s + ('%20.5g,' * n % tuple([epoch] + vals)).rstrip(',') + '\n') diff --git a/utils/plots.py b/utils/plots.py index 775a5de6bc32..0babe8353e72 100644 --- a/utils/plots.py +++ b/utils/plots.py @@ -335,6 +335,7 @@ def plot_labels(labels, names=(), save_dir=Path('')): plt.savefig(save_dir / 'labels.jpg', dpi=200) matplotlib.use('Agg') plt.close() + plt.style.use('default') def profile_idetection(start=0, stop=0, labels=(), save_dir=''): diff --git a/val.py b/val.py index f98fa8922d74..9c495db61996 100644 --- a/val.py +++ b/val.py @@ -18,6 +18,7 @@ import torch from tqdm import tqdm import matplotlib.pyplot as plt +from matplotlib.pyplot import figure FILE = Path(__file__).resolve() ROOT = FILE.parents[0] # YOLOv5 root directory @@ -109,6 +110,8 @@ def run(data, callbacks=Callbacks(), compute_loss=None, ): + plt.style.use('default') + # Initialize/load model and set device training = model is not None if training: # called by train.py @@ -251,35 +254,32 @@ def run(data, print(pf % ('all', seen, nt.sum(), mp, mr, map50, map)) # Print results per class - if training and (nc < 50 and nc > 1 and len(stats)): + if training and not verbose and (nc < 50 and nc > 1 and len(stats)): for i, c in enumerate(ap_class): save_path = save_dir.joinpath('classes').joinpath(names[c]+'.csv') - df = pd.DataFrame({'mAP.5': [ap50[i]], - 'mAP': [ap[i]]}) - if not save_path.exists(): - df.to_csv(str(save_path), mode='a', index=False, header=True) - else: - df.to_csv(str(save_path), mode='a', index=False, header=False) + df = pd.DataFrame({'mAP.5': [ap50[i]], 'mAP': [ap[i]]}) + df.to_csv(str(save_path), mode='a', index=False, header=not save_path.exists()) - if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats): + if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats): + figure(figsize=(16, 12), dpi=150) last_results = [] for i, c in enumerate(ap_class): print(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i])) load_path = save_dir.joinpath('classes').joinpath(names[c]+'.csv') df = pd.read_csv(str(load_path)) + x = np.arange(df.shape[0]) last_results.append((names[c], np.array(df.tail(1)))) - df.plot(y=["mAP.5", "mAP"]) - plt.savefig(load_path.parent.joinpath(str(names[c]))) - plt.figure().clear() - plt.close() - plt.cla() - plt.clf() + plt.plot(x, df['mAP.5'], label='mAP.5{}'.format(names[c])) + plt.plot(x, df['mAP'], label='mAP{}'.format(names[c])) + plt.legend(loc="upper left") + plt.savefig(load_path.parent.joinpath('AllClasses.png')) + reset_plot() class_names = [item[0] for item in last_results] mAP5s = np.multiply([item[1][0][0] for item in last_results], 100) mAPs = np.multiply([item[1][0][1] for item in last_results], 100) - x = np.arange(len(names)) # the label locations + x = np.arange(len(class_names)) # the label locations width = 0.35 # the width of the bars fig, ax = plt.subplots(figsize=(20, 10)) @@ -296,11 +296,7 @@ def run(data, ax.bar_label(rects1, padding=3) ax.bar_label(rects2, padding=3) plt.savefig(save_dir.joinpath('mAP_summary.png')) - plt.figure().clear() - plt.close() - plt.cla() - plt.clf() - + reset_plot() # Print speeds t = tuple(x / seen * 1E3 for x in dt) # speeds per image @@ -349,7 +345,13 @@ def run(data, maps[c] = ap[i] return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t - +def reset_plot(): + plt.figure().clear() + plt.close() + plt.cla() + plt.clf() + figure(figsize=(6.4, 4.8), dpi=100) + def parse_opt(): parser = argparse.ArgumentParser() parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path') From 76b3cde109f84b7daf5376773f18b9c9d9de72e8 Mon Sep 17 00:00:00 2001 From: Adam Polerowicz Date: Fri, 5 Aug 2022 10:08:16 +0200 Subject: [PATCH 07/23] Summary plot fix --- val.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/val.py b/val.py index 9c495db61996..2eed2db1bcc4 100644 --- a/val.py +++ b/val.py @@ -290,7 +290,7 @@ def run(data, # Add some text for labels, title and custom x-axis tick labels, etc. ax.set_ylabel('Percentage') ax.set_title('mAP by class') - plt.xticks([r + (width * 0.1) for r in range(len(class_names))], class_names) + plt.xticks([r + (width * 0.1) for r in range(len(class_names))], class_names, rotation=90) ax.legend() ax.bar_label(rects1, padding=3) From e96a5377f8ea280c9942c6b38083e42ca0f7e141 Mon Sep 17 00:00:00 2001 From: Adam Polerowicz Date: Fri, 5 Aug 2022 10:13:00 +0200 Subject: [PATCH 08/23] Default agumentations --- utils/augmentations.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/utils/augmentations.py b/utils/augmentations.py index 9147c13cc887..9b720ba60f1b 100644 --- a/utils/augmentations.py +++ b/utils/augmentations.py @@ -21,17 +21,17 @@ def __init__(self): try: import albumentations as A check_version(A.__version__, '1.0.3') # version requirement - self.transform = A.Compose([ - A.RandomBrightnessContrast(always_apply=False, p=0.7, brightness_limit=(-0.1, 0.1), contrast_limit=(-0.1, 0.1), brightness_by_max=True), - A.JpegCompression(always_apply=False, p=0.7, quality_lower=40, quality_upper=100), - A.GaussNoise(always_apply=False, p=0.6, mean=-21.0, var_limit=(40.0, 150)), - A.OneOf([ - A.Blur(always_apply=False, p=0.5, blur_limit=(3, 5)), - A.MotionBlur(always_apply=False, p=0.5, blur_limit=(3, 5)), - ], p=0.7), - A.Cutout(always_apply=False, p=0.7, num_holes=32, max_h_size=8, max_w_size=8) - ], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels'])) -# self.transform = A.Compose() +# self.transform = A.Compose([ +# A.RandomBrightnessContrast(always_apply=False, p=0.7, brightness_limit=(-0.1, 0.1), contrast_limit=(-0.1, 0.1), brightness_by_max=True), +# A.JpegCompression(always_apply=False, p=0.7, quality_lower=40, quality_upper=100), +# A.GaussNoise(always_apply=False, p=0.6, mean=-21.0, var_limit=(40.0, 150)), +# A.OneOf([ +# A.Blur(always_apply=False, p=0.5, blur_limit=(3, 5)), +# A.MotionBlur(always_apply=False, p=0.5, blur_limit=(3, 5)), +# ], p=0.7), +# A.Cutout(always_apply=False, p=0.7, num_holes=32, max_h_size=8, max_w_size=8) +# ], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels'])) + self.transform = A.Compose() logging.info(colorstr('albumentations: ') + ', '.join(f'{x}' for x in self.transform.transforms if x.p)) except ImportError: # package not installed, skip From 96433ed4aec57e33e32dd91369498b0a4168730f Mon Sep 17 00:00:00 2001 From: Adam Polerowicz Date: Fri, 5 Aug 2022 10:44:28 +0200 Subject: [PATCH 09/23] code cleanup --- Train.ipynb | 35 +++++++++++++++++++++++++++++++++++ train.py | 3 +-- utils/loggers/__init__.py | 3 +-- utils/plots.py | 2 +- val.py | 5 ++--- 5 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 Train.ipynb diff --git a/Train.ipynb b/Train.ipynb new file mode 100644 index 000000000000..5ddd7093b204 --- /dev/null +++ b/Train.ipynb @@ -0,0 +1,35 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "4fbd6fa3-a31c-4ba1-a917-6669fafcd904", + "metadata": {}, + "outputs": [], + "source": [ + "!python train.py --img 640 --batch 8 --epochs 5 --data ./data/sausages.yaml --hyp data/hyps/hyp.scratch-low.yaml --save-period 1 --weights yolov5s.pt --cache --device 0 --name SausagesTest" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/train.py b/train.py index ed26d5369197..405b7878e1a5 100644 --- a/train.py +++ b/train.py @@ -104,10 +104,9 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary multi_val = False best = [] validation_paths = [] - best_fitnesses = [] train_path, val_paths = data_dict['train'], data_dict['val'] if type(val_paths) == list: - print('More than one validation set detected') + print('Detected {} validation sets'.format(len(val_paths))) multi_val = True for index, val_path in enumerate(val_paths if type(val_paths) == list else [val_paths]): temp_val_path = ''.join([c for c in val_path if c.isalpha()]).lower() diff --git a/utils/loggers/__init__.py b/utils/loggers/__init__.py index 7ad4d873fadf..35f3a6b9f2e2 100644 --- a/utils/loggers/__init__.py +++ b/utils/loggers/__init__.py @@ -109,7 +109,6 @@ def on_fit_epoch_end(self, vals, epoch, best_fitness, fi): file = self.save_dir / 'results.csv' n = len(x) + 1 # number of cols s = '' if file.exists() else (('%20s,' * n % tuple(['epoch'] + self.keys)).rstrip(',') + '\n') # add header - print(('%20.5g,' * n % tuple([epoch] + vals)).rstrip(',') + '\n') with open(file, 'a') as f: f.write(s + ('%20.5g,' * n % tuple([epoch] + vals)).rstrip(',') + '\n') @@ -130,7 +129,7 @@ def on_model_save(self, last, epoch, final_epoch, best_fitness, fi): def on_train_end(self, last, best, plots, epoch): # Callback runs on training end if plots: - plot_results(file=self.save_dir / 'results.csv', best=best) # save results.png + plot_results(file=self.save_dir / 'results.csv') # save results.png files = ['results.png', 'confusion_matrix.png', *[f'{x}_curve.png' for x in ('F1', 'PR', 'P', 'R')]] files = [(self.save_dir / f) for f in files if (self.save_dir / f).exists()] # filter diff --git a/utils/plots.py b/utils/plots.py index 0babe8353e72..e328717d7a2d 100644 --- a/utils/plots.py +++ b/utils/plots.py @@ -395,7 +395,7 @@ def plot_evolve(evolve_csv='path/to/evolve.csv'): # from utils.plots import *; print(f'Saved {f}') -def plot_results(file='path/to/results.csv', best='', dir=''): +def plot_results(file='path/to/results.csv', dir=''): # Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv') save_dir = Path(file).parent if file else Path(dir) files = list(save_dir.glob('results*.csv')) diff --git a/val.py b/val.py index 2eed2db1bcc4..832385fc928d 100644 --- a/val.py +++ b/val.py @@ -18,7 +18,6 @@ import torch from tqdm import tqdm import matplotlib.pyplot as plt -from matplotlib.pyplot import figure FILE = Path(__file__).resolve() ROOT = FILE.parents[0] # YOLOv5 root directory @@ -261,7 +260,7 @@ def run(data, df.to_csv(str(save_path), mode='a', index=False, header=not save_path.exists()) if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats): - figure(figsize=(16, 12), dpi=150) + plt.figure(figsize=(16, 12), dpi=150) last_results = [] for i, c in enumerate(ap_class): print(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i])) @@ -350,7 +349,7 @@ def reset_plot(): plt.close() plt.cla() plt.clf() - figure(figsize=(6.4, 4.8), dpi=100) + plt.figure(figsize=(6.4, 4.8), dpi=100) def parse_opt(): parser = argparse.ArgumentParser() From 2bac69bece03eebefe8031c8ede2fe547fe2307f Mon Sep 17 00:00:00 2001 From: Adam Polerowicz Date: Fri, 5 Aug 2022 10:51:24 +0200 Subject: [PATCH 10/23] training jupyter --- Train.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Train.ipynb b/Train.ipynb index 5ddd7093b204..78ca6a8a8306 100644 --- a/Train.ipynb +++ b/Train.ipynb @@ -7,7 +7,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python train.py --img 640 --batch 8 --epochs 5 --data ./data/sausages.yaml --hyp data/hyps/hyp.scratch-low.yaml --save-period 1 --weights yolov5s.pt --cache --device 0 --name SausagesTest" + "!python -m torch.distributed.launch --nproc_per_node 2 train.py --img --batch --epochs --data --hyp data/hyps/hyp.scratch-low.yaml --weights yolov5m --cache --device 0,1 --name " ] } ], From ee27bb45d269d0c36e486d688711dc1f16036c7e Mon Sep 17 00:00:00 2001 From: Adam Polerowicz Date: Fri, 5 Aug 2022 11:12:13 +0200 Subject: [PATCH 11/23] saving best for each valid set --- train.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/train.py b/train.py index 405b7878e1a5..3e6e847dd27a 100644 --- a/train.py +++ b/train.py @@ -103,6 +103,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary data_dict = data_dict or check_dataset(data) # check if None multi_val = False best = [] + best_fitnesses = [] validation_paths = [] train_path, val_paths = data_dict['train'], data_dict['val'] if type(val_paths) == list: @@ -121,6 +122,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary temp_val_path.mkdir(exist_ok=True) temp_val_path.joinpath('classes').mkdir(exist_ok=True) validation_paths.append(temp_val_path) + best_fitnesses.append(0) nc = 1 if single_cls else int(data_dict['nc']) # number of classes names = ['item'] if single_cls and len(data_dict['names']) != 1 else data_dict['names'] # class names @@ -401,10 +403,12 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary # Update best mAP fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95] + if fi > best_fitnesses[val_index]: + best_fitnesses[val_index] = fi if fi > best_fitness: best_fitness = fi log_vals = list(mloss) + list(results) + lr - callbacks.run('on_fit_epoch_end', log_vals, epoch, best_fitness, fi) + callbacks.run('on_fit_epoch_end', log_vals, epoch, best_fitnesses[val_index], fi) # Save model if (not nosave) or (final_epoch and not evolve): # if save @@ -418,7 +422,7 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary # Save last, best and delete torch.save(ckpt, last) - if best_fitness == fi: + if best_fitnesses[val_index] == fi: torch.save(ckpt, best[val_index]) if (epoch > 0) and (opt.save_period > 0) and (epoch % opt.save_period == 0): torch.save(ckpt, w / f'epoch{epoch}.pt') From 3b2767a43cb138478f61c29e2c0a389dd0350056 Mon Sep 17 00:00:00 2001 From: Kacper Szufnarowski Date: Tue, 25 Oct 2022 13:20:12 +0200 Subject: [PATCH 12/23] Mistakes saved during validation --- utils/plots.py | 4 ++-- val.py | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/utils/plots.py b/utils/plots.py index e328717d7a2d..f1f4827ec6e0 100644 --- a/utils/plots.py +++ b/utils/plots.py @@ -148,7 +148,7 @@ def output_to_target(output): return np.array(targets) -def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=1920, max_subplots=16): +def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=7680, max_subplots=64): # Plot image grid with labels if isinstance(images, torch.Tensor): images = images.cpu().float().numpy() @@ -177,7 +177,7 @@ def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max mosaic = cv2.resize(mosaic, tuple(int(x * ns) for x in (w, h))) # Annotate - fs = int((h + w) * ns * 0.01) # font size + fs = int((h + w) * ns * 0.005) # font size annotator = Annotator(mosaic, line_width=round(fs / 10), font_size=fs, pil=True) for i in range(i + 1): x, y = int(w * (i // ns)), int(h * (i % ns)) # block origin diff --git a/val.py b/val.py index 832385fc928d..08550001a829 100644 --- a/val.py +++ b/val.py @@ -34,6 +34,7 @@ from utils.plots import output_to_target, plot_images, plot_val_study from utils.torch_utils import select_device, time_sync from utils.callbacks import Callbacks +from yolo_analyze_service import YoloAnalyzeService def save_one_txt(predn, save_conf, shape, file): @@ -150,7 +151,7 @@ def run(data, # Dataloader if not training: if device.type != 'cpu': - model(torch.zeros(1, 3, imgsz, imgsz).to(device).type_as(next(model.parameters()))) # run once + model(torch.zeros(1, 1, imgsz, imgsz).to(device).type_as(next(model.parameters()))) # run once pad = 0.0 if task == 'speed' else 0.5 task = task if task in ('train', 'val', 'test') else 'val' # path to train/val/test images dataloader = create_dataloader(data[task], imgsz, batch_size, gs, single_cls, pad=pad, rect=True, @@ -237,6 +238,13 @@ def run(data, Thread(target=plot_images, args=(img, targets, paths, f, names), daemon=True).start() f = valid_predictions_path / f'val_batch{batch_i}_pred.jpg' # predictions Thread(target=plot_images, args=(img, output_to_target(out), paths, f, names), daemon=True).start() + + yolo_analyzer = YoloAnalyzeService(0.5, 0.5) + mistakes = yolo_analyzer.analyze_batch(targets.cpu().numpy(), output_to_target(out)) + mistakes_path = save_dir.joinpath("mistakes") + mistakes_path.mkdir(exist_ok=True) + f = mistakes_path / f'val_batch{batch_i}_mistakes.jpg' + Thread(target=plot_images, args=(img, mistakes, paths, f, names), daemon=True).start() # Compute statistics stats = [np.concatenate(x, 0) for x in zip(*stats)] # to numpy From c979d033a80a1f20ebed70e7cbe06664c7ba0729 Mon Sep 17 00:00:00 2001 From: Kacper Szufnarowski Date: Wed, 16 Nov 2022 10:16:41 +0100 Subject: [PATCH 13/23] Added yolo_analyze_service, changed hyps to have new params --- data/hyps/hyp.finetune.yaml | 7 ++ data/hyps/hyp.finetune_objects365.yaml | 7 ++ data/hyps/hyp.scratch-high.yaml | 9 ++- data/hyps/hyp.scratch-low.yaml | 8 ++- data/hyps/hyp.scratch.yaml | 7 ++ train.py | 7 +- val.py | 16 +++-- yolo_analyze_service.py | 90 ++++++++++++++++++++++++++ yolo_detections_matching_service.py | 52 +++++++++++++++ 9 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 yolo_analyze_service.py create mode 100644 yolo_detections_matching_service.py diff --git a/data/hyps/hyp.finetune.yaml b/data/hyps/hyp.finetune.yaml index b89d66ff8dee..71e8404ec9d7 100644 --- a/data/hyps/hyp.finetune.yaml +++ b/data/hyps/hyp.finetune.yaml @@ -37,3 +37,10 @@ fliplr: 0.5 mosaic: 1.0 mixup: 0.243 copy_paste: 0.0 + +# Custom Params +area_threshold: 0.3 # area threshold for random perspective +max_mistakes_size: 7680 # image size for mistakes mosaic +max_mistakes_subplots: 64 # subimages in mistakes mosaic +min_mistakes_iou: 0.5 # mistakes minimum iou for match +min_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file diff --git a/data/hyps/hyp.finetune_objects365.yaml b/data/hyps/hyp.finetune_objects365.yaml index 073720a65be5..81ddddad4a00 100644 --- a/data/hyps/hyp.finetune_objects365.yaml +++ b/data/hyps/hyp.finetune_objects365.yaml @@ -29,3 +29,10 @@ fliplr: 0.5 mosaic: 1.0 mixup: 0.0 copy_paste: 0.0 + +# Custom Params +area_threshold: 0.3 # area threshold for random perspective +max_mistakes_size: 7680 # image size for mistakes mosaic +max_mistakes_subplots: 64 # subimages in mistakes mosaic +min_mistakes_iou: 0.5 # mistakes minimum iou for match +min_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file diff --git a/data/hyps/hyp.scratch-high.yaml b/data/hyps/hyp.scratch-high.yaml index 519c82687e09..2aefe4f5a302 100644 --- a/data/hyps/hyp.scratch-high.yaml +++ b/data/hyps/hyp.scratch-high.yaml @@ -31,4 +31,11 @@ flipud: 0.0 # image flip up-down (probability) fliplr: 0.5 # image flip left-right (probability) mosaic: 1.0 # image mosaic (probability) mixup: 0.1 # image mixup (probability) -copy_paste: 0.1 # segment copy-paste (probability) \ No newline at end of file +copy_paste: 0.1 # segment copy-paste (probability) + +# Custom Params +area_threshold: 0.3 # area threshold for random perspective +max_mistakes_size: 7680 # image size for mistakes mosaic +max_mistakes_subplots: 64 # subimages in mistakes mosaic +min_mistakes_iou: 0.5 # mistakes minimum iou for match +min_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file diff --git a/data/hyps/hyp.scratch-low.yaml b/data/hyps/hyp.scratch-low.yaml index e12a1d36ccaa..e9b5e6bdd575 100644 --- a/data/hyps/hyp.scratch-low.yaml +++ b/data/hyps/hyp.scratch-low.yaml @@ -32,4 +32,10 @@ fliplr: 0.5 # image flip left-right (probability) mosaic: 1.0 # image mosaic (probability) mixup: 0.0 # image mixup (probability) copy_paste: 0.0 # segment copy-paste (probability) -area_threshold: 0.5 # area threshold for random perspective + +# Custom Params +area_threshold: 0.3 # area threshold for random perspective +max_mistakes_size: 7680 # image size for mistakes mosaic +max_mistakes_subplots: 64 # subimages in mistakes mosaic +min_mistakes_iou: 0.5 # mistakes minimum iou for match +min_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file diff --git a/data/hyps/hyp.scratch.yaml b/data/hyps/hyp.scratch.yaml index 31f6d142e285..ace85ef44f4a 100644 --- a/data/hyps/hyp.scratch.yaml +++ b/data/hyps/hyp.scratch.yaml @@ -32,3 +32,10 @@ fliplr: 0.5 # image flip left-right (probability) mosaic: 1.0 # image mosaic (probability) mixup: 0.0 # image mixup (probability) copy_paste: 0.0 # segment copy-paste (probability) + +# Custom Params +area_threshold: 0.3 # area threshold for random perspective +max_mistakes_size: 7680 # image size for mistakes mosaic +max_mistakes_subplots: 64 # subimages in mistakes mosaic +min_mistakes_iou: 0.5 # mistakes minimum iou for match +min_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file diff --git a/train.py b/train.py index 3e6e847dd27a..26ca4e2c588b 100644 --- a/train.py +++ b/train.py @@ -399,7 +399,12 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary save_dir=validation_paths[val_index], plots=False, callbacks=callbacks, - compute_loss=compute_loss) + compute_loss=compute_loss, + max_mistakes_size=hyp['max_mistakes_size'], + max_mistakes_subplots=hyp['max_mistakes_subplots'], + minimum_mistakes_iou=hyp['minimum_mistakes_iou'], + minimum_mistakes_confidence=hyp['minimum_mistakes_confidence'] + ) # Update best mAP fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95] diff --git a/val.py b/val.py index 08550001a829..2891397fa2db 100644 --- a/val.py +++ b/val.py @@ -109,6 +109,10 @@ def run(data, plots=True, callbacks=Callbacks(), compute_loss=None, + max_mistakes_size = 7680, + max_mistakes_subplots = 64, + minimum_mistakes_iou = 0.5, + minimum_mistakes_confidence = 0.5, ): plt.style.use('default') @@ -230,21 +234,25 @@ def run(data, # Plot images if plots and batch_i < 16: + max_size=1920 + max_subplots=16 + valid_labels_path = save_dir.joinpath('labels') valid_predictions_path = save_dir.joinpath('pred') valid_labels_path.mkdir(exist_ok=True) valid_predictions_path.mkdir(exist_ok=True) f = valid_labels_path / f'val_batch{batch_i}_labels.jpg' # labels - Thread(target=plot_images, args=(img, targets, paths, f, names), daemon=True).start() + Thread(target=plot_images, args=(img, targets, paths, f, names, max_size, max_subplots), daemon=True).start() f = valid_predictions_path / f'val_batch{batch_i}_pred.jpg' # predictions - Thread(target=plot_images, args=(img, output_to_target(out), paths, f, names), daemon=True).start() + Thread(target=plot_images, args=(img, output_to_target(out), paths, f, names, max_size, max_subplots), daemon=True).start() - yolo_analyzer = YoloAnalyzeService(0.5, 0.5) + yolo_analyzer = YoloAnalyzeService(minimum_mistakes_iou, minimum_mistakes_confidence) mistakes = yolo_analyzer.analyze_batch(targets.cpu().numpy(), output_to_target(out)) mistakes_path = save_dir.joinpath("mistakes") mistakes_path.mkdir(exist_ok=True) f = mistakes_path / f'val_batch{batch_i}_mistakes.jpg' - Thread(target=plot_images, args=(img, mistakes, paths, f, names), daemon=True).start() + + Thread(target=plot_images, args=(img, mistakes, paths, f, names, max_mistakes_size, max_mistakes_subplots), daemon=True).start() # Compute statistics stats = [np.concatenate(x, 0) for x in zip(*stats)] # to numpy diff --git a/yolo_analyze_service.py b/yolo_analyze_service.py new file mode 100644 index 000000000000..b499c36f8470 --- /dev/null +++ b/yolo_analyze_service.py @@ -0,0 +1,90 @@ +""" +Copyright (c) Surveily. All rights reserved. +""" +from typing import List + +import numpy as np + +from yolo_detections_matching_service import YoloDetectionsMatchingService + + +class YoloAnalyzeService: + def __init__(self, minimum_iou: float, minimum_confidence: float) -> None: + self.__minimum_iou = minimum_iou + self.__minimum_confidence = minimum_confidence + self.__detections_matching_service = YoloDetectionsMatchingService() + + def analyze_batch(self, targets: np.ndarray, detections: np.ndarray): + wrong_detections = [] + not_detected_targets = [] + wrong_detections_labels = [] + + targets_batched = self.__parse_targets(targets) + detections_batched = self.__parse_targets(detections) + + for targets_batch, detections_batch in zip(targets_batched, detections_batched): + rows_assignment, columns_assignments, cost_matrix = self.__detections_matching_service.find_detections_assignment( + targets_batch, detections_batch) + + found_targets = set() + matched_detections = set() + + for row, col in zip(rows_assignment, columns_assignments): + iou = cost_matrix[row][col] + + if iou < self.__minimum_iou: + continue + + found_targets.add(row) + matched_detections.add(col) + + target_label = targets_batch[row][1] + detection_label = detections_batch[col][1] + + if target_label != detection_label: + wrong_detections_labels.append(detections_batch[col]) + continue + + for target_index in range(targets_batch.shape[0]): + if not found_targets.__contains__(target_index): + not_detected_targets.append(targets_batch[target_index]) + + for detection_index in range(detections_batch.shape[0]): + if not matched_detections.__contains__(detection_index): + wrong_detections.append(detections_batch[detection_index]) + + all_mistakes = wrong_detections + wrong_detections_labels + not_detected_targets + + if all_mistakes == []: + return np.array([]) + return np.concatenate(all_mistakes).reshape(-1, 7) + + def __parse_targets(self, targets: np.ndarray) -> List[np.ndarray]: + targets_batched = [] + last_batch_index = -1 + + for target in targets: + if target.shape[0] == 6: + target = np.append(target, [1.0]) + + if target[6] < self.__minimum_confidence: + continue + + batch_index = int(target[0]) + + while batch_index != last_batch_index: + targets_batched.append([]) + last_batch_index += 1 + + targets_batched[batch_index].append(target) + + targets_final = [] + + for targets_batch in targets_batched: + if targets_batch == []: + targets_final.append(np.array([])) + continue + target_final = np.concatenate(targets_batch) + target_final = target_final.reshape(-1, 7) + targets_final.append(target_final) + return targets_final diff --git a/yolo_detections_matching_service.py b/yolo_detections_matching_service.py new file mode 100644 index 000000000000..ee1e01db0dbd --- /dev/null +++ b/yolo_detections_matching_service.py @@ -0,0 +1,52 @@ +""" +Copyright (c) Surveily. All rights reserved. +""" + +from collections import namedtuple +from typing import Tuple + +import numpy as np +from scipy.optimize import linear_sum_assignment + + +class YoloDetectionsMatchingService: + + def __init__(self) -> None: + pass + + def find_detections_assignment(self, targets: np.ndarray, detections: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + cost_matrix = self.__calculate_cost_matrix( + targets, detections) + + rows_assignment, cols_assignments = linear_sum_assignment( + -cost_matrix) + + return rows_assignment, cols_assignments, cost_matrix + + def __calculate_cost_matrix(self, targets: np.ndarray, detections: np.ndarray) -> np.ndarray: + cost_matrix = np.zeros((targets.shape[0], detections.shape[0])) + for targets_index in range(targets.shape[0]): + for detections_index in range(detections.shape[0]): + cost_matrix[targets_index][detections_index] = self.__calculate_iou_yolo( + targets[targets_index], detections[detections_index]) + + return cost_matrix + + def __calculate_iou_yolo(self, target: np.ndarray, detection: np.ndarray) -> float: + Rect = namedtuple("Rect", "x y width height") + lhs = Rect(target[2], target[3], target[4], target[5]) + rhs = Rect(detection[2], detection[3], detection[4], detection[5]) + + intersection_width = min( + lhs.x + lhs.width, rhs.x + rhs.width) - max(lhs.x, rhs.x) + intersection_height = min( + lhs.y + lhs.height, rhs.y + rhs.height) - max(lhs.y, rhs.y) + + if intersection_width <= 0 or intersection_height <= 0: + return 0 + + intersection = intersection_width * intersection_height + union = ((lhs.width * lhs.height) + + (rhs.width * rhs.height)) - intersection + + return intersection / union From e239d3865b52d230ac3e9457e6166c42d7433e66 Mon Sep 17 00:00:00 2001 From: Kacper Szufnarowski Date: Wed, 16 Nov 2022 10:17:19 +0100 Subject: [PATCH 14/23] Added changes in plots --- utils/plots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/plots.py b/utils/plots.py index f1f4827ec6e0..a55d0cec73fb 100644 --- a/utils/plots.py +++ b/utils/plots.py @@ -148,7 +148,7 @@ def output_to_target(output): return np.array(targets) -def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=7680, max_subplots=64): +def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=1920, max_subplots=16): # Plot image grid with labels if isinstance(images, torch.Tensor): images = images.cpu().float().numpy() From 54c346ec3f78d7842b017678e1e5618f175440a0 Mon Sep 17 00:00:00 2001 From: Kacper Szufnarowski Date: Wed, 16 Nov 2022 10:38:32 +0100 Subject: [PATCH 15/23] Removed copyrights --- yolo_analyze_service.py | 3 --- yolo_detections_matching_service.py | 4 ---- 2 files changed, 7 deletions(-) diff --git a/yolo_analyze_service.py b/yolo_analyze_service.py index b499c36f8470..fa2705f75f38 100644 --- a/yolo_analyze_service.py +++ b/yolo_analyze_service.py @@ -1,6 +1,3 @@ -""" -Copyright (c) Surveily. All rights reserved. -""" from typing import List import numpy as np diff --git a/yolo_detections_matching_service.py b/yolo_detections_matching_service.py index ee1e01db0dbd..f6e7cf0a5f15 100644 --- a/yolo_detections_matching_service.py +++ b/yolo_detections_matching_service.py @@ -1,7 +1,3 @@ -""" -Copyright (c) Surveily. All rights reserved. -""" - from collections import namedtuple from typing import Tuple From 98cb6fb38aed549ac70d5e37ee35e4efff61392e Mon Sep 17 00:00:00 2001 From: Krzysztof Begiedza <8686134+kbegiedza@users.noreply.github.com> Date: Fri, 18 Nov 2022 12:55:50 +0100 Subject: [PATCH 16/23] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 22b51fc490e3..d602142ba267 100755 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,7 @@ Pillow>=7.1.2 PyYAML>=5.3.1 requests>=2.23.0 scipy>=1.4.1 +thop>=0.1.1 # FLOPs computation torch>=1.7.0 torchvision>=0.8.1 tqdm>=4.41.0 @@ -33,4 +34,3 @@ seaborn>=0.11.0 # Cython # for pycocotools https://github.com/cocodataset/cocoapi/issues/172 # pycocotools>=2.0 # COCO mAP # roboflow -thop # FLOPs computation From 6544d52764afc6435a1ce613f250a90af1744b82 Mon Sep 17 00:00:00 2001 From: Kacper Szufnarowski Date: Fri, 18 Nov 2022 16:31:46 +0100 Subject: [PATCH 17/23] Fixed loss.py for newer torch, fixed typo in hyps --- data/hyps/hyp.finetune.yaml | 8 ++++---- data/hyps/hyp.finetune_objects365.yaml | 8 ++++---- data/hyps/hyp.scratch-high.yaml | 8 ++++---- data/hyps/hyp.scratch-low.yaml | 8 ++++---- data/hyps/hyp.scratch.yaml | 8 ++++---- train.py | 4 ++-- utils/loss.py | 6 +++--- val.py | 6 +++--- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/data/hyps/hyp.finetune.yaml b/data/hyps/hyp.finetune.yaml index 71e8404ec9d7..a328d93a2307 100644 --- a/data/hyps/hyp.finetune.yaml +++ b/data/hyps/hyp.finetune.yaml @@ -40,7 +40,7 @@ copy_paste: 0.0 # Custom Params area_threshold: 0.3 # area threshold for random perspective -max_mistakes_size: 7680 # image size for mistakes mosaic -max_mistakes_subplots: 64 # subimages in mistakes mosaic -min_mistakes_iou: 0.5 # mistakes minimum iou for match -min_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file +maximum_mistakes_size: 7680 # image size for mistakes mosaic +maximum_mistakes_subplots: 64 # subimages in mistakes mosaic +minimum_mistakes_iou: 0.5 # mistakes minimum iou for match +minimum_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file diff --git a/data/hyps/hyp.finetune_objects365.yaml b/data/hyps/hyp.finetune_objects365.yaml index 81ddddad4a00..af75de5cf8d0 100644 --- a/data/hyps/hyp.finetune_objects365.yaml +++ b/data/hyps/hyp.finetune_objects365.yaml @@ -32,7 +32,7 @@ copy_paste: 0.0 # Custom Params area_threshold: 0.3 # area threshold for random perspective -max_mistakes_size: 7680 # image size for mistakes mosaic -max_mistakes_subplots: 64 # subimages in mistakes mosaic -min_mistakes_iou: 0.5 # mistakes minimum iou for match -min_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file +maximum_mistakes_size: 7680 # image size for mistakes mosaic +maximum_mistakes_subplots: 64 # subimages in mistakes mosaic +minimum_mistakes_iou: 0.5 # mistakes minimum iou for match +minimum_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file diff --git a/data/hyps/hyp.scratch-high.yaml b/data/hyps/hyp.scratch-high.yaml index 2aefe4f5a302..6a06e53f96db 100644 --- a/data/hyps/hyp.scratch-high.yaml +++ b/data/hyps/hyp.scratch-high.yaml @@ -35,7 +35,7 @@ copy_paste: 0.1 # segment copy-paste (probability) # Custom Params area_threshold: 0.3 # area threshold for random perspective -max_mistakes_size: 7680 # image size for mistakes mosaic -max_mistakes_subplots: 64 # subimages in mistakes mosaic -min_mistakes_iou: 0.5 # mistakes minimum iou for match -min_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file +maximum_mistakes_size: 7680 # image size for mistakes mosaic +maximum_mistakes_subplots: 64 # subimages in mistakes mosaic +minimum_mistakes_iou: 0.5 # mistakes minimum iou for match +minimum_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file diff --git a/data/hyps/hyp.scratch-low.yaml b/data/hyps/hyp.scratch-low.yaml index e9b5e6bdd575..f6d66263baf9 100644 --- a/data/hyps/hyp.scratch-low.yaml +++ b/data/hyps/hyp.scratch-low.yaml @@ -35,7 +35,7 @@ copy_paste: 0.0 # segment copy-paste (probability) # Custom Params area_threshold: 0.3 # area threshold for random perspective -max_mistakes_size: 7680 # image size for mistakes mosaic -max_mistakes_subplots: 64 # subimages in mistakes mosaic -min_mistakes_iou: 0.5 # mistakes minimum iou for match -min_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file +maximum_mistakes_size: 7680 # image size for mistakes mosaic +maximum_mistakes_subplots: 64 # subimages in mistakes mosaic +minimum_mistakes_iou: 0.5 # mistakes minimum iou for match +minimum_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file diff --git a/data/hyps/hyp.scratch.yaml b/data/hyps/hyp.scratch.yaml index ace85ef44f4a..beb3124af829 100644 --- a/data/hyps/hyp.scratch.yaml +++ b/data/hyps/hyp.scratch.yaml @@ -35,7 +35,7 @@ copy_paste: 0.0 # segment copy-paste (probability) # Custom Params area_threshold: 0.3 # area threshold for random perspective -max_mistakes_size: 7680 # image size for mistakes mosaic -max_mistakes_subplots: 64 # subimages in mistakes mosaic -min_mistakes_iou: 0.5 # mistakes minimum iou for match -min_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file +maximum_mistakes_size: 7680 # image size for mistakes mosaic +maximum_mistakes_subplots: 64 # subimages in mistakes mosaic +minimum_mistakes_iou: 0.5 # mistakes minimum iou for match +minimum_mistakes_confidence: 0.5 # mistakes minimum confidence for checking \ No newline at end of file diff --git a/train.py b/train.py index 26ca4e2c588b..ab2c325082f0 100644 --- a/train.py +++ b/train.py @@ -400,8 +400,8 @@ def train(hyp, # path/to/hyp.yaml or hyp dictionary plots=False, callbacks=callbacks, compute_loss=compute_loss, - max_mistakes_size=hyp['max_mistakes_size'], - max_mistakes_subplots=hyp['max_mistakes_subplots'], + maximum_mistakes_size=hyp['maximum_mistakes_size'], + maximum_mistakes_subplots=hyp['maximum_mistakes_subplots'], minimum_mistakes_iou=hyp['minimum_mistakes_iou'], minimum_mistakes_confidence=hyp['minimum_mistakes_confidence'] ) diff --git a/utils/loss.py b/utils/loss.py index fac432d0edc3..cc2b9850e51c 100644 --- a/utils/loss.py +++ b/utils/loss.py @@ -181,8 +181,8 @@ def build_targets(self, p, targets): ], device=targets.device).float() * g # offsets for i in range(self.nl): - anchors = self.anchors[i] - gain[2:6] = torch.tensor(p[i].shape)[[3, 2, 3, 2]] # xyxy gain + anchors, shape = self.anchors[i], p[i].shape + gain[2:6] = torch.tensor(shape)[[3, 2, 3, 2]] # xyxy gain # Match targets to anchors t = targets * gain @@ -214,7 +214,7 @@ def build_targets(self, p, targets): # Append a = t[:, 6].long() # anchor indices - indices.append((b, a, gj.clamp_(0, gain[3] - 1), gi.clamp_(0, gain[2] - 1))) # image, anchor, grid indices + indices.append((b, a, gj.clamp_(0, shape[2] - 1), gi.clamp_(0, shape[3] - 1))) # image, anchor, grid tbox.append(torch.cat((gxy - gij, gwh), 1)) # box anch.append(anchors[a]) # anchors tcls.append(c) # class diff --git a/val.py b/val.py index 2891397fa2db..2e403f57effc 100644 --- a/val.py +++ b/val.py @@ -109,8 +109,8 @@ def run(data, plots=True, callbacks=Callbacks(), compute_loss=None, - max_mistakes_size = 7680, - max_mistakes_subplots = 64, + maximum_mistakes_size = 7680, + maximum_mistakes_subplots = 64, minimum_mistakes_iou = 0.5, minimum_mistakes_confidence = 0.5, ): @@ -252,7 +252,7 @@ def run(data, mistakes_path.mkdir(exist_ok=True) f = mistakes_path / f'val_batch{batch_i}_mistakes.jpg' - Thread(target=plot_images, args=(img, mistakes, paths, f, names, max_mistakes_size, max_mistakes_subplots), daemon=True).start() + Thread(target=plot_images, args=(img, mistakes, paths, f, names, maximum_mistakes_size, maximum_mistakes_subplots), daemon=True).start() # Compute statistics stats = [np.concatenate(x, 0) for x in zip(*stats)] # to numpy From ddb5104cf292ec4c1b95c4b86fcae9aeb333f8c2 Mon Sep 17 00:00:00 2001 From: Kacper Szufnarowski Date: Thu, 8 Dec 2022 11:14:40 +0100 Subject: [PATCH 18/23] Fixes after testing --- utils/dataloaders.py | 25 +++++++++++++++++-------- utils/loggers/__init__.py | 3 ++- utils/plots.py | 4 +--- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/utils/dataloaders.py b/utils/dataloaders.py index 6b10cbc86ed2..fad03e27ae0c 100644 --- a/utils/dataloaders.py +++ b/utils/dataloaders.py @@ -303,16 +303,18 @@ def __next__(self): else: # Read image self.count += 1 - img0 = cv2.imread(path,cv2.IMREAD_GRAYSCALE) # BGR - img0 = img0.reshape(img0.shape[0],img0.shape[1],1) + img0 = cv2.imread(path,cv2.IMREAD_GRAYSCALE) # GRAY + img0 = img0.reshape(img0.shape[0],img0.shape[1],1) assert im0 is not None, f'Image Not Found {path}' s = f'image {self.count}/{self.nf} {path}: ' if self.transforms: im = self.transforms(im0) # transforms + im = im.reshape(im.shape[0], im.shape[1], 1) else: im = letterbox(im0, self.img_size, stride=self.stride, auto=self.auto)[0] # padded resize - im = im.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB + # im = im.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB + im = im.reshape(im.shape[0], im.shape[1], 1) # GRAY im = np.ascontiguousarray(im) # contiguous return path, im, im0, self.cap, s @@ -592,6 +594,7 @@ def check_cache_ram(self, safety_margin=0.1, prefix=''): n = min(self.n, 30) # extrapolate from 30 random images for _ in range(n): im = cv2.imread(random.choice(self.im_files), cv2.IMREAD_GRAYSCALE) # sample image + im = im.reshape(im.shape[0], im.shape[1], 1) ratio = self.img_size / max(im.shape[0], im.shape[1]) # max(h, w) # ratio b += im.nbytes * ratio ** 2 mem_required = b * self.n / n # GB required to cache dataset into RAM @@ -721,6 +724,7 @@ def __getitem__(self, index): # Convert #img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB + img = img.reshape(1, img.shape[0], img.shape[1]) img = np.ascontiguousarray(img) return torch.from_numpy(img), labels_out, self.im_files[index], shapes @@ -749,7 +753,8 @@ def cache_images_to_disk(self, i): # Saves an image as an *.npy file for faster loading f = self.npy_files[i] if not f.exists(): - np.save(f.as_posix(), cv2.imread(self.im_files[i],cv2.IMREAD_GRAYSCALE)) + image = cv2.imread(self.im_files[i],cv2.IMREAD_GRAYSCALE) + np.save(f.as_posix(), image.reshape(image.shape[0], image.shape[1], 1)) def load_mosaic(self, index): # YOLOv5 4-mosaic loader. Loads 1 image + 3 random images into a 4-image mosaic @@ -1186,13 +1191,17 @@ def __init__(self, root, augment, imgsz, cache=False): def __getitem__(self, i): f, j, fn, im = self.samples[i] # filename, index, filename.with_suffix('.npy'), image if self.cache_ram and im is None: - im = self.samples[i][3] = cv2.imread(f,cv2.IMREAD_GRAYSCALE).reshape(im.shape[0],im.shape[1],1) + img = cv2.imread(f,cv2.IMREAD_GRAYSCALE) + im = self.samples[i][3] = img.reshape(img.shape[0],img.shape[1],1) elif self.cache_disk: if not fn.exists(): # load npy - np.save(fn.as_posix(), cv2.imread(f,cv2.IMREAD_GRAYSCALE).reshape(im.shape[0],im.shape[1],1)) - im = np.load(fn).reshape(im.shape[0], im.shape[1], 1) + img = cv2.imread(f,cv2.IMREAD_GRAYSCALE) + np.save(fn.as_posix(), img.reshape(img.shape[0],img.shape[1],1)) + im = np.load(fn) + im = im.reshape(im.shape[0], im.shape[1], 1) else: # read image - im = cv2.imread(f,cv2.IMREAD_GRAYSCALE).reshape(im.shape[0],im.shape[1],1) # BGR + im = cv2.imread(f,cv2.IMREAD_GRAYSCALE) + im = im.reshape(im.shape[0],im.shape[1],1) # BGR if self.album_transforms: sample = self.album_transforms(image=cv2.cvtColor(im, cv2.COLOR_BGR2RGB))["image"] else: diff --git a/utils/loggers/__init__.py b/utils/loggers/__init__.py index 8fd748000983..c51ac81f02eb 100644 --- a/utils/loggers/__init__.py +++ b/utils/loggers/__init__.py @@ -102,6 +102,7 @@ def __init__(self, save_dir=None, weights=None, opt=None, hyp=None, logger=None, prefix = colorstr('TensorBoard: ') self.logger.info(f"{prefix}Start with 'tensorboard --logdir {s.parent}', view at http://localhost:6006/") self.tb = SummaryWriter(str(s)) + self.tb = False # W&B if wandb and 'wandb' in self.include: @@ -397,7 +398,7 @@ def log_tensorboard_graph(tb, model, imgsz=(640, 640)): try: p = next(model.parameters()) # for device, type imgsz = (imgsz, imgsz) if isinstance(imgsz, int) else imgsz # expand - im = torch.zeros((1, 3, *imgsz)).to(p.device).type_as(p) # input image (WARNING: must be zeros, not empty) + im = torch.zeros((1, 1, *imgsz)).to(p.device).type_as(p) # input image (WARNING: must be zeros, not empty) with warnings.catch_warnings(): warnings.simplefilter('ignore') # suppress jit trace warning tb.add_graph(torch.jit.trace(de_parallel(model), im, strict=False), []) diff --git a/utils/plots.py b/utils/plots.py index 03960b529ef5..edf5b3dd79e2 100644 --- a/utils/plots.py +++ b/utils/plots.py @@ -226,15 +226,13 @@ def output_to_target(output, max_det=300): @threaded -def plot_images(images, targets, paths=None, fname='images.jpg', names=None): +def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=1920, max_subplots=16): # Plot image grid with labels if isinstance(images, torch.Tensor): images = images.cpu().float().numpy() if isinstance(targets, torch.Tensor): targets = targets.cpu().numpy() - max_size = 1920 # max image size - max_subplots = 16 # max image subplots, i.e. 4x4 bs, _, h, w = images.shape # batch size, _, height, width bs = min(bs, max_subplots) # limit plot images ns = np.ceil(bs ** 0.5) # number of subplots (square) From 9e3828993c63ba499d0c9af9c88803442e4be9b7 Mon Sep 17 00:00:00 2001 From: Kacper Szufnarowski Date: Thu, 8 Dec 2022 13:45:08 +0100 Subject: [PATCH 19/23] Added switch for rgb and grayscale --- train.py | 7 +++++-- utils/dataloaders.py | 40 ++++++++++++++++++++++++++++------------ val.py | 5 ++++- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/train.py b/train.py index f25f5d80bbc2..409e92a485fd 100644 --- a/train.py +++ b/train.py @@ -222,7 +222,8 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio image_weights=opt.image_weights, quad=opt.quad, prefix=colorstr('train: '), - shuffle=True) + shuffle=True, + rgb_mode=opt.rgb_mode) if dataset.albumentations.transform is not None: augemntations = {str(number+1):str(transform) for number,transform in enumerate(dataset.albumentations.transform.transforms) if transform.p} @@ -256,7 +257,8 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio rank=-1, workers=workers, pad=0.5, - prefix=colorstr('val: '))[0]) + prefix=colorstr('val: '))[0], + rgb_mode=opt.rgb_mode) if not resume: if not opt.noautoanchor: check_anchors(dataset, model=model, thr=hyp['anchor_t'], imgsz=imgsz) # run AutoAnchor @@ -515,6 +517,7 @@ def parse_opt(known=False): parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)') parser.add_argument('--seed', type=int, default=0, help='Global training seed') parser.add_argument('--local_rank', type=int, default=-1, help='Automatic DDP Multi-GPU argument, do not modify') + parser.add_argument('--rgb-mode', action='store_true', help='train model in grayscale mode, with image_channels=1.') # Logger arguments parser.add_argument('--entity', default=None, help='Entity') diff --git a/utils/dataloaders.py b/utils/dataloaders.py index fad03e27ae0c..4bec0d6b160c 100644 --- a/utils/dataloaders.py +++ b/utils/dataloaders.py @@ -115,7 +115,8 @@ def create_dataloader(path, image_weights=False, quad=False, prefix='', - shuffle=False): + shuffle=False, + rgb_mode=False): if rect and shuffle: LOGGER.warning('WARNING ⚠️ --rect is incompatible with DataLoader shuffle, setting shuffle=False') shuffle = False @@ -132,7 +133,8 @@ def create_dataloader(path, stride=int(stride), pad=pad, image_weights=image_weights, - prefix=prefix) + prefix=prefix, + rgb_mode=rgb_mode) batch_size = min(batch_size, len(dataset)) nd = torch.cuda.device_count() # number of CUDA devices @@ -451,7 +453,8 @@ def __init__(self, stride=32, pad=0.0, min_items=0, - prefix=''): + prefix='', + rgb_mode=False): self.img_size = img_size self.augment = augment self.hyp = hyp @@ -462,6 +465,7 @@ def __init__(self, self.stride = stride self.path = path self.albumentations = Albumentations(size=img_size) if augment else None + self.rgb_mode = rgb_mode try: f = [] # image files @@ -593,8 +597,11 @@ def check_cache_ram(self, safety_margin=0.1, prefix=''): b, gb = 0, 1 << 30 # bytes of cached images, bytes per gigabytes n = min(self.n, 30) # extrapolate from 30 random images for _ in range(n): - im = cv2.imread(random.choice(self.im_files), cv2.IMREAD_GRAYSCALE) # sample image - im = im.reshape(im.shape[0], im.shape[1], 1) + if self.rgb_mode: + im = cv2.imread(random.choice(self.im_files)) + else: + im = cv2.imread(random.choice(self.im_files), cv2.IMREAD_GRAYSCALE) # sample image + im = im.reshape(im.shape[0], im.shape[1], 1) ratio = self.img_size / max(im.shape[0], im.shape[1]) # max(h, w) # ratio b += im.nbytes * ratio ** 2 mem_required = b * self.n / n # GB required to cache dataset into RAM @@ -723,8 +730,10 @@ def __getitem__(self, index): labels_out[:, 1:] = torch.from_numpy(labels) # Convert - #img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB - img = img.reshape(1, img.shape[0], img.shape[1]) + if self.rgb_mode: + img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB + else: + img = img.reshape(1, img.shape[0], img.shape[1]) img = np.ascontiguousarray(img) return torch.from_numpy(img), labels_out, self.im_files[index], shapes @@ -736,8 +745,11 @@ def load_image(self, i): if fn.exists(): # load npy im = np.load(fn) else: # read image - im = cv2.imread(f,cv2.IMREAD_GRAYSCALE) # BGR - im = im.reshape(im.shape[0],im.shape[1],1) + if self.rgb_mode: + im = cv2.imread(f) + else: + im = cv2.imread(f,cv2.IMREAD_GRAYSCALE) # BGR + im = im.reshape(im.shape[0],im.shape[1],1) assert im is not None, f'Image Not Found {f}' h0, w0 = im.shape[:2] # orig hw r = self.img_size / max(h0, w0) # ratio @@ -745,7 +757,8 @@ def load_image(self, i): interp = cv2.INTER_LINEAR if (self.augment or r > 1) else cv2.INTER_AREA im = cv2.resize(im, (int(w0 * r), int(h0 * r)), interpolation=interp) - im = im.reshape(im.shape[0],im.shape[1],1) + if not self.rgb_mode: + im = im.reshape(im.shape[0],im.shape[1],1) return im, (h0, w0), im.shape[:2] # im, hw_original, hw_resized return self.ims[i], self.im_hw0[i], self.im_hw[i] # im, hw_original, hw_resized @@ -753,8 +766,11 @@ def cache_images_to_disk(self, i): # Saves an image as an *.npy file for faster loading f = self.npy_files[i] if not f.exists(): - image = cv2.imread(self.im_files[i],cv2.IMREAD_GRAYSCALE) - np.save(f.as_posix(), image.reshape(image.shape[0], image.shape[1], 1)) + if self.rgb_mode: + np.save(f.as_posix(), cv2.imread(self.im_files[i])) + else: + image = cv2.imread(self.im_files[i],cv2.IMREAD_GRAYSCALE) + np.save(f.as_posix(), image.reshape(image.shape[0], image.shape[1], 1)) def load_mosaic(self, index): # YOLOv5 4-mosaic loader. Loads 1 image + 3 random images into a 4-image mosaic diff --git a/val.py b/val.py index 2f0adf53d884..327b8b9a2ca7 100644 --- a/val.py +++ b/val.py @@ -128,6 +128,7 @@ def run( plots=True, callbacks=Callbacks(), compute_loss=None, + rgb_mode=False, maximum_mistakes_size = 7680, maximum_mistakes_subplots = 64, minimum_mistakes_iou = 0.5, @@ -188,7 +189,8 @@ def run( pad=pad, rect=rect, workers=workers, - prefix=colorstr(f'{task}: '))[0] + prefix=colorstr(f'{task}: '), + rgb_mode=rgb_mode)[0] seen = 0 confusion_matrix = ConfusionMatrix(nc=nc) @@ -438,6 +440,7 @@ def parse_opt(): parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference') parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference') + parser.add_argument('--rgb', action='store_true', help='train model in grayscale mode, with image_channels=1.') opt = parser.parse_args() opt.data = check_yaml(opt.data) # check YAML opt.save_json |= opt.data.endswith('coco.yaml') From c22b8b223a6da8f16cb052e1b00a0fce8daa1a81 Mon Sep 17 00:00:00 2001 From: Kacper Szufnarowski Date: Thu, 8 Dec 2022 14:01:32 +0100 Subject: [PATCH 20/23] Typo and small mistakes fixes --- train.py | 11 +++++++++-- utils/dataloaders.py | 3 ++- val.py | 7 +++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/train.py b/train.py index 409e92a485fd..197196378ef0 100644 --- a/train.py +++ b/train.py @@ -400,6 +400,7 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio plots=False, callbacks=callbacks, compute_loss=compute_loss, + rgb_mode=opt.rgb_mode, maximum_mistakes_size=hyp['maximum_mistakes_size'], maximum_mistakes_subplots=hyp['maximum_mistakes_subplots'], minimum_mistakes_iou=hyp['minimum_mistakes_iou'], @@ -470,7 +471,13 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio verbose=True, plots=plots, callbacks=callbacks, - compute_loss=compute_loss) # val best model with plots + compute_loss=compute_loss, + rgb_mode=opt.rgb_mode, + maximum_mistakes_size=hyp['maximum_mistakes_size'], + maximum_mistakes_subplots=hyp['maximum_mistakes_subplots'], + minimum_mistakes_iou=hyp['minimum_mistakes_iou'], + minimum_mistakes_confidence=hyp['minimum_mistakes_confidence'] + ) # val best model with plots if is_coco: callbacks.run('on_fit_epoch_end', list(mloss) + list(results) + lr, epoch, best_fitness, fi) @@ -517,7 +524,7 @@ def parse_opt(known=False): parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)') parser.add_argument('--seed', type=int, default=0, help='Global training seed') parser.add_argument('--local_rank', type=int, default=-1, help='Automatic DDP Multi-GPU argument, do not modify') - parser.add_argument('--rgb-mode', action='store_true', help='train model in grayscale mode, with image_channels=1.') + parser.add_argument('--rgb-mode', action='store_true', help='train model in rgb mode, with image_channels=3.') # Logger arguments parser.add_argument('--entity', default=None, help='Entity') diff --git a/utils/dataloaders.py b/utils/dataloaders.py index 4bec0d6b160c..38c93651de86 100644 --- a/utils/dataloaders.py +++ b/utils/dataloaders.py @@ -707,7 +707,8 @@ def __getitem__(self, index): nl = len(labels) # update after albumentations # HSV color-space - # augment_hsv(img, hgain=hyp['hsv_h'], sgain=hyp['hsv_s'], vgain=hyp['hsv_v']) + if self.rgb_mode: + augment_hsv(img, hgain=hyp['hsv_h'], sgain=hyp['hsv_s'], vgain=hyp['hsv_v']) # Flip up-down if random.random() < hyp['flipud']: diff --git a/val.py b/val.py index 327b8b9a2ca7..498df591e06b 100644 --- a/val.py +++ b/val.py @@ -177,7 +177,10 @@ def run( ncm = model.model.nc assert ncm == nc, f'{weights} ({ncm} classes) trained on different --data than what you passed ({nc} ' \ f'classes). Pass correct combination of --weights and --data that are trained together.' - model.warmup(imgsz=(1 if pt else batch_size, 1, imgsz, imgsz)) # warmup + if rgb_mode: + model.warmup(imgsz=(3 if pt else batch_size, 1, imgsz, imgsz)) # warmup + else: + model.warmup(imgsz=(1 if pt else batch_size, 1, imgsz, imgsz)) # warmup pad, rect = (0.0, False) if task == 'speed' else (0.5, pt) # square inference for benchmarks task = task if task in ('train', 'val', 'test') else 'val' # path to train/val/test images @@ -440,7 +443,7 @@ def parse_opt(): parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference') parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference') - parser.add_argument('--rgb', action='store_true', help='train model in grayscale mode, with image_channels=1.') + parser.add_argument('--rgb-mode', action='store_true', help='train model in rgb mode, with image_channels=3.') opt = parser.parse_args() opt.data = check_yaml(opt.data) # check YAML opt.save_json |= opt.data.endswith('coco.yaml') From a833598c04eb45b596f88d360c26702898eeed11 Mon Sep 17 00:00:00 2001 From: Kacper Szufnarowski Date: Fri, 9 Dec 2022 09:55:43 +0100 Subject: [PATCH 21/23] Rgb/Grayscale handling for tb graph --- utils/loggers/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/utils/loggers/__init__.py b/utils/loggers/__init__.py index c51ac81f02eb..57901365531f 100644 --- a/utils/loggers/__init__.py +++ b/utils/loggers/__init__.py @@ -102,7 +102,6 @@ def __init__(self, save_dir=None, weights=None, opt=None, hyp=None, logger=None, prefix = colorstr('TensorBoard: ') self.logger.info(f"{prefix}Start with 'tensorboard --logdir {s.parent}', view at http://localhost:6006/") self.tb = SummaryWriter(str(s)) - self.tb = False # W&B if wandb and 'wandb' in self.include: @@ -186,7 +185,7 @@ def on_train_batch_end(self, model, ni, imgs, targets, paths, vals): f = new_save_dir / f'train_batch{ni}.jpg' # filename plot_images(imgs, targets, paths, f) if ni == 0 and self.tb and not self.opt.sync_bn: - log_tensorboard_graph(self.tb, model, imgsz=(self.opt.imgsz, self.opt.imgsz)) + log_tensorboard_graph(self.tb, model, imgsz=(self.opt.imgsz, self.opt.imgsz),rgb_mode=self.opt.rgb_mode) if ni == 10 and (self.wandb or self.clearml): files = sorted(self.save_dir.glob('train*.jpg')) if self.wandb: @@ -393,12 +392,15 @@ def update_params(self, params): wandb.run.config.update(params, allow_val_change=True) -def log_tensorboard_graph(tb, model, imgsz=(640, 640)): +def log_tensorboard_graph(tb, model, imgsz=(640, 640), rgb_mode=False): # Log model graph to TensorBoard try: p = next(model.parameters()) # for device, type imgsz = (imgsz, imgsz) if isinstance(imgsz, int) else imgsz # expand - im = torch.zeros((1, 1, *imgsz)).to(p.device).type_as(p) # input image (WARNING: must be zeros, not empty) + if rgb_mode: + im = torch.zeros((1, 3, *imgsz)).to(p.device).type_as(p) + else: + im = torch.zeros((1, 1, *imgsz)).to(p.device).type_as(p) # input image (WARNING: must be zeros, not empty) with warnings.catch_warnings(): warnings.simplefilter('ignore') # suppress jit trace warning tb.add_graph(torch.jit.trace(de_parallel(model), im, strict=False), []) From 5c10af83529ad71aa1f87f20c3eb970361a014fd Mon Sep 17 00:00:00 2001 From: Kacper Szufnarowski Date: Fri, 9 Dec 2022 11:25:54 +0100 Subject: [PATCH 22/23] Typo fixes --- train.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/train.py b/train.py index 197196378ef0..904715e2fbe5 100644 --- a/train.py +++ b/train.py @@ -117,15 +117,10 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio if type(val_paths) == list: print('Detected {} validation sets'.format(len(val_paths))) multi_val = True - for index, val_path in enumerate(val_paths if type(val_paths) == list else [val_paths]): - temp_val_path = ''.join([c for c in val_path if c.isalpha()]).lower() - if 'validyolo' in temp_val_path: - temp_val_path = val_path[:len(val_path)-11] - elif len(temp_val_path) >= 3: - temp_val_path = temp_val_path[:3] - else: - temp_val_path = str(index) - best.append(w.joinpath('best_' + str(Path(temp_val_path).stem) + '.pt')) + for val_path in val_paths if type(val_paths) == list else [val_paths]: + temp_val_path = Path(val_path).stem.replace("Valid", "").replace("Yolo", "") + + best.append(w.joinpath(f'best_{temp_val_path}.pt')) temp_val_path = Path(save_dir.joinpath('results_' + temp_val_path)) temp_val_path.mkdir(exist_ok=True) temp_val_path.joinpath('classes').mkdir(exist_ok=True) @@ -154,6 +149,7 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio else: model = Model(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create amp = check_amp(model) # check AMP + amp = True # Freeze freeze = [f'model.{x}.' for x in (freeze if len(freeze) > 1 else range(freeze[0]))] # layers to freeze @@ -257,8 +253,8 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio rank=-1, workers=workers, pad=0.5, - prefix=colorstr('val: '))[0], - rgb_mode=opt.rgb_mode) + prefix=colorstr('val: '), + rgb_mode=opt.rgb_mode)[0]) if not resume: if not opt.noautoanchor: check_anchors(dataset, model=model, thr=hyp['anchor_t'], imgsz=imgsz) # run AutoAnchor From 0e3b2716369344fc6971a1252f02d3d860fc4a0e Mon Sep 17 00:00:00 2001 From: Kacper Szufnarowski Date: Fri, 9 Dec 2022 11:28:19 +0100 Subject: [PATCH 23/23] Tiny code readability improvement --- train.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/train.py b/train.py index 904715e2fbe5..da49c38d3a96 100644 --- a/train.py +++ b/train.py @@ -117,7 +117,8 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio if type(val_paths) == list: print('Detected {} validation sets'.format(len(val_paths))) multi_val = True - for val_path in val_paths if type(val_paths) == list else [val_paths]: + + for val_path in (val_paths if multi_val else [val_paths]): temp_val_path = Path(val_path).stem.replace("Valid", "").replace("Yolo", "") best.append(w.joinpath(f'best_{temp_val_path}.pt')) @@ -150,7 +151,7 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio model = Model(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create amp = check_amp(model) # check AMP amp = True - + # Freeze freeze = [f'model.{x}.' for x in (freeze if len(freeze) > 1 else range(freeze[0]))] # layers to freeze for k, v in model.named_parameters():