Download utils.py from amd/yolov5s: direct link, hf CLI and curl.
- Browser
- Download file 39.5 kB
-
https://huggingface.co/amd/yolov5s/resolve/refs%2Fpr%2F2/utils.py
- Command line
-
hf download hf://amd/yolov5s@refs/pr/2/utils.py
-
curl -L -o utils.py https://huggingface.co/amd/yolov5s/resolve/refs%2Fpr%2F2/utils.py
39.5 kB
| import numpy as np | |
| import cv2 | |
| from pathlib import Path | |
| import torch | |
| import time | |
| import torchvision | |
| import re | |
| import glob | |
| from torch.utils.data import Dataset | |
| import yaml | |
| import os | |
| from multiprocessing.pool import ThreadPool, Pool | |
| from tqdm import tqdm | |
| from itertools import repeat | |
| import logging | |
| from PIL import Image, ExifTags | |
| import hashlib | |
| import sys | |
| import pathlib | |
| CURRENT_DIR = pathlib.Path(__file__).parent | |
| sys.path.append(str(CURRENT_DIR)) | |
| # Parameters | |
| IMG_FORMATS = ['bmp', 'jpg', 'jpeg', 'png', 'tif', 'tiff', 'dng', 'webp', 'mpo'] | |
| NUM_THREADS = min(8, os.cpu_count()) | |
| img_formats = IMG_FORMATS # acceptable image suffixes | |
| vid_formats = ['mov', 'avi', 'mp4', 'mpg', 'mpeg', 'm4v', 'wmv', 'mkv'] # acceptable video suffixes | |
| # Get orientation exif tag | |
| for orientation in ExifTags.TAGS.keys(): | |
| if ExifTags.TAGS[orientation] == 'Orientation': | |
| break | |
| def make_dirs(dir='./datasets/coco'): | |
| # Create folders | |
| dir = Path(dir) | |
| for p in [dir / 'labels']: | |
| p.mkdir(parents=True, exist_ok=True) # make dir | |
| return dir | |
| def coco91_to_coco80_class(): # converts 80-index (val2014) to 91-index (paper) | |
| # https://tech.amikelive.com/node-718/what-object-categories-labels-are-in-coco-dataset/ | |
| x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, None, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, None, 24, 25, None, | |
| None, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, None, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, | |
| 51, 52, 53, 54, 55, 56, 57, 58, 59, None, 60, None, None, 61, None, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, | |
| None, 73, 74, 75, 76, 77, 78, 79, None] | |
| return x | |
| def is_ascii(s=""): | |
| # Is string composed of all ASCII (no UTF) characters? (note str().isascii() introduced in python 3.7) | |
| s = str(s) # convert list, tuple, None, etc. to str | |
| return len(s.encode().decode("ascii", "ignore")) == len(s) | |
| def is_chinese(s="人工智能"): | |
| # Is string composed of any Chinese characters? | |
| return re.search("[\u4e00-\u9fff]", s) | |
| def letterbox( | |
| im, | |
| new_shape=(640, 640), | |
| color=(114, 114, 114), | |
| auto=True, | |
| 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): | |
| new_shape = (new_shape, new_shape) | |
| # Scale ratio (new / old) | |
| r = min(new_shape[0] / shape[0], new_shape[1] / shape[1]) | |
| if not scaleup: # only scale down, do not scale up (for better val mAP) | |
| r = min(r, 1.0) | |
| # Compute padding | |
| ratio = r, r # width, height ratios | |
| new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) | |
| dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding | |
| if auto: # minimum rectangle | |
| dw, dh = np.mod(dw, stride), np.mod(dh, stride) # wh padding | |
| elif scaleFill: # stretch | |
| dw, dh = 0.0, 0.0 | |
| new_unpad = (new_shape[1], new_shape[0]) | |
| ratio = new_shape[1] / shape[1], new_shape[0] / shape[0] # width, height ratios | |
| dw /= 2 # divide padding into 2 sides | |
| dh /= 2 | |
| if shape[::-1] != new_unpad: # resize | |
| im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR) | |
| top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) | |
| left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) | |
| im = cv2.copyMakeBorder( | |
| im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color | |
| ) # add border | |
| return im, ratio, (dw, dh) | |
| def xyxy2xywh(x): | |
| # Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] where xy1=top-left, xy2=bottom-right | |
| y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x) | |
| y[:, 0] = (x[:, 0] + x[:, 2]) / 2 # x center | |
| y[:, 1] = (x[:, 1] + x[:, 3]) / 2 # y center | |
| y[:, 2] = x[:, 2] - x[:, 0] # width | |
| y[:, 3] = x[:, 3] - x[:, 1] # height | |
| return y | |
| def xywh2xyxy(x): | |
| # Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right | |
| y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x) | |
| y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left x | |
| y[:, 1] = x[:, 1] - x[:, 3] / 2 # top left y | |
| y[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right x | |
| y[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right y | |
| return y | |
| def non_max_suppression( | |
| prediction, | |
| conf_thres=0.25, | |
| iou_thres=0.45, | |
| classes=None, | |
| agnostic=False, | |
| multi_label=False, | |
| labels=(), | |
| max_det=300, | |
| ): | |
| """Runs Non-Maximum Suppression (NMS) on inference results | |
| Returns: | |
| list of detections, on (n,6) tensor per image [xyxy, conf, cls] | |
| """ | |
| nc = prediction.shape[2] - 5 # number of classes | |
| xc = prediction[..., 4] > conf_thres # candidates | |
| # Checks | |
| assert ( | |
| 0 <= conf_thres <= 1 | |
| ), f"Invalid Confidence threshold {conf_thres}, valid values are between 0.0 and 1.0" | |
| assert ( | |
| 0 <= iou_thres <= 1 | |
| ), f"Invalid IoU {iou_thres}, valid values are between 0.0 and 1.0" | |
| # Settings | |
| min_wh, max_wh = 2, 4096 # (pixels) minimum and maximum box width and height | |
| max_nms = 30000 # maximum number of boxes into torchvision.ops.nms() | |
| time_limit = 10.0 # seconds to quit after | |
| redundant = True # require redundant detections | |
| multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img) | |
| merge = False # use merge-NMS | |
| t = time.time() | |
| output = [torch.zeros((0, 6), device=prediction.device)] * prediction.shape[0] | |
| for xi, x in enumerate(prediction): # image index, image inference | |
| # Apply constraints | |
| # x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0 # width-height | |
| x = x[xc[xi]] # confidence | |
| # Cat apriori labels if autolabelling | |
| if labels and len(labels[xi]): | |
| l = labels[xi] | |
| v = torch.zeros((len(l), nc + 5), device=x.device) | |
| v[:, :4] = l[:, 1:5] # box | |
| v[:, 4] = 1.0 # conf | |
| v[range(len(l)), l[:, 0].long() + 5] = 1.0 # cls | |
| x = torch.cat((x, v), 0) | |
| # If none remain process next image | |
| if not x.shape[0]: | |
| continue | |
| # Compute conf | |
| x[:, 5:] *= x[:, 4:5] # conf = obj_conf * cls_conf | |
| # Box (center x, center y, width, height) to (x1, y1, x2, y2) | |
| box = xywh2xyxy(x[:, :4]) | |
| # Detections matrix nx6 (xyxy, conf, cls) | |
| if multi_label: | |
| i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T | |
| x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1) | |
| else: # best class only | |
| conf, j = x[:, 5:].max(1, keepdim=True) | |
| x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres] | |
| # Filter by class | |
| if classes is not None: | |
| x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)] | |
| # Apply finite constraint | |
| # if not torch.isfinite(x).all(): | |
| # x = x[torch.isfinite(x).all(1)] | |
| # Check shape | |
| n = x.shape[0] # number of boxes | |
| if not n: # no boxes | |
| continue | |
| elif n > max_nms: # excess boxes | |
| x = x[x[:, 4].argsort(descending=True)[:max_nms]] # sort by confidence | |
| # Batched NMS | |
| c = x[:, 5:6] * (0 if agnostic else max_wh) # classes | |
| boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores | |
| i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS | |
| if i.shape[0] > max_det: # limit detections | |
| i = i[:max_det] | |
| if merge and (1 < n < 3e3): # Merge NMS (boxes merged using weighted mean) | |
| # update boxes as boxes(i,4) = weights(i,n) * boxes(n,4) | |
| iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix | |
| weights = iou * scores[None] # box weights | |
| x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum( | |
| 1, keepdim=True | |
| ) # merged boxes | |
| if redundant: | |
| i = i[iou.sum(1) > 1] # require redundancy | |
| output[xi] = x[i] | |
| if (time.time() - t) > time_limit: | |
| print(f"WARNING: NMS time limit {time_limit}s exceeded") | |
| break # time limit exceeded | |
| return output | |
| def clip_coords(boxes, shape): | |
| # Clip bounding xyxy bounding boxes to image shape (height, width) | |
| if isinstance(boxes, torch.Tensor): # faster individually | |
| boxes[:, 0].clamp_(0, shape[1]) # x1 | |
| boxes[:, 1].clamp_(0, shape[0]) # y1 | |
| boxes[:, 2].clamp_(0, shape[1]) # x2 | |
| boxes[:, 3].clamp_(0, shape[0]) # y2 | |
| else: # np.array (faster grouped) | |
| boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, shape[1]) # x1, x2 | |
| boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, shape[0]) # y1, y2 | |
| def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None): | |
| # Rescale coords (xyxy) from img1_shape to img0_shape | |
| if ratio_pad is None: # calculate from img0_shape | |
| gain = min( | |
| img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1] | |
| ) # gain = old / new | |
| pad = (img1_shape[1] - img0_shape[1] * gain) / 2, ( | |
| img1_shape[0] - img0_shape[0] * gain | |
| ) / 2 # wh padding | |
| else: | |
| gain = ratio_pad[0][0] | |
| pad = ratio_pad[1] | |
| coords[:, [0, 2]] -= pad[0] # x padding | |
| coords[:, [1, 3]] -= pad[1] # y padding | |
| coords[:, :4] /= gain | |
| clip_coords(coords, img0_shape) | |
| return coords | |
| class Annotator: | |
| # YOLOv5 Annotator for train/val mosaics and jpgs and detect/hub inference annotations | |
| def __init__( | |
| self, | |
| im, | |
| line_width=None, | |
| font_size=None, | |
| font="Arial.ttf", | |
| pil=False, | |
| example="abc", | |
| ): | |
| assert ( | |
| im.data.contiguous | |
| ), "Image not contiguous. Apply np.ascontiguousarray(im) to Annotator() input images." | |
| self.pil = pil or not is_ascii(example) or is_chinese(example) | |
| self.im = im | |
| self.lw = line_width or max(round(sum(im.shape) / 2 * 0.003), 2) # line width | |
| def box_label( | |
| self, box, label="", color=(128, 128, 128), txt_color=(255, 255, 255) | |
| ): | |
| # Add one xyxy box to image with label | |
| p1, p2 = (int(box[0]), int(box[1])), (int(box[2]), int(box[3])) | |
| cv2.rectangle( | |
| self.im, p1, p2, color, thickness=self.lw, lineType=cv2.LINE_AA | |
| ) | |
| if label: | |
| tf = max(self.lw - 1, 1) # font thickness | |
| w, h = cv2.getTextSize(label, 0, fontScale=self.lw / 3, thickness=tf)[ | |
| 0 | |
| ] # text width, height | |
| outside = p1[1] - h - 3 >= 0 # label fits outside box | |
| p2 = p1[0] + w, p1[1] - h - 3 if outside else p1[1] + h + 3 | |
| cv2.rectangle(self.im, p1, p2, color, -1, cv2.LINE_AA) # filled | |
| cv2.putText( | |
| self.im, | |
| label, | |
| (p1[0], p1[1] - 2 if outside else p1[1] + h + 2), | |
| 0, | |
| self.lw / 3, | |
| txt_color, | |
| thickness=tf, | |
| lineType=cv2.LINE_AA, | |
| ) | |
| def rectangle(self, xy, fill=None, outline=None, width=1): | |
| # Add rectangle to image (PIL-only) | |
| self.draw.rectangle(xy, fill, outline, width) | |
| def result(self): | |
| # Return annotated image as array | |
| return np.asarray(self.im) | |
| class Colors: | |
| # Ultralytics color palette https://ultralytics.com/ | |
| def __init__(self): | |
| # hex = matplotlib.colors.TABLEAU_COLORS.values() | |
| hex = ( | |
| "FF3838", | |
| "FF9D97", | |
| "FF701F", | |
| "FFB21D", | |
| "CFD231", | |
| "48F90A", | |
| "92CC17", | |
| "3DDB86", | |
| "1A9334", | |
| "00D4BB", | |
| "2C99A8", | |
| "00C2FF", | |
| "344593", | |
| "6473FF", | |
| "0018EC", | |
| "8438FF", | |
| "520085", | |
| "CB38FF", | |
| "FF95C8", | |
| "FF37C7", | |
| ) | |
| self.palette = [self.hex2rgb("#" + c) for c in hex] | |
| self.n = len(self.palette) | |
| def __call__(self, i, bgr=False): | |
| c = self.palette[int(i) % self.n] | |
| return (c[2], c[1], c[0]) if bgr else c | |
| def hex2rgb(h): # rgb order (PIL) | |
| return tuple(int(h[1 + i : 1 + i + 2], 16) for i in (0, 2, 4)) | |
| def create_dataloader(path, imgsz, batch_size, stride, single_cls=False, hyp=None, augment=False, cache=False, pad=0.0, | |
| rect=False, rank=-1, workers=8, image_weights=False, quad=False, prefix=''): | |
| dataset = LoadImagesAndLabels(path, imgsz, batch_size, | |
| augment=augment, # augment images | |
| hyp=hyp, # augmentation hyperparameters | |
| rect=rect, # rectangular training | |
| cache_images=cache, | |
| single_cls=single_cls, | |
| stride=int(stride), | |
| pad=pad, | |
| image_weights=image_weights, | |
| prefix=prefix) | |
| batch_size = min(batch_size, len(dataset)) | |
| nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, workers]) # number of workers | |
| sampler = torch.utils.data.distributed.DistributedSampler(dataset) if rank != -1 else None | |
| loader = torch.utils.data.DataLoader if image_weights else InfiniteDataLoader | |
| # Use torch.utils.data.DataLoader() if dataset.properties will update during training else InfiniteDataLoader() | |
| dataloader = loader(dataset, | |
| batch_size=batch_size, | |
| num_workers=nw, | |
| sampler=sampler, | |
| pin_memory=True, | |
| collate_fn=LoadImagesAndLabels.collate_fn) | |
| return dataloader, dataset | |
| class LoadImagesAndLabels(Dataset): | |
| # YOLOv5 train_loader/val_loader, loads images and labels for training and validation | |
| cache_version = 0.5 # dataset labels *.cache version | |
| def __init__(self, path, img_size=640, batch_size=16, augment=False, hyp=None, rect=False, image_weights=False, | |
| cache_images=False, single_cls=False, stride=32, pad=0.0, prefix=''): | |
| self.img_size = img_size | |
| self.augment = augment | |
| self.hyp = hyp | |
| self.image_weights = image_weights | |
| self.rect = False if image_weights else rect | |
| self.mosaic = False # load 4 images at a time into a mosaic (only during training) | |
| self.mosaic_border = [-img_size // 2, -img_size // 2] | |
| self.stride = stride | |
| self.path = path | |
| self.albumentations = None | |
| f = [] # image files | |
| for p in path if isinstance(path, list) else [path]: | |
| p = Path(p) # os-agnostic | |
| if p.is_dir(): # dir | |
| f += glob.glob(str(p / '**' / '*.*'), recursive=True) | |
| # f = list(p.rglob('**/*.*')) # pathlib | |
| elif p.is_file(): # file | |
| with open(p, 'r') as t: | |
| t = t.read().strip().splitlines() | |
| parent = str(p.parent) + os.sep | |
| f += [x.replace('./', parent) if x.startswith('./') else x for x in t] # local to global path | |
| # f += [p.parent / x.lstrip(os.sep) for x in t] # local to global path (pathlib) | |
| else: | |
| raise Exception(f'{prefix}{p} does not exist') | |
| self.img_files = sorted([x.replace('/', os.sep) for x in f if x.split('.')[-1].lower() in IMG_FORMATS]) | |
| # self.img_files = sorted([x for x in f if x.suffix[1:].lower() in img_formats]) # pathlib | |
| assert self.img_files, f'{prefix}No images found' | |
| # Check cache | |
| self.label_files = img2label_paths(self.img_files) # labels | |
| cache_path = (p if p.is_file() else Path(self.label_files[0]).parent).with_suffix('.cache') | |
| try: | |
| cache, exists = np.load(cache_path, allow_pickle=True).item(), True # load dict | |
| assert cache['version'] == self.cache_version # same version | |
| assert cache['hash'] == get_hash(self.label_files + self.img_files) # same hash | |
| except: | |
| cache, exists = self.cache_labels(cache_path, prefix), False # cache | |
| # Display cache | |
| nf, nm, ne, nc, n = cache.pop('results') # found, missing, empty, corrupted, total | |
| if exists: | |
| d = f"Scanning '{cache_path}' images and labels... {nf} found, {nm} missing, {ne} empty, {nc} corrupted" | |
| tqdm(None, desc=prefix + d, total=n, initial=n) # display cache results | |
| if cache['msgs']: | |
| logging.info('\n'.join(cache['msgs'])) # display warnings | |
| # Read cache | |
| [cache.pop(k) for k in ('hash', 'version', 'msgs')] # remove items | |
| labels, shapes, self.segments = zip(*cache.values()) | |
| self.labels = list(labels) | |
| self.shapes = np.array(shapes, dtype=np.float64) | |
| self.img_files = list(cache.keys()) # update | |
| self.label_files = img2label_paths(cache.keys()) # update | |
| if single_cls: | |
| for x in self.labels: | |
| x[:, 0] = 0 | |
| n = len(shapes) # number of images | |
| bi = np.floor(np.arange(n) / batch_size).astype(int) # batch index | |
| nb = bi[-1] + 1 # number of batches | |
| self.batch = bi # batch index of image | |
| self.n = n | |
| self.indices = range(n) | |
| # Rectangular Training | |
| if self.rect: | |
| # Sort by aspect ratio | |
| s = self.shapes # wh | |
| ar = s[:, 1] / s[:, 0] # aspect ratio | |
| irect = ar.argsort() | |
| self.img_files = [self.img_files[i] for i in irect] | |
| self.label_files = [self.label_files[i] for i in irect] | |
| self.labels = [self.labels[i] for i in irect] | |
| self.shapes = s[irect] # wh | |
| ar = ar[irect] | |
| # Set training image shapes | |
| shapes = [[1, 1]] * nb | |
| for i in range(nb): | |
| ari = ar[bi == i] | |
| mini, maxi = ari.min(), ari.max() | |
| if maxi < 1: | |
| shapes[i] = [maxi, 1] | |
| elif mini > 1: | |
| shapes[i] = [1, 1 / mini] | |
| self.batch_shapes = np.ceil(np.array(shapes) * img_size / stride + pad).astype(int) * stride | |
| # Cache images into memory for faster training (WARNING: large datasets may exceed system RAM) | |
| self.imgs, self.img_npy = [None] * n, [None] * n | |
| if cache_images: | |
| if cache_images == 'disk': | |
| self.im_cache_dir = Path(Path(self.img_files[0]).parent.as_posix() + '_npy') | |
| self.img_npy = [self.im_cache_dir / Path(f).with_suffix('.npy').name for f in self.img_files] | |
| self.im_cache_dir.mkdir(parents=True, exist_ok=True) | |
| gb = 0 # Gigabytes of cached images | |
| self.img_hw0, self.img_hw = [None] * n, [None] * n | |
| results = ThreadPool(NUM_THREADS).imap(lambda x: load_image(*x), zip(repeat(self), range(n))) | |
| pbar = tqdm(enumerate(results), total=n) | |
| for i, x in pbar: | |
| if cache_images == 'disk': | |
| if not self.img_npy[i].exists(): | |
| np.save(self.img_npy[i].as_posix(), x[0]) | |
| gb += self.img_npy[i].stat().st_size | |
| else: | |
| self.imgs[i], self.img_hw0[i], self.img_hw[i] = x # im, hw_orig, hw_resized = load_image(self, i) | |
| gb += self.imgs[i].nbytes | |
| pbar.desc = f'{prefix}Caching images ({gb / 1E9:.1f}GB {cache_images})' | |
| pbar.close() | |
| def cache_labels(self, path=Path('./labels.cache'), prefix=''): | |
| # Cache dataset labels, check images and read shapes | |
| x = {} # dict | |
| nm, nf, ne, nc, msgs = 0, 0, 0, 0, [] # number missing, found, empty, corrupt, messages | |
| desc = f"{prefix}Scanning '{path.parent / path.stem}' images and labels..." | |
| with Pool(NUM_THREADS) as pool: | |
| pbar = tqdm(pool.imap(verify_image_label, zip(self.img_files, self.label_files, repeat(prefix))), desc=desc, total=len(self.img_files)) | |
| for im_file, l, shape, segments, nm_f, nf_f, ne_f, nc_f, msg in pbar: | |
| nm += nm_f | |
| nf += nf_f | |
| ne += ne_f | |
| nc += nc_f | |
| if im_file: | |
| x[im_file] = [l, shape, segments] | |
| if msg: | |
| msgs.append(msg) | |
| pbar.desc = f"{desc}{nf} found, {nm} missing, {ne} empty, {nc} corrupted" | |
| pbar.close() | |
| if msgs: | |
| logging.info('\n'.join(msgs)) | |
| x['hash'] = get_hash(self.label_files + self.img_files) | |
| x['results'] = nf, nm, ne, nc, len(self.img_files) | |
| x['msgs'] = msgs # warnings | |
| x['version'] = self.cache_version # cache version | |
| try: | |
| np.save(path, x) # save cache for next time | |
| path.with_suffix('.cache.npy').rename(path) # remove .npy suffix | |
| logging.info(f'{prefix}New cache created: {path}') | |
| except Exception as e: | |
| logging.info(f'{prefix}WARNING: Cache directory {path.parent} is not writeable: {e}') # path not writeable | |
| return x | |
| def __len__(self): | |
| return len(self.img_files) | |
| # def __iter__(self): | |
| # self.count = -1 | |
| # print('ran dataset iter') | |
| # #self.shuffled_vector = np.random.permutation(self.nF) if self.augment else np.arange(self.nF) | |
| # return self | |
| def __getitem__(self, index): | |
| index = self.indices[index] # linear, shuffled, or image_weights | |
| hyp = self.hyp | |
| mosaic = self.mosaic | |
| # Load image | |
| img, (h0, w0), (h, w) = load_image(self, index) | |
| # Letterbox | |
| shape = self.batch_shapes[self.batch[index]] if self.rect else self.img_size # final letterboxed shape | |
| img, ratio, pad = letterbox(img, shape, auto=False, scaleup=self.augment) | |
| shapes = (h0, w0), ((h / h0, w / w0), pad) # for COCO mAP rescaling | |
| labels = self.labels[index].copy() | |
| if labels.size: # normalized xywh to pixel xyxy format | |
| labels[:, 1:] = xywhn2xyxy(labels[:, 1:], ratio[0] * w, ratio[1] * h, padw=pad[0], padh=pad[1]) | |
| nl = len(labels) # number of labels | |
| if nl: | |
| labels[:, 1:5] = xyxy2xywhn(labels[:, 1:5], w=img.shape[1], h=img.shape[0], clip=True, eps=1E-3) | |
| labels_out = torch.zeros((nl, 6)) | |
| if nl: | |
| labels_out[:, 1:] = torch.from_numpy(labels) | |
| # Convert | |
| img = img.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB | |
| img = np.ascontiguousarray(img) | |
| return torch.from_numpy(img), labels_out, self.img_files[index], shapes | |
| def collate_fn(batch): | |
| img, label, path, shapes = zip(*batch) # transposed | |
| for i, l in enumerate(label): | |
| l[:, 0] = i # add target image index for build_targets() | |
| return torch.stack(img, 0), torch.cat(label, 0), path, shapes | |
| def coco80_to_coco91_class(): # converts 80-index (val2014) to 91-index (paper) | |
| # https://tech.amikelive.com/node-718/what-object-categories-labels-are-in-coco-dataset/ | |
| # a = np.loadtxt('data/coco.names', dtype='str', delimiter='\n') | |
| # b = np.loadtxt('data/coco_paper.names', dtype='str', delimiter='\n') | |
| # x1 = [list(a[i] == b).index(True) + 1 for i in range(80)] # darknet to coco | |
| # x2 = [list(b[i] == a).index(True) if any(b[i] == a) else None for i in range(91)] # coco to darknet | |
| x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 28, 31, 32, 33, 34, | |
| 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, | |
| 64, 65, 67, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90] | |
| return x | |
| def check_dataset(data, autodownload=True): | |
| # Download and/or unzip dataset if not found locally | |
| # Usage: https://github.com/ultralytics/yolov5/releases/download/v1.0/coco128_with_yaml.zip | |
| # Download (optional) | |
| extract_dir = '' | |
| # Read yaml (optional) | |
| if isinstance(data, (str, Path)): | |
| with open(data, errors='ignore') as f: | |
| data = yaml.safe_load(f) # dictionary | |
| # Parse yaml | |
| path = extract_dir or Path(data.get('path') or '') # optional 'path' default to '.' | |
| for k in 'train', 'val', 'test': | |
| if data.get(k): # prepend path | |
| data[k] = str(path / data[k]) if isinstance(data[k], str) else [str(path / x) for x in data[k]] | |
| assert 'nc' in data, "Dataset 'nc' key missing." | |
| if 'names' not in data: | |
| data['names'] = [f'class{i}' for i in range(data['nc'])] # assign class names if missing | |
| train, val, test, s = [data.get(x) for x in ('train', 'val', 'test', 'download')] | |
| if val: | |
| val = [Path(x).resolve() for x in (val if isinstance(val, list) else [val])] # val path | |
| if not all(x.exists() for x in val): | |
| print('\nWARNING: Dataset not found, nonexistent paths: %s' % [str(x) for x in val if not x.exists()]) | |
| return data # dictionary | |
| def box_iou(box1, box2): | |
| # https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py | |
| """ | |
| Return intersection-over-union (Jaccard index) of boxes. | |
| Both sets of boxes are expected to be in (x1, y1, x2, y2) format. | |
| Arguments: | |
| box1 (Tensor[N, 4]) | |
| box2 (Tensor[M, 4]) | |
| Returns: | |
| iou (Tensor[N, M]): the NxM matrix containing the pairwise | |
| IoU values for every element in boxes1 and boxes2 | |
| """ | |
| def box_area(box): | |
| # box = 4xn | |
| return (box[2] - box[0]) * (box[3] - box[1]) | |
| area1 = box_area(box1.T) | |
| area2 = box_area(box2.T) | |
| # inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2) | |
| inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2) | |
| return inter / (area1[:, None] + area2 - inter) # iou = inter / (area1 + area2 - inter) | |
| def increment_path(path, exist_ok=False, sep='', mkdir=False): | |
| # Increment file or directory path, i.e. runs/exp --> runs/exp{sep}2, runs/exp{sep}3, ... etc. | |
| path = Path(path) # os-agnostic | |
| if path.exists() and not exist_ok: | |
| suffix = path.suffix | |
| path = path.with_suffix('') | |
| dirs = glob.glob(f"{path}{sep}*") # similar paths | |
| matches = [re.search(rf"%s{sep}(\d+)" % path.stem, d) for d in dirs] | |
| i = [int(m.groups()[0]) for m in matches if m] # indices | |
| n = max(i) + 1 if i else 2 # increment number | |
| path = Path(f"{path}{sep}{n}{suffix}") # update path | |
| dir = path if path.suffix == '' else path.parent # directory | |
| if not dir.exists() and mkdir: | |
| dir.mkdir(parents=True, exist_ok=True) # make directory | |
| return path | |
| def colorstr(*input): | |
| # Colors a string https://en.wikipedia.org/wiki/ANSI_escape_code, i.e. colorstr('blue', 'hello world') | |
| *args, string = input if len(input) > 1 else ('blue', 'bold', input[0]) # color arguments, string | |
| colors = {'black': '\033[30m', # basic colors | |
| 'red': '\033[31m', | |
| 'green': '\033[32m', | |
| 'yellow': '\033[33m', | |
| 'blue': '\033[34m', | |
| 'magenta': '\033[35m', | |
| 'cyan': '\033[36m', | |
| 'white': '\033[37m', | |
| 'bright_black': '\033[90m', # bright colors | |
| 'bright_red': '\033[91m', | |
| 'bright_green': '\033[92m', | |
| 'bright_yellow': '\033[93m', | |
| 'bright_blue': '\033[94m', | |
| 'bright_magenta': '\033[95m', | |
| 'bright_cyan': '\033[96m', | |
| 'bright_white': '\033[97m', | |
| 'end': '\033[0m', # misc | |
| 'bold': '\033[1m', | |
| 'underline': '\033[4m'} | |
| return ''.join(colors[x] for x in args) + f'{string}' + colors['end'] | |
| def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names=()): | |
| """ Compute the average precision, given the recall and precision curves. | |
| Source: https://github.com/rafaelpadilla/Object-Detection-Metrics. | |
| # Arguments | |
| tp: True positives (nparray, nx1 or nx10). | |
| conf: Objectness value from 0-1 (nparray). | |
| pred_cls: Predicted object classes (nparray). | |
| target_cls: True object classes (nparray). | |
| plot: Plot precision-recall curve at mAP@0.5 | |
| save_dir: Plot save directory | |
| # Returns | |
| The average precision as computed in py-faster-rcnn. | |
| """ | |
| # Sort by objectness | |
| i = np.argsort(-conf) | |
| tp, conf, pred_cls = tp[i], conf[i], pred_cls[i] | |
| # Find unique classes | |
| unique_classes = np.unique(target_cls) | |
| nc = unique_classes.shape[0] # number of classes, number of detections | |
| # Create Precision-Recall curve and compute AP for each class | |
| px, py = np.linspace(0, 1, 1000), [] # for plotting | |
| ap, p, r = np.zeros((nc, tp.shape[1])), np.zeros((nc, 1000)), np.zeros((nc, 1000)) | |
| for ci, c in enumerate(unique_classes): | |
| i = pred_cls == c | |
| n_l = (target_cls == c).sum() # number of labels | |
| n_p = i.sum() # number of predictions | |
| if n_p == 0 or n_l == 0: | |
| continue | |
| else: | |
| # Accumulate FPs and TPs | |
| fpc = (1 - tp[i]).cumsum(0) | |
| tpc = tp[i].cumsum(0) | |
| # 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 | |
| # AP from recall-precision curve | |
| for j in range(tp.shape[1]): | |
| ap[ci, j], mpre, mrec = compute_ap(recall[:, j], precision[:, j]) | |
| if plot and j == 0: | |
| py.append(np.interp(px, mrec, mpre)) # precision at mAP@0.5 | |
| # Compute F1 (harmonic mean of precision and recall) | |
| f1 = 2 * p * r / (p + r + 1e-16) | |
| i = f1.mean(0).argmax() # max F1 index | |
| return p[:, i], r[:, i], ap, f1[:, i], unique_classes.astype('int32') | |
| def compute_ap(recall, precision): | |
| """ Compute the average precision, given the recall and precision curves | |
| # Arguments | |
| recall: The recall curve (list) | |
| precision: The precision curve (list) | |
| # Returns | |
| Average precision, precision curve, recall curve | |
| """ | |
| # Append sentinel values to beginning and end | |
| mrec = np.concatenate(([0.0], recall, [1.0])) | |
| mpre = np.concatenate(([1.0], precision, [0.0])) | |
| # Compute the precision envelope | |
| mpre = np.flip(np.maximum.accumulate(np.flip(mpre))) | |
| # Integrate area under curve | |
| method = 'interp' # methods: 'continuous', 'interp' | |
| if method == 'interp': | |
| x = np.linspace(0, 1, 101) # 101-point interp (COCO) | |
| ap = np.trapz(np.interp(x, mrec, mpre), x) # integrate | |
| else: # 'continuous' | |
| i = np.where(mrec[1:] != mrec[:-1])[0] # points where x axis (recall) changes | |
| ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) # area under curve | |
| return ap, mpre, mrec | |
| def output_to_target(output): | |
| # Convert model output to target format [batch_id, class_id, x, y, w, h, conf] | |
| targets = [] | |
| for i, o in enumerate(output): | |
| for *box, conf, cls in o.cpu().numpy(): | |
| targets.append([i, cls, *list(*xyxy2xywh(np.array(box)[None])), conf]) | |
| return np.array(targets) | |
| def check_yaml(file, suffix=('.yaml', '.yml')): | |
| # Search/download YAML file (if necessary) and return path, checking suffix | |
| return check_file(file, suffix) | |
| def check_file(file, suffix=''): | |
| # Search/download file (if necessary) and return path | |
| check_suffix(file, suffix) # optional | |
| file = str(file) # convert to str() | |
| return file | |
| def check_suffix(file='yolov5s.pt', suffix=('.pt',), msg=''): | |
| # Check file(s) for acceptable suffixes | |
| if file and suffix: | |
| if isinstance(suffix, str): | |
| suffix = [suffix] | |
| for f in file if isinstance(file, (list, tuple)) else [file]: | |
| assert Path(f).suffix.lower() in suffix, f"{msg}{f} acceptable suffix is {suffix}" | |
| def img2label_paths(img_paths): | |
| # Define label paths as a function of image paths | |
| sa, sb = os.sep + 'images' + os.sep, os.sep + 'labels' + os.sep # /images/, /labels/ substrings | |
| return [sb.join(x.rsplit(sa, 1)).rsplit('.', 1)[0] + '.txt' for x in img_paths] | |
| def exif_size(img): | |
| # Returns exif-corrected PIL size | |
| s = img.size # (width, height) | |
| try: | |
| rotation = dict(img._getexif().items())[orientation] | |
| if rotation == 6: # rotation 270 | |
| s = (s[1], s[0]) | |
| elif rotation == 8: # rotation 90 | |
| s = (s[1], s[0]) | |
| except: | |
| pass | |
| return s | |
| def verify_image_label(args): | |
| # Verify one image-label pair | |
| im_file, lb_file, prefix = args | |
| nm, nf, ne, nc, msg, segments = 0, 0, 0, 0, '', [] # number (missing, found, empty, corrupt), message, segments | |
| try: | |
| # verify images | |
| im = Image.open(im_file) | |
| im.verify() # PIL verify | |
| shape = exif_size(im) # image size | |
| assert (shape[0] > 9) & (shape[1] > 9), f'image size {shape} <10 pixels' | |
| assert im.format.lower() in IMG_FORMATS, f'invalid image format {im.format}' | |
| if im.format.lower() in ('jpg', 'jpeg'): | |
| with open(im_file, 'rb') as f: | |
| f.seek(-2, 2) | |
| if f.read() != b'\xff\xd9': # corrupt JPEG | |
| Image.open(im_file).save(im_file, format='JPEG', subsampling=0, quality=100) # re-save image | |
| msg = f'{prefix}WARNING: corrupt JPEG restored and saved {im_file}' | |
| # verify labels | |
| if os.path.isfile(lb_file): | |
| nf = 1 # label found | |
| with open(lb_file, 'r') as f: | |
| l = [x.split() for x in f.read().strip().splitlines() if len(x)] | |
| if any([len(x) > 8 for x in l]): # is segment | |
| classes = np.array([x[0] for x in l], dtype=np.float32) | |
| segments = [np.array(x[1:], dtype=np.float32).reshape(-1, 2) for x in l] # (cls, xy1...) | |
| l = np.concatenate((classes.reshape(-1, 1), segments2boxes(segments)), 1) # (cls, xywh) | |
| l = np.array(l, dtype=np.float32) | |
| if len(l): | |
| assert l.shape[1] == 5, 'labels require 5 columns each' | |
| assert (l >= 0).all(), 'negative labels' | |
| assert (l[:, 1:] <= 1).all(), 'non-normalized or out of bounds coordinate labels' | |
| assert np.unique(l, axis=0).shape[0] == l.shape[0], 'duplicate labels' | |
| else: | |
| ne = 1 # label empty | |
| l = np.zeros((0, 5), dtype=np.float32) | |
| else: | |
| nm = 1 # label missing | |
| l = np.zeros((0, 5), dtype=np.float32) | |
| return im_file, l, shape, segments, nm, nf, ne, nc, msg | |
| except Exception as e: | |
| nc = 1 | |
| msg = f'{prefix}WARNING: Ignoring corrupted image and/or label {im_file}: {e}' | |
| return [None, None, None, None, nm, nf, ne, nc, msg] | |
| def segments2boxes(segments): | |
| # Convert segment labels to box labels, i.e. (cls, xy1, xy2, ...) to (cls, xywh) | |
| boxes = [] | |
| for s in segments: | |
| x, y = s.T # segment xy | |
| boxes.append([x.min(), y.min(), x.max(), y.max()]) # cls, xyxy | |
| return xyxy2xywh(np.array(boxes)) # cls, xywh | |
| def get_hash(paths): | |
| # Returns a single hash value of a list of paths (files or dirs) | |
| size = sum(os.path.getsize(p) for p in paths if os.path.exists(p)) # sizes | |
| h = hashlib.md5(str(size).encode()) # hash sizes | |
| h.update(''.join(paths).encode()) # hash paths | |
| return h.hexdigest() # return hash | |
| class InfiniteDataLoader(torch.utils.data.dataloader.DataLoader): | |
| """ Dataloader that reuses workers | |
| Uses same syntax as vanilla DataLoader | |
| """ | |
| def __init__(self, *args, **kwargs): | |
| super().__init__(*args, **kwargs) | |
| object.__setattr__(self, 'batch_sampler', _RepeatSampler(self.batch_sampler)) | |
| self.iterator = super().__iter__() | |
| def __len__(self): | |
| return len(self.batch_sampler.sampler) | |
| def __iter__(self): | |
| for i in range(len(self)): | |
| yield next(self.iterator) | |
| class _RepeatSampler(object): | |
| """ Sampler that repeats forever | |
| Args: | |
| sampler (Sampler) | |
| """ | |
| def __init__(self, sampler): | |
| self.sampler = sampler | |
| def __iter__(self): | |
| while True: | |
| yield from iter(self.sampler) | |
| def load_image(self, i): | |
| # loads 1 image from dataset index 'i', returns im, original hw, resized hw | |
| im = self.imgs[i] | |
| if im is None: # not cached in ram | |
| npy = self.img_npy[i] | |
| if npy and npy.exists(): # load npy | |
| im = np.load(npy) | |
| else: # read image | |
| path = self.img_files[i] | |
| im = cv2.imread(path) # BGR | |
| 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) | |
| 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 | |
| def xywhn2xyxy(x, w=640, h=640, padw=0, padh=0): | |
| # Convert nx4 boxes from [x, y, w, h] normalized to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right | |
| y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x) | |
| y[:, 0] = w * (x[:, 0] - x[:, 2] / 2) + padw # top left x | |
| y[:, 1] = h * (x[:, 1] - x[:, 3] / 2) + padh # top left y | |
| y[:, 2] = w * (x[:, 0] + x[:, 2] / 2) + padw # bottom right x | |
| y[:, 3] = h * (x[:, 1] + x[:, 3] / 2) + padh # bottom right y | |
| return y | |
| def xyxy2xywhn(x, w=640, h=640, clip=False, eps=0.0): | |
| # Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] normalized where xy1=top-left, xy2=bottom-right | |
| if clip: | |
| clip_coords(x, (h - eps, w - eps)) # warning: inplace clip | |
| y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x) | |
| y[:, 0] = ((x[:, 0] + x[:, 2]) / 2) / w # x center | |
| y[:, 1] = ((x[:, 1] + x[:, 3]) / 2) / h # y center | |
| y[:, 2] = (x[:, 2] - x[:, 0]) / w # width | |
| y[:, 3] = (x[:, 3] - x[:, 1]) / h # height | |
| return y | |
| def post_process(x): | |
| grid = np.load("./grid.npy", allow_pickle=True) | |
| anchor_grid = np.load("./anchor_grid.npy", allow_pickle=True) | |
| x = list(x) | |
| z = [] # inference output | |
| stride = [8, 16, 32] | |
| for i in range(3): | |
| bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85) | |
| x[i] = ( | |
| torch.tensor(x[i]) | |
| .view(bs, 3, 85, ny, nx) | |
| .permute(0, 1, 3, 4, 2) | |
| .contiguous() | |
| ) | |
| y = x[i].sigmoid() | |
| xy = (y[..., 0:2] * 2.0 - 0.5 + grid[i]) * stride[i] | |
| wh = (y[..., 2:4] * 2) ** 2 * anchor_grid[i] | |
| y = torch.cat((xy, wh, y[..., 4:]), -1) | |
| z.append(y.view(bs, -1, 85)) | |
| return (torch.cat(z, 1), x) |