From 6ad389bb6fa6afe164f6c8e6156c8a37b0556611 Mon Sep 17 00:00:00 2001 From: Laughing-q <1185102784@qq.com> Date: Wed, 20 Jul 2022 18:00:11 +0800 Subject: [PATCH] add val_instseg.py&&remove useless code --- evaluator.py | 4 +- seg_dataloaders.py | 199 ++----------- train_instseg.py | 2 +- utils/boxes.py | 298 -------------------- utils/seg_plots.py | 689 --------------------------------------------- val_instseg.py | 85 ++++++ 6 files changed, 105 insertions(+), 1172 deletions(-) delete mode 100644 utils/boxes.py delete mode 100644 utils/seg_plots.py create mode 100644 val_instseg.py diff --git a/evaluator.py b/evaluator.py index 3e5e3ded21f0..27533c3048f1 100644 --- a/evaluator.py +++ b/evaluator.py @@ -270,7 +270,7 @@ def before_infer(self, weights, batch_size, imgsz, save_txt, task="val"): # Load model check_suffix(weights, ".pt") - model = attempt_load(weights, map_location=self.device) # load FP32 model + model = attempt_load(weights, device=self.device) # load FP32 model gs = max(int(model.stride.max()), 32) # grid size (max stride) imgsz = check_img_size(imgsz, s=gs) # check image size @@ -612,4 +612,4 @@ def get_maps(self, nc): @property def ap_class_index(self): # boxes and masks have the same ap_class_index - return self.metric_box.ap_class_index \ No newline at end of file + return self.metric_box.ap_class_index diff --git a/seg_dataloaders.py b/seg_dataloaders.py index ac6da36fab09..4d74bb00c1a9 100644 --- a/seg_dataloaders.py +++ b/seg_dataloaders.py @@ -8,11 +8,14 @@ import json import logging import time +import numpy as np from functools import wraps from itertools import repeat from multiprocessing.pool import ThreadPool, Pool from pathlib import Path from zipfile import ZipFile +from PIL import Image +from tqdm import tqdm import torch.nn.functional as F import yaml @@ -20,8 +23,6 @@ from torch.utils.data import distributed from torch.utils.data.sampler import BatchSampler as torchBatchSampler from torch.utils.data.sampler import RandomSampler -from torch.utils.data.sampler import Sampler -from tqdm import tqdm from seg_augmentations import (Albumentations, augment_hsv, copy_paste, letterbox, mixup, random_perspective, ) from utils.general import colorstr, check_dataset, check_yaml, xywhn2xyxy, xyxy2xywhn, xyn2xy @@ -60,8 +61,8 @@ def __iter__(self): 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="", shuffle=False, neg_dir="", - bg_dir="", area_thr=0.2, mask_head=False, mask_downsample_ratio=1, overlap_mask=False): + rect=False, rank=-1, workers=8, image_weights=False, quad=False, prefix="", shuffle=False, + area_thr=0.2, mask_head=False, mask_downsample_ratio=1, overlap_mask=False): if rect and shuffle: print("WARNING: --rect is incompatible with DataLoader shuffle, setting shuffle=False") shuffle = False @@ -72,7 +73,7 @@ def create_dataloader(path, imgsz, batch_size, stride, single_cls=False, hyp=Non 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, neg_dir=neg_dir, bg_dir=bg_dir, area_thr=area_thr, ) + prefix=prefix, area_thr=area_thr, ) if mask_head: dataset.downsample_ratio = mask_downsample_ratio dataset.overlap = overlap_mask @@ -88,10 +89,6 @@ def create_dataloader(path, imgsz, batch_size, stride, single_cls=False, hyp=Non # batch-size and batch-sampler is exclusion batch_sampler=batch_sampler, pin_memory=True, collate_fn=data_load.collate_fn4 if quad else data_load.collate_fn, - # Make sure each process has different random seed, especially for 'fork' method. - # Check https://github.com/pytorch/pytorch/issues/63311 for more details. - # but this will make init_seed() not work. - # worker_init_fn=worker_init_reset_seed, ) return dataloader, dataset @@ -141,7 +138,7 @@ class LoadImagesAndLabels(Dataset): cache_version = 0.6 # 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="", neg_dir="", bg_dir="", area_thr=0.2, ): + cache_images=False, single_cls=False, stride=32, pad=0.0, prefix="", area_thr=0.2, ): super().__init__(augment=augment) self.img_size = img_size self.hyp = hyp @@ -154,7 +151,6 @@ def __init__(self, path, img_size=640, batch_size=16, augment=False, hyp=None, r self.albumentations = Albumentations() if augment else None # additional feature - self.img_neg_files, self.img_bg_files = self.get_neg_and_bg(neg_dir, bg_dir) self.area_thr = area_thr p = Path(path) # os-agnostic @@ -235,20 +231,6 @@ def get_img_files(self, p, prefix): raise Exception(f"{prefix}Error loading data from {str(p)}: {e}\nSee {HELP_URL}") return img_files - def get_neg_and_bg(self, neg_dir, bg_dir): - """Get negative pictures and background pictures.""" - img_neg_files, img_bg_files = [], [] - if os.path.isdir(neg_dir): - img_neg_files = [os.path.join(neg_dir, i) for i in os.listdir(neg_dir)] - logging.info(colorstr( - "Negative dir: ") + f"'{neg_dir}', using {len(img_neg_files)} pictures from the dir as negative samples during training") - - if os.path.isdir(bg_dir): - img_bg_files = [os.path.join(bg_dir, i) for i in os.listdir(bg_dir)] - logging.info(colorstr( - "Background dir: ") + f"{bg_dir}, using {len(img_bg_files)} pictures from the dir as background during training") - return img_neg_files, img_bg_files - def load_cache(self, cache_path, prefix): """Load labels from *.cache file.""" try: @@ -454,11 +436,11 @@ def collate_fn4(batch): class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing 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, prefix="", neg_dir="", bg_dir="", area_thr=0.2, + cache_images=False, single_cls=False, stride=32, pad=0, prefix="", area_thr=0.2, downsample_ratio=1, overlap=False, ): super().__init__(path, img_size, batch_size, augment, hyp, rect, image_weights, cache_images, single_cls, - stride, pad, prefix, neg_dir, bg_dir, area_thr, ) + stride, pad, prefix, area_thr, ) self.downsample_ratio = downsample_ratio self.overlap = overlap @@ -590,66 +572,23 @@ def load_image(self, i): return (self.imgs[i], self.img_hw0[i], self.img_hw[i],) # im, hw_original, hw_resized -def load_neg_image(self, index): - path = self.img_neg_files[index] - img = cv2.imread(path) # BGR - assert img is not None, "Image Not Found " + path - h0, w0 = img.shape[:2] # orig hw - r = self.img_size / max(h0, w0) # resize image to img_size - if r != 1: # always resize down, only resize up if training with augmentation - interp = cv2.INTER_AREA if r < 1 and not self.augment else cv2.INTER_LINEAR - img = cv2.resize(img, (int(w0 * r), int(h0 * r)), interpolation=interp) - return img, (h0, w0), img.shape[:2] # img, hw_original, hw_resized - - -def load_bg_image(self, index): - path = self.img_files[index] - bg_path = self.img_bg_files[np.random.randint(0, len(self.img_bg_files))] - img, coord, _, (w, h) = paste1(path, bg_path, bg_size=self.img_size, fg_scale=random.uniform(1.5, 5)) - label = self.labels[index] - label[:, 1] = (label[:, 1] * w + coord[0]) / img.shape[1] - label[:, 2] = (label[:, 2] * h + coord[1]) / img.shape[0] - label[:, 3] = label[:, 3] * w / img.shape[1] - label[:, 4] = label[:, 4] * h / img.shape[0] - - assert img is not None, "Image Not Found " + path - h0, w0 = img.shape[:2] # orig hw - r = self.img_size / max(h0, w0) # resize image to img_size - if r != 1: # always resize down, only resize up if training with augmentation - interp = cv2.INTER_AREA if r < 1 and not self.augment else cv2.INTER_LINEAR - img = cv2.resize(img, (int(w0 * r), int(h0 * r)), interpolation=interp) - return img, (h0, w0), img.shape[:2], label # img, hw_original, hw_resized - - def load_mosaic(self, index, return_seg=False): # YOLOv5 4-mosaic loader. Loads 1 image + 3 random images into a 4-image mosaic labels4, segments4 = [], [] s = self.img_size yc, xc = [int(random.uniform(-x, 2 * s + x)) for x in self.mosaic_border] # mosaic center x, y - num_neg = random.randint(0, 2) if len(self.img_neg_files) else 0 # 3 additional image indices - indices = [index] + random.choices(self.indices, k=(3 - num_neg)) - indices = indices + random.choices(range(len(self.img_neg_files)), k=num_neg) - ri = list(range(4)) - random.shuffle(ri) - for j, (i, index) in enumerate(zip(ri, indices)): - temp_label = None + indices = [index] + random.choices(self.indices, k=3) # 3 additional image indices + for i, index in enumerate(indices): # Load image - # TODO - if j < (4 - num_neg): - if len(self.img_bg_files) and (random.uniform(0, 1) > 0.5): - img, _, (h, w), temp_label = load_bg_image(self, index) - else: - img, _, (h, w) = load_image(self, index) - else: - img, _, (h, w) = load_neg_image(self, index) + img, _, (h, w) = load_image(self, index) + # place img in img4 - if j == 0: - img4 = np.full((s * 2, s * 2, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles if i == 0: # top left - x1a, y1a, x2a, y2a = (max(xc - w, 0), max(yc - h, 0), xc, yc,) # xmin, ymin, xmax, ymax (large image) - x1b, y1b, x2b, y2b = (w - (x2a - x1a), h - (y2a - y1a), w, h,) # xmin, ymin, xmax, ymax (small image) + img4 = np.full((s * 2, s * 2, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles + x1a, y1a, x2a, y2a = max(xc - w, 0), max(yc - h, 0), xc, yc # xmin, ymin, xmax, ymax (large image) + x1b, y1b, x2b, y2b = w - (x2a - x1a), h - (y2a - y1a), w, h # xmin, ymin, xmax, ymax (small image) elif i == 1: # top right x1a, y1a, x2a, y2a = xc, max(yc - h, 0), min(xc + w, s * 2), yc x1b, y1b, x2b, y2b = 0, h - (y2a - y1a), min(w, x2a - x1a), h @@ -664,15 +603,7 @@ def load_mosaic(self, index, return_seg=False): padw = x1a - x1b padh = y1a - y1b - # Labels - if j >= (4 - num_neg): - continue - - # TODO: deal with segments - if len(self.img_bg_files) and temp_label is not None: - labels, segments = temp_label, [] - else: - labels, segments = self.labels[index].copy(), self.segments[index].copy() + labels, segments = self.labels[index].copy(), self.segments[index].copy() if labels.size: labels[:, 1:] = xywhn2xyxy(labels[:, 1:], w, h, padw, padh) # normalized xywh to pixel xyxy format @@ -873,7 +804,6 @@ def hub_ops(f, max_dim=1920): import glob import shutil import hashlib -import uuid import torch import cv2 import random @@ -936,14 +866,6 @@ def exif_transpose(image): image.info["exif"] = exif.tobytes() return image - -def worker_init_reset_seed(worker_id): - seed = uuid.uuid4().int % 2 ** 32 - random.seed(seed) - torch.set_rng_state(torch.manual_seed(seed).get_state()) - np.random.seed(seed) - - def polygon2mask(img_size, polygons, color=1, downsample_ratio=1): """ Args: @@ -1170,90 +1092,3 @@ def __len__(self): def __iter__(self): for i in range(len(self)): yield next(self.iterator) - - -# REFACTOR IN A NEW FILE -from PIL import Image -import numpy as np -from PIL import ImageFile - -# import numbers - -ImageFile.LOAD_TRUNCATED_IMAGES = True - - -def get_raito(new_size, original_size): - """Get the ratio bewtten input_size and original_size""" - # # mmdet way - # iw, ih = new_size - # ow, oh = original_size - # max_long_edge = max(iw, ih) - # max_short_edge = min(iw, ih) - # ratio = min(max_long_edge / max(ow, oh), max_short_edge / min(ow, oh)) - # return ratio - - # # yolov5 way - return min(new_size[0] / original_size[0], new_size[1] / original_size[1]) - - -def imresize(img, new_size): - """Resize the img with new_size by PIL(keep aspect). - - Args: - img (PIL): The original image. - new_size (tuple): The new size(w, h). - """ - if isinstance(new_size, int): - new_size = (new_size, new_size) - old_size = img.size - ratio = get_raito(new_size, old_size) - img = img.resize((int(old_size[0] * ratio), int(old_size[1] * ratio))) - return img - - -def get_wh(a, b): - return np.random.randint(a, b) - - -def paste2(sample1, sample2, background, scale=1.2): - sample1 = Image.open(sample1) - d_w1, d_h1 = sample1.size - - sample2 = Image.open(sample2) - d_w2, d_h2 = sample2.size - - # print(sample.size) - background = Image.open(background) - background = background.resize((int((d_w1 + d_w2) * scale), int((d_h1 + d_h2) * scale))) - bw, bh = background.size - - x1, y1 = get_wh(0, int(d_w1 * scale) - d_w1), get_wh(0, bh - d_h1) - x2, y2 = get_wh(int(d_w1 * scale), bw - d_w2), get_wh(0, bh - d_h2) - # x1, y1 = get_wh(0, int(bw / 2) - d_w1), get_wh(0, bh - d_h1) - # x2, y2 = get_wh(int(bw / 2), bw - d_w2), get_wh(0, bh - d_h2) - - background.paste(sample1, (x1, y1)) - background.paste(sample2, (x2, y2)) - # background = background.resize((416, 416)) - - return np.array(background), (x1, y1, x2, y2), background # print(background.size) # background.show() - - -def paste1(sample, background, bg_size, fg_scale=1.5): - sample = Image.open(sample) - background = Image.open(background) - background = imresize(background, bg_size) - bw, bh = background.size - # background = background.resize((int(d_w * scale), int(d_h * scale))) - new_w, new_h = int(bw / fg_scale), int(bh / fg_scale) - sample = imresize(sample, (new_w, new_h)) - - d_w, d_h = sample.size - x1, y1 = get_wh(0, bw - d_w), get_wh(0, bh - d_h) - background.paste(sample, (x1, y1)) - # draw = ImageDraw.Draw(background) - # draw.rectangle((x1 + 240, y1 + 254, x1 + 240 + 5, y1 + 254 + 5), 'red', 'green') - # draw.rectangle((x1 + 80, y1 + 28, x1 + 400, y1 + 480), None, 'green') - # background = background.resize((416, 416)) - - return np.array(background.convert('RGB'))[:, :, ::-1], (x1, y1), background, (d_w, d_h) diff --git a/train_instseg.py b/train_instseg.py index eaffd189e574..b1ea72ff5757 100644 --- a/train_instseg.py +++ b/train_instseg.py @@ -54,7 +54,7 @@ from utils.loggers.wandb.wandb_utils import check_wandb_resume from utils.seg_loss import ComputeLoss #from utils.metrics import fitness -from utils.seg_plots import plot_evolve, plot_labels +from utils.plots import plot_evolve, plot_labels from utils.torch_utils import EarlyStopping, ModelEMA, de_parallel, select_device, torch_distributed_zero_first diff --git a/utils/boxes.py b/utils/boxes.py deleted file mode 100644 index 1881dde83c81..000000000000 --- a/utils/boxes.py +++ /dev/null @@ -1,298 +0,0 @@ -import time - -import cv2 -import numpy as np -import torch -import torchvision - -from utils.general import clip_coords, scale_coords, xywh2xyxy, xyxy2xywh -from .general import increment_path -from .metrics import box_iou - - -def nms_numpy(boxes, scores, class_id, threshold, method=None, agnostic=False): - """ - :param boxes: numpy(N, 4), xyxy - :param scores: numpy(N, ) - :param class_id: numpy(N, ) - :param threshold: float - :param method: - :return: kept boxed index - """ - if boxes.size == 0: - return np.empty((0,), dtype=np.int8) - max_wh = 4096 - if isinstance(boxes, torch.Tensor): - boxes = boxes.cpu().numpy() - if isinstance(scores, torch.Tensor): - scores = scores.cpu().numpy() - if isinstance(class_id, torch.Tensor): - class_id = class_id.cpu().numpy() - - if boxes.ndim == 1: - boxes = boxes[None, :] - assert boxes.shape[1] == 4, f"expected boxes shape [N, 4], but got {boxes.shape}" - if len(class_id.shape) == 1: - class_id = class_id[:, None] - - assert (boxes.shape[0] == class_id.shape[0] == scores.shape[0]), f"boxes, class_id and scores shapes must be equal" - - c = class_id * (0 if agnostic else max_wh) - boxes = boxes + c - x1 = boxes[:, 0].copy() - y1 = boxes[:, 1].copy() - x2 = boxes[:, 2].copy() - y2 = boxes[:, 3].copy() - - s = scores - area = (x2 - x1 + 1) * (y2 - y1 + 1) - - I = np.argsort(s) # 从小到大排序索引 - pick = np.zeros_like(s, dtype=np.int16) - counter = 0 - while I.size > 0: - i = I[-1] - pick[counter] = i - counter += 1 - idx = I[0:-1] - - xx1 = np.maximum(x1[i], x1[idx]).copy() - yy1 = np.maximum(y1[i], y1[idx]).copy() - xx2 = np.minimum(x2[i], x2[idx]).copy() - yy2 = np.minimum(y2[i], y2[idx]).copy() - - w = np.maximum(0.0, xx2 - xx1 + 1).copy() - h = np.maximum(0.0, yy2 - yy1 + 1).copy() - - inter = w * h - if method == "Min": - o = inter / np.minimum(area[i], area[idx]) - else: - o = inter / (area[i] + area[idx] - inter) - I = I[np.where(o <= threshold)] - - pick = pick[:counter].copy() - return pick - - -def save_one_box(xyxy, im, file="image.jpg", gain=1.02, pad=10, square=False, BGR=False, save=True): - # Save image crop as {file} with crop size multiple {gain} and {pad} pixels. Save and/or return crop - xyxy = torch.tensor(xyxy).view(-1, 4) - b = xyxy2xywh(xyxy) # boxes - if square: - b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1) # attempt rectangle to square - b[:, 2:] = b[:, 2:] * gain + pad # box wh * gain + pad - xyxy = xywh2xyxy(b).long() - clip_coords(xyxy, im.shape) - crop = im[int(xyxy[0, 1]): int(xyxy[0, 3]), int(xyxy[0, 0]): int(xyxy[0, 2]), :: (1 if BGR else -1), ] - if save: - cv2.imwrite(str(increment_path(file, mkdir=True).with_suffix(".jpg")), crop) - return crop - - -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 non_max_suppression_numpy(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 - max_nms = 30000 # maximum number of boxes into torchvision.ops.nms() - time_limit = 10.0 # seconds to quit after - multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img) - - t = time.time() - output = [np.zeros((0, 6))] * 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 = np.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 = np.concatenate((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 = np.concatenate((box[i], x[i, j + 5, None], j[:, None].float()), 1) - else: # best class only - conf, j = x[:, 5:].max(1), x[:, 5:].argmax(1) - x = np.concatenate((box, conf[:, None], j.astype(np.float)[:, None]), 1)[conf > conf_thres] - - # Filter by class - if classes is not None: - x = x[(x[:, 5:6] == np.array(classes)).any(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 - boxes, scores, cls = x[:, :4], x[:, 4], x[:, 5] - i = nms_numpy(boxes, scores, cls, iou_thres, agnostic) # NMS - if i.shape[0] > max_det: # limit detections - i = i[:max_det] - - output[xi] = x[i][None, :] if x[i].ndim == 1 else 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 apply_classifier(x, model, img, im0): - # Apply a second stage classifier to yolo outputs - im0 = [im0] if isinstance(im0, np.ndarray) else im0 - for i, d in enumerate(x): # per image - if d is not None and len(d): - d = d.clone() - - # Reshape and pad cutouts - b = xyxy2xywh(d[:, :4]) # boxes - b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1) # rectangle to square - b[:, 2:] = b[:, 2:] * 1.3 + 30 # pad - d[:, :4] = xywh2xyxy(b).long() - - # Rescale boxes from img_size to im0 size - scale_coords(img.shape[2:], d[:, :4], im0[i].shape) - - # Classes - pred_cls1 = d[:, 5].long() - ims = [] - for j, a in enumerate(d): # per item - cutout = im0[i][int(a[1]): int(a[3]), int(a[0]): int(a[2])] - im = cv2.resize(cutout, (224, 224)) # BGR - # cv2.imwrite('example%i.jpg' % j, cutout) - - im = im[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416 - im = np.ascontiguousarray(im, dtype=np.float32) # uint8 to float32 - im /= 255.0 # 0 - 255 to 0.0 - 1.0 - ims.append(im) - - pred_cls2 = model(torch.Tensor(ims).to(d.device)).argmax(1) # classifier prediction - x[i] = x[i][pred_cls1 == pred_cls2] # retain matching class detections - - return x diff --git a/utils/seg_plots.py b/utils/seg_plots.py deleted file mode 100644 index 3f09d2ad272c..000000000000 --- a/utils/seg_plots.py +++ /dev/null @@ -1,689 +0,0 @@ -# YOLOv5 🚀 by Ultralytics, GPL-3.0 license -""" -Plotting utils -""" - -import math -import os -from copy import copy -from itertools import repeat -from pathlib import Path - -import cv2 -import matplotlib -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -import seaborn as sn -import torch -from PIL import Image, ImageDraw - -from utils.general import check_font, is_ascii, is_chinese -from utils.seg_metrics import fitness -from .boxes import xywh2xyxy, xyxy2xywh - -# Settings -RANK = int(os.getenv("RANK", -1)) -matplotlib.rc("font", **{"size": 11}) -matplotlib.use("Agg") # for writing to files only - - -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 - - @staticmethod - def hex2rgb(h): # rgb order (PIL) - return tuple(int(h[1 + i: 1 + i + 2], 16) for i in (0, 2, 4)) - - -colors = Colors() # create instance for 'from utils.plots import colors' - - -class Annotator: - if RANK in (-1, 0): - check_font() # download TTF if necessary - - # 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) - if self.pil: # use PIL - self.im = im if isinstance(im, Image.Image) else Image.fromarray(im) - self.draw = ImageDraw.Draw(self.im) - self.font = check_font(font="Arial.Unicode.ttf" if is_chinese(example) else font, - size=font_size or max(round(sum(self.im.size) / 2 * 0.035), 12), ) - else: # use cv2 - 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 - if self.pil or not is_ascii(label): - self.draw.rectangle(box, width=self.lw, outline=color) # box - if label: - w, h = self.font.getsize(label) # text width, height - outside = box[1] - h >= 0 # label fits outside box - self.draw.rectangle([box[0], box[1] - h if outside else box[1], box[0] + w + 1, - box[1] + 1 if outside else box[1] + h + 1, ], fill=color, ) - # self.draw.text((box[0], box[1]), label, fill=txt_color, font=self.font, anchor='ls') # for PIL>8.0 - self.draw.text((box[0], box[1] - h if outside else box[1]), label, fill=txt_color, font=self.font, ) - else: # cv2 - 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 text(self, xy, text, txt_color=(255, 255, 255)): - # Add text to image (PIL-only) - w, h = self.font.getsize(text) # text width, height - self.draw.text((xy[0], xy[1] - h + 1), text, fill=txt_color, font=self.font) - - def result(self): - # Return annotated image as array - return np.asarray(self.im) - - -class Visualizer(object): - """Visualization of one model.""" - - def __init__(self, names) -> None: - super().__init__() - self.names = names - - def draw_one_img(self, img, output, vis_conf=0.4): - """Visualize one images. - - Args: - imgs (numpy.ndarray): one image. - outputs (torch.Tensor): one output, (num_boxes, classes+5) - vis_confs (float, optional): Visualize threshold. - Return: - img (numpy.ndarray): Image after visualization. - """ - if isinstance(output, list): - output = output[0] - if output is None or len(output) == 0: - return img - for (*xyxy, conf, cls) in reversed(output[:, :6]): - if conf < vis_conf: - continue - label = '%s %.2f' % (self.names[int(cls)], conf) - color = colors(int(cls)) - plot_one_box(xyxy, img, label=label, color=color, line_thickness=2) - return img - - def draw_multi_img(self, imgs, outputs, vis_confs=0.4): - """Visualize multi images. - - Args: - imgs (List[numpy.array]): multi images. - outputs (List[torch.Tensor]): multi outputs, List[num_boxes, classes+5]. - vis_confs (float | tuple[float], optional): Visualize threshold. - Return: - imgs (List[numpy.ndarray]): Images after visualization. - """ - if isinstance(vis_confs, float): - vis_confs = list(repeat(vis_confs, len(imgs))) - assert len(imgs) == len(outputs) == len(vis_confs) - for i, output in enumerate(outputs): # detections per image - self.draw_one_img(imgs[i], output, vis_confs[i]) - return imgs - - def draw_imgs(self, imgs, outputs, vis_confs=0.4): - if isinstance(imgs, np.ndarray): - return self.draw_one_img(imgs, outputs, vis_confs) - else: - return self.draw_multi_img(imgs, outputs, vis_confs) - - def __call__(self, imgs, outputs, vis_confs=0.4): - return self.draw_imgs(imgs, outputs, vis_confs) - - -def hist2d(x, y, n=100): - # 2d histogram used in labels.png and evolve.png - xedges, yedges = np.linspace(x.min(), x.max(), n), np.linspace(y.min(), y.max(), n) - hist, xedges, yedges = np.histogram2d(x, y, (xedges, yedges)) - xidx = np.clip(np.digitize(x, xedges) - 1, 0, hist.shape[0] - 1) - yidx = np.clip(np.digitize(y, yedges) - 1, 0, hist.shape[1] - 1) - return np.log(hist[xidx, yidx]) - - -def butter_lowpass_filtfilt(data, cutoff=1500, fs=50000, order=5): - from scipy.signal import butter, filtfilt - - # https://stackoverflow.com/questions/28536191/how-to-filter-smooth-with-scipy-numpy - def butter_lowpass(cutoff, fs, order): - nyq = 0.5 * fs - normal_cutoff = cutoff / nyq - return butter(order, normal_cutoff, btype="low", analog=False) - - b, a = butter_lowpass(cutoff, fs, order=order) - return filtfilt(b, a, data) # forward-backward filter - - -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()[:, :6]: - targets.append([i, cls, *list(*xyxy2xywh(np.array(box)[None])), conf]) - return np.array(targets) - - -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() - if np.max(images[0]) <= 1: - images *= 255.0 # de-normalise (optional) - 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) - - # Build Image - mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8) # init - for i, im in enumerate(images): - if i == max_subplots: # if last batch has fewer images than we expect - break - x, y = int(w * (i // ns)), int(h * (i % ns)) # block origin - im = im.transpose(1, 2, 0) - mosaic[y: y + h, x: x + w, :] = im - - # Resize (optional) - scale = max_size / ns / max(h, w) - if scale < 1: - h = math.ceil(scale * h) - w = math.ceil(scale * w) - mosaic = cv2.resize(mosaic, tuple(int(x * ns) for x in (w, h))) - - # Annotate - fs = int((h + w) * ns * 0.01) # 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 - annotator.rectangle([x, y, x + w, y + h], None, (255, 255, 255), width=2) # borders - if paths: - annotator.text((x + 5, y + 5 + h), text=Path(paths[i]).name[:40], txt_color=(220, 220, 220), ) # filenames - if len(targets) > 0: - ti = targets[targets[:, 0] == i] # image targets - boxes = xywh2xyxy(ti[:, 2:6]).T - classes = ti[:, 1].astype("int") - labels = ti.shape[1] == 6 # labels if no conf column - conf = None if labels else ti[:, 6] # check for confidence presence (label vs pred) - - if boxes.shape[1]: - if boxes.max() <= 1.01: # if normalized with tolerance 0.01 - boxes[[0, 2]] *= w # scale to pixels - boxes[[1, 3]] *= h - elif scale < 1: # absolute coords need scale if image scales - boxes *= scale - boxes[[0, 2]] += x - boxes[[1, 3]] += y - for j, box in enumerate(boxes.T.tolist()): - cls = classes[j] - color = colors(cls) - cls = names[cls] if names else cls - if labels or conf[j] > 0.25: # 0.25 conf thresh - label = f"{cls}" if labels else f"{cls} {conf[j]:.1f}" - annotator.box_label(box, label, color=color) - annotator.im.save(fname) # save - return annotator.result() - - -def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=""): - # Plot LR simulating training for full epochs - optimizer, scheduler = copy(optimizer), copy(scheduler) # do not modify originals - y = [] - for _ in range(epochs): - scheduler.step() - y.append(optimizer.param_groups[0]["lr"]) - plt.plot(y, ".-", label="LR") - plt.xlabel("epoch") - plt.ylabel("LR") - plt.grid() - plt.xlim(0, epochs) - plt.ylim(0) - plt.savefig(Path(save_dir) / "LR.png", dpi=200) - plt.close() - - -def plot_val_txt(): # from utils.plots import *; plot_val() - # Plot val.txt histograms - x = np.loadtxt("val.txt", dtype=np.float32) - box = xyxy2xywh(x[:, :4]) - cx, cy = box[:, 0], box[:, 1] - - fig, ax = plt.subplots(1, 1, figsize=(6, 6), tight_layout=True) - ax.hist2d(cx, cy, bins=600, cmax=10, cmin=0) - ax.set_aspect("equal") - plt.savefig("hist2d.png", dpi=300) - - fig, ax = plt.subplots(1, 2, figsize=(12, 6), tight_layout=True) - ax[0].hist(cx, bins=600) - ax[1].hist(cy, bins=600) - plt.savefig("hist1d.png", dpi=200) - - -def plot_targets_txt(): # from utils.plots import *; plot_targets_txt() - # Plot targets.txt histograms - x = np.loadtxt("targets.txt", dtype=np.float32).T - s = ["x targets", "y targets", "width targets", "height targets"] - fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True) - ax = ax.ravel() - for i in range(4): - ax[i].hist(x[i], bins=100, label="%.3g +/- %.3g" % (x[i].mean(), x[i].std())) - ax[i].legend() - ax[i].set_title(s[i]) - plt.savefig("targets.jpg", dpi=200) - - -def plot_val_study(file="", dir="", x=None): # from utils.plots import *; plot_val_study() - # Plot file=study.txt generated by val.py (or plot all study*.txt in dir) - save_dir = Path(file).parent if file else Path(dir) - plot2 = False # plot additional results - if plot2: - ax = plt.subplots(2, 4, figsize=(10, 6), tight_layout=True)[1].ravel() - - fig2, ax2 = plt.subplots(1, 1, figsize=(8, 4), tight_layout=True) - # for f in [save_dir / f'study_coco_{x}.txt' for x in ['yolov5n6', 'yolov5s6', 'yolov5m6', 'yolov5l6', 'yolov5x6']]: - for f in sorted(save_dir.glob("study*.txt")): - y = np.loadtxt(f, dtype=np.float32, usecols=[0, 1, 2, 3, 7, 8, 9], ndmin=2).T - x = np.arange(y.shape[1]) if x is None else np.array(x) - if plot2: - s = ["P", "R", "mAP@.5", "mAP@.5:.95", "t_preprocess (ms/img)", "t_inference (ms/img)", "t_NMS (ms/img)", ] - for i in range(7): - ax[i].plot(x, y[i], ".-", linewidth=2, markersize=8) - ax[i].set_title(s[i]) - - j = y[3].argmax() + 1 - ax2.plot(y[5, 1:j], y[3, 1:j] * 1e2, ".-", linewidth=2, markersize=8, - label=f.stem.replace("study_coco_", "").replace("yolo", "YOLO"), ) - - 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=0.25, label="EfficientDet", ) - - ax2.grid(alpha=0.2) - ax2.set_yticks(np.arange(20, 60, 5)) - ax2.set_xlim(0, 57) - ax2.set_ylim(25, 55) - ax2.set_xlabel("GPU Speed (ms/img)") - ax2.set_ylabel("COCO AP val") - ax2.legend(loc="lower right") - f = save_dir / "study.png" - print(f"Saving {f}...") - plt.savefig(f, dpi=300) - - -def plot_labels(labels, names=(), save_dir=Path("")): - # plot dataset labels - print("Plotting labels... ") - c, b = labels[:, 0], labels[:, 1:].transpose() # classes, boxes - nc = int(c.max() + 1) # number of classes - x = pd.DataFrame(b.transpose(), columns=["x", "y", "width", "height"]) - - # seaborn correlogram - sn.pairplot(x, corner=True, diag_kind="auto", kind="hist", diag_kws=dict(bins=50), plot_kws=dict(pmax=0.9), ) - plt.savefig(save_dir / "labels_correlogram.jpg", dpi=200) - plt.close() - - # matplotlib labels - matplotlib.use("svg") # faster - ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)[1].ravel() - y = ax[0].hist(c, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8) - # [y[2].patches[i].set_color([x / 255 for x in colors(i)]) for i in range(nc)] # update colors bug #3195 - ax[0].set_ylabel("instances") - if 0 < len(names) < 30: - ax[0].set_xticks(range(len(names))) - ax[0].set_xticklabels(names, rotation=90, fontsize=10) - else: - ax[0].set_xlabel("classes") - sn.histplot(x, x="x", y="y", ax=ax[2], bins=50, pmax=0.9) - sn.histplot(x, x="width", y="height", ax=ax[3], bins=50, pmax=0.9) - - # rectangles - labels[:, 1:3] = 0.5 # center - labels[:, 1:] = xywh2xyxy(labels[:, 1:]) * 2000 - img = Image.fromarray(np.ones((2000, 2000, 3), dtype=np.uint8) * 255) - for cls, *box in labels[:1000]: - ImageDraw.Draw(img).rectangle(box, width=1, outline=colors(cls)) # plot - ax[1].imshow(img) - ax[1].axis("off") - - for a in [0, 1, 2, 3]: - for s in ["top", "right", "left", "bottom"]: - ax[a].spines[s].set_visible(False) - - plt.savefig(save_dir / "labels.jpg", dpi=200) - matplotlib.use("Agg") - plt.close() - - -def profile_idetection(start=0, stop=0, labels=(), save_dir=""): - # Plot iDetection '*.txt' per-image logs. from utils.plots import *; profile_idetection() - ax = plt.subplots(2, 4, figsize=(12, 6), tight_layout=True)[1].ravel() - s = ["Images", "Free Storage (GB)", "RAM Usage (GB)", "Battery", "dt_raw (ms)", "dt_smooth (ms)", - "real-world FPS", ] - files = list(Path(save_dir).glob("frames*.txt")) - for fi, f in enumerate(files): - try: - results = np.loadtxt(f, ndmin=2).T[:, 90:-30] # clip first and last rows - n = results.shape[1] # number of rows - x = np.arange(start, min(stop, n) if stop else n) - results = results[:, x] - t = results[0] - results[0].min() # set t0=0s - results[0] = x - for i, a in enumerate(ax): - if i < len(results): - label = labels[fi] if len(labels) else f.stem.replace("frames_", "") - a.plot(t, results[i], marker=".", label=label, linewidth=1, markersize=5, ) - a.set_title(s[i]) - a.set_xlabel("time (s)") - # if fi == len(files) - 1: - # a.set_ylim(bottom=0) - for side in ["top", "right"]: - a.spines[side].set_visible(False) - else: - a.remove() - except Exception as e: - print("Warning: Plotting error for %s; %s" % (f, e)) - ax[1].legend() - plt.savefig(Path(save_dir) / "idetection_profile.png", dpi=200) - - -def plot_evolve(evolve_csv="path/to/evolve.csv", ): # from utils.plots import *; plot_evolve() - # Plot evolve.csv hyp evolution results - evolve_csv = Path(evolve_csv) - data = pd.read_csv(evolve_csv) - keys = [x.strip() for x in data.columns] - x = data.values - f = fitness(x) - j = np.argmax(f) # max fitness index - plt.figure(figsize=(10, 12), tight_layout=True) - matplotlib.rc("font", **{"size": 8}) - for i, k in enumerate(keys[7:]): - v = x[:, 7 + i] - mu = v[j] # best single result - plt.subplot(6, 5, i + 1) - plt.scatter(v, f, c=hist2d(v, f, 20), cmap="viridis", alpha=0.8, edgecolors="none") - plt.plot(mu, f.max(), "k+", markersize=15) - plt.title("%s = %.3g" % (k, mu), fontdict={"size": 9}) # limit to 40 characters - if i % 5 != 0: - plt.yticks([]) - print("%15s: %.3g" % (k, mu)) - f = evolve_csv.with_suffix(".png") # filename - plt.savefig(f, dpi=200) - plt.close() - print(f"Saved {f}") - - -def plot_results(file="path/to/results.csv", dir="", best=True): - # 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")) - assert len(files), f"No results.csv files found in {save_dir.resolve()}, nothing to plot." - for _, f in enumerate(files): - try: - data = pd.read_csv(f) - index = np.argmax(0.9 * data.values[:, 7] + 0.1 * data.values[:, 6]) - 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=2) - if best: - # best - ax[i].scatter(index, y[index], color="r", label=f"best:{index}", marker="*", linewidth=3, ) - ax[i].set_title(s[j] + f"\n{round(y[index], 5)}") - else: - # last - ax[i].scatter(x[-1], y[-1], color="r", label="last", marker="*", linewidth=3) - ax[i].set_title(s[ - j] + f"\n{round(y[-1], 5)}") # 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]) - 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 plot_results_with_masks(file="path/to/results.csv", dir="", best=True): - # 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, 8, figsize=(18, 6), tight_layout=True) - ax = ax.ravel() - files = list(save_dir.glob("results*.csv")) - assert len(files), f"No results.csv files found in {save_dir.resolve()}, nothing to plot." - for _, f in enumerate(files): - try: - data = pd.read_csv(f) - index = np.argmax( - 0.9 * data.values[:, 8] + 0.1 * data.values[:, 7] + 0.9 * data.values[:, 12] + 0.1 * data.values[:, - 11], ) - s = [x.strip() for x in data.columns] - x = data.values[:, 0] - for i, j in enumerate([1, 2, 3, 4, 5, 6, 9, 10, 13, 14, 15, 16, 7, 8, 11, 12]): - 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=2) - if best: - # best - ax[i].scatter(index, y[index], color="r", label=f"best:{index}", marker="*", linewidth=3, ) - ax[i].set_title(s[j] + f"\n{round(y[index], 5)}") - else: - # last - ax[i].scatter(x[-1], y[-1], color="r", label="last", marker="*", linewidth=3) - ax[i].set_title(s[ - j] + f"\n{round(y[-1], 5)}") # 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]) - 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 plot_one_box(x, img, color=None, label=None, line_thickness=None): - import random - - # Plots one bounding box on image img - tl = (line_thickness or round(0.002 * (img.shape[0] + img.shape[1]) / 2) + 1) # line/font thickness - color = color or [random.randint(0, 255) for _ in range(3)] - c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3])) - cv2.rectangle(img, c1, c2, color, thickness=tl, lineType=cv2.LINE_AA) - if label: - tf = max(tl - 1, 1) # font thickness - t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0] - c2 = c1[0] + t_size[0], c1[1] - t_size[1] - 3 - cv2.rectangle(img, c1, c2, color, -1, cv2.LINE_AA) # filled - cv2.putText(img, label, (c1[0], c1[1] - 2), 0, tl / 3, [225, 255, 255], thickness=tf, lineType=cv2.LINE_AA, ) - - -def feature_visualization(x, module_type, stage, n=32, save_dir=Path("runs/detect/exp")): - """ - x: Features to be visualized - module_type: Module type - stage: Module stage within model - n: Maximum number of feature maps to plot - save_dir: Directory to save results - """ - if "Detect" not in module_type: - batch, channels, height, width = x.shape # batch, channels, height, width - if height > 1 and width > 1: - f = f"stage{stage}_{module_type.split('.')[-1]}_features.png" # filename - - blocks = torch.chunk(x[0].cpu(), channels, dim=0) # select batch index 0, block by channels - n = min(n, channels) # number of plots - fig, ax = plt.subplots(math.ceil(n / 8), 8, tight_layout=True) # 8 rows x n/8 cols - ax = ax.ravel() - plt.subplots_adjust(wspace=0.05, hspace=0.05) - for i in range(n): - ax[i].imshow(blocks[i].squeeze()) # cmap='gray' - ax[i].axis("off") - - print(f"Saving {save_dir / f}... ({n}/{channels})") - plt.savefig(save_dir / f, dpi=300, bbox_inches="tight") - plt.close() - - -def plot_images_and_masks(images, targets, masks, paths=None, fname="images.jpg", names=None, max_size=640, - max_subplots=16, ): - # Plot image grid with labels - # print("targets:", targets.shape) - # print("masks:", masks.shape) - # print('--------------------------') - - if isinstance(images, torch.Tensor): - images = images.cpu().float().numpy() - if isinstance(targets, torch.Tensor): - targets = targets.cpu().numpy() - if isinstance(masks, torch.Tensor): - masks = masks.cpu().numpy() - masks = masks.astype(int) - - # un-normalise - if np.max(images[0]) <= 1: - images *= 255 - - tl = 3 # line thickness - tf = max(tl - 1, 1) # font thickness - 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) - - # Check if we should resize - scale_factor = max_size / max(h, w) - if scale_factor < 1: - h = math.ceil(scale_factor * h) - w = math.ceil(scale_factor * w) - - mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8) # init - for i, img in enumerate(images): - if i == max_subplots: # if last batch has fewer images than we expect - break - - block_x = int(w * (i // ns)) - block_y = int(h * (i % ns)) - - img = img.transpose(1, 2, 0) - if scale_factor < 1: - img = cv2.resize(img, (w, h)) - - mosaic[block_y: block_y + h, block_x: block_x + w, :] = img - if len(targets) > 0: - idx = (targets[:, 0]).astype(int) - image_targets = targets[idx == i] - # print(targets.shape) - # print(masks.shape) - image_masks = masks[idx == i] - # mosaic_masks - # mosaic_masks[block_y:block_y + h, - # block_x:block_x + w, :] = image_masks - boxes = xywh2xyxy(image_targets[:, 2:6]).T - classes = image_targets[:, 1].astype("int") - labels = image_targets.shape[1] == 6 # labels if no conf column - conf = (None if labels else image_targets[:, 6]) # check for confidence presence (label vs pred) - - if boxes.shape[1]: - if boxes.max() <= 1.01: # if normalized with tolerance 0.01 - boxes[[0, 2]] *= w # scale to pixels - boxes[[1, 3]] *= h - elif scale_factor < 1: # absolute coords need scale if image scales - boxes *= scale_factor - boxes[[0, 2]] += block_x - boxes[[1, 3]] += block_y - for j, box in enumerate(boxes.T): - cls = int(classes[j]) - color = colors(cls) - cls = names[cls] if names else cls - mask = image_masks[j].astype(np.bool) - # print(mask.shape) - # print(mosaic.shape) - if labels or conf[j] > 0.25: # 0.25 conf thresh - label = "%s" % cls if labels else "%s %.1f" % (cls, conf[j]) - plot_one_box(box, mosaic, label=label, color=color, line_thickness=tl) - mosaic[block_y: block_y + h, block_x: block_x + w, :][mask] = \ - mosaic[block_y: block_y + h, block_x: block_x + w, :][mask] * 0.35 + (np.array(color) * 0.65) - - # Draw image filename labels - if paths: - label = Path(paths[i]).name[:40] # trim to 40 char - t_size = cv2.getTextSize(label, 0, fontScale=tl / 3, thickness=tf)[0] - cv2.putText(mosaic, label, (block_x + 5, block_y + t_size[1] + 5), 0, tl / 3, [220, 220, 220], thickness=tf, - lineType=cv2.LINE_AA, ) - - # Image border - cv2.rectangle(mosaic, (block_x, block_y), (block_x + w, block_y + h), (255, 255, 255), thickness=3, ) - - if fname: - r = min(1280.0 / max(h, w) / ns, 1.0) # ratio to limit image size - mosaic = cv2.resize(mosaic, (int(ns * w * r), int(ns * h * r)), interpolation=cv2.INTER_AREA) - # cv2.imwrite(fname, cv2.cvtColor(mosaic, cv2.COLOR_BGR2RGB)) # cv2 save - Image.fromarray(mosaic).save(fname) # PIL save - return mosaic - - -def plot_images_boxes_and_masks(images, targets, masks=None, paths=None, fname="images.jpg", names=None, max_size=640, - max_subplots=16, ): - if masks is not None: - return plot_images_and_masks(images, targets, masks, paths, fname, names, max_size, max_subplots) - else: - return plot_images(images, targets, paths, fname, names, max_size, max_subplots) - - -def plot_masks(img, masks, colors, alpha=0.5): - """ - Args: - img (tensor): img on cuda, shape: [3, h, w], range: [0, 1] - masks (tensor): predicted masks on cuda, shape: [n, h, w] - colors (List[List[Int]]): colors for predicted masks, [[r, g, b] * n] - Return: - img after draw masks, shape: [h, w, 3] - - transform colors and send img_gpu to cpu for the most time. - """ - img_gpu = img.clone() - num_masks = len(masks) - # [n, 1, 1, 3] - # faster this way to transform colors - colors = torch.tensor(colors, device=img.device).float() / 255.0 - colors = colors[:, None, None, :] - # [n, h, w, 1] - masks = masks[:, :, :, None] - masks_color = masks.repeat(1, 1, 1, 3) * colors * alpha - inv_alph_masks = masks * (-alpha) + 1 - masks_color_summand = masks_color[0] - if num_masks > 1: - inv_alph_cumul = inv_alph_masks[: (num_masks - 1)].cumprod(dim=0) - masks_color_cumul = masks_color[1:] * inv_alph_cumul - masks_color_summand += masks_color_cumul.sum(dim=0) - - # print(inv_alph_masks.prod(dim=0).shape) # [h, w, 1] - img_gpu = img_gpu.flip(dims=[0]) # filp channel for opencv - img_gpu = img_gpu.permute(1, 2, 0).contiguous() - # [h, w, 3] - img_gpu = img_gpu * inv_alph_masks.prod(dim=0) + masks_color_summand - return (img_gpu * 255).byte().cpu().numpy() diff --git a/val_instseg.py b/val_instseg.py new file mode 100644 index 000000000000..20183b6d7118 --- /dev/null +++ b/val_instseg.py @@ -0,0 +1,85 @@ +# YOLOv5 🚀 by Ultralytics, GPL-3.0 license +""" +Validate a trained YOLOv5 model accuracy on a custom dataset + +Usage: + $ python path/to/val.py --data coco128.yaml --weights yolov5s.pt --img 640 +""" + +import argparse +from evaluator import Yolov5Evaluator + +from utils.general import ( + set_logging, + print_args, + check_yaml, + check_requirements, +) + + +def parse_opt(): + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--data', type=str, default='data/coco128.yaml', help='dataset.yaml path') + parser.add_argument('-w', '--weights', nargs='+', type=str, default='yolov5s.pt', help='model.pt path(s)') + parser.add_argument('-b', '--batch-size', type=int, default=32, help='batch size') + parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)') + parser.add_argument('--conf-thres', type=float, default=0.001, help='confidence threshold') + parser.add_argument('--iou-thres', type=float, default=0.6, help='NMS IoU threshold') + parser.add_argument('--task', default='val', help='train, val, test, speed or study') + parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') + parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset') + parser.add_argument('--augment', action='store_true', help='augmented inference') + parser.add_argument('--verbose', action='store_true', help='report mAP by class') + parser.add_argument('--save-txt', action='store_true', help='save results to *.txt') + parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels') + parser.add_argument('--save-json', action='store_true', help='save a COCO-JSON results file') + parser.add_argument('--nosave', action='store_true', help='do not save anything.') + parser.add_argument('--project', default='runs/val', help='save to project/name') + parser.add_argument('--name', default='exp', help='save to project/name') + 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('--overlap-mask', action='store_true', help='Eval overlapping masks') + + opt = parser.parse_args() + opt.data = check_yaml(opt.data) # check YAML + opt.save_json |= opt.data.endswith('coco.yaml') + print_args(vars(opt)) + return opt + +def main(opt): + set_logging() + check_requirements(exclude=("tensorboard", "thop")) + evaluator = Yolov5Evaluator( + data=opt.data, + conf_thres=opt.conf_thres, + iou_thres=opt.iou_thres, + device=opt.device, + single_cls=opt.single_cls, + augment=opt.augment, + verbose=opt.verbose, + project=opt.project, + name=opt.name, + exist_ok=opt.exist_ok, + half=opt.half, + mask=True, + nosave=opt.nosave, + overlap=opt.overlap_mask, + ) + + if opt.task in ("train", "val", "test"): # run normally + evaluator.run( + weights=opt.weights, + batch_size=opt.batch_size, + imgsz=opt.imgsz, + save_txt=opt.save_txt, + save_conf=opt.save_conf, + save_json=opt.save_json, + task=opt.task, + ) + else: + raise ValueError(f"not support task {opt.task}") + + +if __name__ == "__main__": + opt = parse_opt() + main(opt)