美文网首页
YOLOv5-4.0版本源码解读--general.py

YOLOv5-4.0版本源码解读--general.py

作者: 彩虹直至黑白_Joon | 来源:发表于2021-09-01 15:33 被阅读0次

    0|前言

    YOLOv5为兼顾速度与性能的目标检测算法。笔者将在近期更新一系列YOLOv5的代码导读博客。YOLOv5为2021.1.5日发布的4.0版本。
    YOLOv5开源项目github网址
    本博客导读的代码为utils文件夹下的general.py,取自1.27日更新的版本。

    general.py

    该文件提供了模型多个过程中用到的通用方法,每个功能以函数的方式进行定义。
    这里为了对比阅读,附上模型验证指标的全部代码,以供读者阅读:

    # General utils
    
    import glob
    import logging
    import math
    import os
    import platform
    import random
    import re
    import subprocess
    import time
    from pathlib import Path
    
    import cv2
    import numpy as np
    import torch
    import torchvision
    import yaml
    
    from utils.google_utils import gsutil_getsize
    from utils.metrics import fitness
    from utils.torch_utils import init_torch_seeds
    
    # Settings
    torch.set_printoptions(linewidth=320, precision=5, profile='long')
    np.set_printoptions(linewidth=320, formatter={'float_kind': '{:11.5g}'.format})  # format short g, %precision=5
    cv2.setNumThreads(0)  # prevent OpenCV from multithreading (incompatible with PyTorch DataLoader)
    
    
    def set_logging(rank=-1):
        logging.basicConfig(
            format="%(message)s",
            level=logging.INFO if rank in [-1, 0] else logging.WARN)
    
    
    def init_seeds(seed=0):
        random.seed(seed)
        np.random.seed(seed)
        init_torch_seeds(seed)
    
    
    def get_latest_run(search_dir='.'):
        # Return path to most recent 'last.pt' in /runs (i.e. to --resume from)
        last_list = glob.glob(f'{search_dir}/**/last*.pt', recursive=True)
        return max(last_list, key=os.path.getctime) if last_list else ''
    
    
    def check_git_status():
        # Suggest 'git pull' if repo is out of date
        if platform.system() in ['Linux', 'Darwin'] and not os.path.isfile('/.dockerenv'):
            s = subprocess.check_output('if [ -d .git ]; then git fetch && git status -uno; fi', shell=True).decode('utf-8')
            if 'Your branch is behind' in s:
                print(s[s.find('Your branch is behind'):s.find('\n\n')] + '\n')
    
    
    def check_img_size(img_size, s=32):
        # Verify img_size is a multiple of stride s
        new_size = make_divisible(img_size, int(s))  # ceil gs-multiple
        if new_size != img_size:
            print('WARNING: --img-size %g must be multiple of max stride %g, updating to %g' % (img_size, s, new_size))
        return new_size
    
    
    def check_file(file):
        # Search for file if not found
        if os.path.isfile(file) or file == '':
            return file
        else:
            files = glob.glob('./**/' + file, recursive=True)  # find file
            assert len(files), 'File Not Found: %s' % file  # assert file was found
            assert len(files) == 1, "Multiple files match '%s', specify exact path: %s" % (file, files)  # assert unique
            return files[0]  # return file
    
    
    def check_dataset(dict):
        # Download dataset if not found locally
        val, s = dict.get('val'), dict.get('download')
        if val and len(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()])
                if s and len(s):  # download script
                    print('Downloading %s ...' % s)
                    if s.startswith('http') and s.endswith('.zip'):  # URL
                        f = Path(s).name  # filename
                        torch.hub.download_url_to_file(s, f)
                        r = os.system('unzip -q %s -d ../ && rm %s' % (f, f))  # unzip
                    else:  # bash script
                        r = os.system(s)
                    print('Dataset autodownload %s\n' % ('success' if r == 0 else 'failure'))  # analyze return value
                else:
                    raise Exception('Dataset not found.')
    
    
    def make_divisible(x, divisor):
        # Returns x evenly divisible by divisor
        return math.ceil(x / divisor) * divisor
    
    
    def clean_str(s):
        # Cleans a string by replacing special characters with underscore _
        return re.sub(pattern="[|@#!¡·$€%&()=?¿^*;:,¨´><+]", repl="_", string=s)
    
    
    def one_cycle(y1=0.0, y2=1.0, steps=100):
        # lambda function for sinusoidal ramp from y1 to y2
        return lambda x: ((1 - math.cos(x * math.pi / steps)) / 2) * (y2 - y1) + y1
    
    
    def labels_to_class_weights(labels, nc=80):
        # Get class weights (inverse frequency) from training labels
        if labels[0] is None:  # no labels loaded
            return torch.Tensor()
    
        labels = np.concatenate(labels, 0)  # labels.shape = (866643, 5) for COCO
        classes = labels[:, 0].astype(np.int)  # labels = [class xywh]
        weights = np.bincount(classes, minlength=nc)  # occurrences per class
    
        # Prepend gridpoint count (for uCE training)
        # gpi = ((320 / 32 * np.array([1, 2, 4])) ** 2 * 3).sum()  # gridpoints per image
        # weights = np.hstack([gpi * len(labels)  - weights.sum() * 9, weights * 9]) ** 0.5  # prepend gridpoints to start
    
        weights[weights == 0] = 1  # replace empty bins with 1
        weights = 1 / weights  # number of targets per class
        weights /= weights.sum()  # normalize
        return torch.from_numpy(weights)
    
    
    def labels_to_image_weights(labels, nc=80, class_weights=np.ones(80)):
        # Produces image weights based on class_weights and image contents
        class_counts = np.array([np.bincount(x[:, 0].astype(np.int), minlength=nc) for x in labels])
        image_weights = (class_weights.reshape(1, nc) * class_counts).sum(1)
        # index = random.choices(range(n), weights=image_weights, k=1)  # weight image sample
        return image_weights
    
    
    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 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 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
    
    
    def clip_coords(boxes, img_shape):
        # Clip bounding xyxy bounding boxes to image shape (height, width)
        boxes[:, 0].clamp_(0, img_shape[1])  # x1
        boxes[:, 1].clamp_(0, img_shape[0])  # y1
        boxes[:, 2].clamp_(0, img_shape[1])  # x2
        boxes[:, 3].clamp_(0, img_shape[0])  # y2
    
    
    def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-9):
        # Returns the IoU of box1 to box2. box1 is 4, box2 is nx4
        box2 = box2.T
    
        # Get the coordinates of bounding boxes
        if x1y1x2y2:  # x1, y1, x2, y2 = box1
            b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3]
            b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3]
        else:  # transform from xywh to xyxy
            b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2
            b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2
            b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2
            b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2
    
        # Intersection area
        inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \
                (torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0)
    
        # Union Area
        w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps
        w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps
        union = w1 * h1 + w2 * h2 - inter + eps
    
        iou = inter / union
        if GIoU or DIoU or CIoU:
            cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1)  # convex (smallest enclosing box) width
            ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1)  # convex height
            if CIoU or DIoU:  # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
                c2 = cw ** 2 + ch ** 2 + eps  # convex diagonal squared
                rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 +
                        (b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4  # center distance squared
                if DIoU:
                    return iou - rho2 / c2  # DIoU
                elif CIoU:  # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
                    v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / h2) - torch.atan(w1 / h1), 2)
                    with torch.no_grad():
                        alpha = v / ((1 + eps) - iou + v)
                    return iou - (rho2 / c2 + v * alpha)  # CIoU
            else:  # GIoU https://arxiv.org/pdf/1902.09630.pdf
                c_area = cw * ch + eps  # convex area
                return iou - (c_area - union) / c_area  # GIoU
        else:
            return iou  # IoU
    
    
    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 wh_iou(wh1, wh2):
        # Returns the nxm IoU matrix. wh1 is nx2, wh2 is mx2
        wh1 = wh1[:, None]  # [N,1,2]
        wh2 = wh2[None]  # [1,M,2]
        inter = torch.min(wh1, wh2).prod(2)  # [N,M]
        return inter / (wh1.prod(2) + wh2.prod(2) - inter)  # iou = inter / (area1 + area2 - inter)
    
    
    def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, labels=()):
        """Performs Non-Maximum Suppression (NMS) on inference results
    
        Returns:
             detections with shape: nx6 (x1, y1, x2, y2, conf, cls)
        """
    
        nc = prediction.shape[2] - 5  # number of classes
        xc = prediction[..., 4] > conf_thres  # candidates
    
        # Settings
        min_wh, max_wh = 2, 4096  # (pixels) minimum and maximum box width and height
        max_det = 300  # maximum number of detections per image
        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 strip_optimizer(f='weights/best.pt', s=''):  # from utils.general import *; strip_optimizer()
        # Strip optimizer from 'f' to finalize training, optionally save as 's'
        x = torch.load(f, map_location=torch.device('cpu'))
        x['optimizer'] = None
        x['training_results'] = None
        x['epoch'] = -1
        x['model'].half()  # to FP16
        for p in x['model'].parameters():
            p.requires_grad = False
        torch.save(x, s or f)
        mb = os.path.getsize(s or f) / 1E6  # filesize
        print('Optimizer stripped from %s,%s %.1fMB' % (f, (' saved as %s,' % s) if s else '', mb))
    
    
    def print_mutation(hyp, results, yaml_file='hyp_evolved.yaml', bucket=''):
        # Print mutation results to evolve.txt (for use with train.py --evolve)
        a = '%10s' * len(hyp) % tuple(hyp.keys())  # hyperparam keys
        b = '%10.3g' * len(hyp) % tuple(hyp.values())  # hyperparam values
        c = '%10.4g' * len(results) % results  # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
        print('\n%s\n%s\nEvolved fitness: %s\n' % (a, b, c))
    
        if bucket:
            url = 'gs://%s/evolve.txt' % bucket
            if gsutil_getsize(url) > (os.path.getsize('evolve.txt') if os.path.exists('evolve.txt') else 0):
                os.system('gsutil cp %s .' % url)  # download evolve.txt if larger than local
    
        with open('evolve.txt', 'a') as f:  # append result
            f.write(c + b + '\n')
        x = np.unique(np.loadtxt('evolve.txt', ndmin=2), axis=0)  # load unique rows
        x = x[np.argsort(-fitness(x))]  # sort
        np.savetxt('evolve.txt', x, '%10.3g')  # save sort by fitness
    
        # Save yaml
        for i, k in enumerate(hyp.keys()):
            hyp[k] = float(x[0, i + 7])
        with open(yaml_file, 'w') as f:
            results = tuple(x[0, :7])
            c = '%10.4g' * len(results) % results  # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
            f.write('# Hyperparameter Evolution Results\n# Generations: %g\n# Metrics: ' % len(x) + c + '\n\n')
            yaml.dump(hyp, f, sort_keys=False)
    
        if bucket:
            os.system('gsutil cp evolve.txt %s gs://%s' % (yaml_file, bucket))  # upload
    
    
    def apply_classifier(x, model, img, im0):
        # applies 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('test%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
    
    
    def increment_path(path, exist_ok=True, sep=''):
        # Increment path, i.e. runs/exp --> runs/exp{sep}0, runs/exp{sep}1 etc.
        path = Path(path)  # os-agnostic
        if (path.exists() and exist_ok) or (not path.exists()):
            return str(path)
        else:
            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
            return f"{path}{sep}{n}"  # update path
    
    

    1) 首先欣赏一下导入模块

    # General utils
    
    import glob      # 仅支持部分通配符的文件搜索模块
    import logging   # 日志模块
    import math      # 数学公式模块
    import os        # 与操作系统进行交互的模块
    import platform  # 获取操作系统相关信息的模块
    import random    # 生成随机数的模块
    import re        # 用来匹配字符串(动态、模糊)的模块
    import subprocess  # 创建子进程的模块
    import time        # 用来获取系统时间的模块
    from pathlib import Path #Path对象 简便对path进行操作
    
    import cv2         # OpenCV库
    import numpy as np # numpy矩阵处理函数库
    import torch       # pytorch框架
    import torchvision # 为pytorch提供一些辅助工具
    import yaml        # yaml配置文件模块
    
    # 以下调用三个函数具体注释见相应模块
    from utils.google_utils import gsutil_getsize #用于返回网站链接对应文件的大小
    from utils.metrics import fitness   #返回指标的加权值的行向量
    from utils.torch_utils import init_torch_seeds  #功能为初始化随机种子
    

    1) 运行相关的一些基本的设置

    # Settings
    # 下两行为设置tensor和numpy array的打印格式 linewidth为每一行字符上限 precision为精度
    torch.set_printoptions(linewidth=320, precision=5, profile='long')
    np.set_printoptions(linewidth=320, formatter={'float_kind': '{:11.5g}'.format})  # format short g, %precision=5
    cv2.setNumThreads(0)  # 阻止openCV参与多线程(与pytorch 的DataLoader不兼容)
    

    2) 该函数为对日志的设置进行初始化rank为-1或0时设置输出级别为WARN

    def set_logging(rank=-1):
        logging.basicConfig(
            format="%(message)s",
            level=logging.INFO if rank in [-1, 0] else logging.WARN)
    

    3). 初始化随机种子统一

    # --该函数为初始化随机种子 统一random numpy torch 种子--#
    def init_seeds(seed=0):
        # 初始化随机数种子生成器
        random.seed(seed)
        np.random.seed(seed)
        init_torch_seeds(seed)
    

    4). 该函数返回最近的模型"last.pt"对应的路径

    # --该函数返回最近的模型“last.pt”对应的路径 --#
    def get_latest_run(search_dir='.'):
        # Return path to most recent 'last.pt' in /runs (i.e. to --resume from)
        # 从python版本3.5开始, glob模块支持该“**”指令(仅当传递recursive标志时才会解析该指令)
        # glob.glob函数匹配所有符合条件的文件,并将其以list的形式返回
        last_list = glob.glob(f'{search_dir}/**/last*.pt', recursive=True)
        # os.getctime 返回路径对应文件的创建时间
        # 也就是返回所有文件中创建时间最晚的路径
        return max(last_list, key=os.path.getctime) if last_list else ' '
    

    5). 检查当前代码是否是最新版 如果不是最新的 会提示使用git pull命令进行升级

    def check_git_status():
        # Suggest 'git pull' if repo is out of date
        # 
        if platform.system() in ['Linux', 'Darwin'] and not os.path.isfile('/.dockerenv'):
            s = subprocess.check_output('if [ -d .git ]; then git fetch && git status -uno; fi', shell=True).decode('utf-8')
            if 'Your branch is behind' in s:
                print(s[s.find('Your branch is behind'):s.find('\n\n')] + '\n')
    

    6). 将img_size 是否能被s整除, 这里默认s为32

    def check_img_size(img_size, s=32):
        # Verify img_size is a multiple of stride s
        # make_divisible 函数见下文注解
        new_size = make_divisible(img_size, int(s))  # 返回大于等于img_size的最小能被s整除的值
        if new_size != img_size: # 新size和旧size不同时 打印出信息
            print('WARNING: --img-size %g must be multiple of max stride %g, updating to %g' % (img_size, s, new_size))
        return new_size  #返回能被s整除的new_size
    

    7). 检查相关路径能否找到文件, 如果不能则在全局路径中寻找,匹配到多个时返回第一个

    def check_file(file):
        # Search for file if not found
        if os.path.isfile(file) or file == '':
            return file
        else:
            files = glob.glob('./**/' + file, recursive=True)  # 寻找文件
            assert len(files), 'File Not Found: %s' % file  # assert 文件没有找到
            assert len(files) == 1, "Multiple files match '%s', specify exact path: %s" % (file, files)  # assert unique
            return files[0]  # 返回文件
    

    8). 检查数据集,如果本地没有则从torch库中下载并并解压数据集

    # dict 内容需要实际调试(暂时无法错行调试)该方法只能适用于某几个数据集
    def check_dataset(dict):
        # Download dataset if not found locally
        val, s = dict.get('val'), dict.get('download')
        if val and len(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()])
                if s and len(s):  # download script
                    print('Downloading %s ...' % s)
                    if s.startswith('http') and s.endswith('.zip'):  # URL
                        f = Path(s).name  # filename
                        torch.hub.download_url_to_file(s, f)
                        r = os.system('unzip -q %s -d ../ && rm %s' % (f, f))  # unzip
                    else:  # bash script
                        r = os.system(s)
                    print('Dataset autodownload %s\n' % ('success' if r == 0 else 'failure'))  # analyze return value
                else:
                    raise Exception('Dataset not found.')
    

    9). 取大于等于x的最小值,该值能被divisor整除

    def make_divisible(x, divisor):
        # Returns x evenly divisible by divisor
        # 如100,3 返回34; 100,5 返回20
        return math.ceil(x / divisor) * divisor
    

    10). 对字符串s里在pattern中字符替换为下划线_pattern中[]不能省

    #--对字符串s里在pattern中字符替换为下划线_pattern中[]不能省 --#
    def clean_str(s):
        # Cleans a string by replacing special characters with underscore _
        return re.sub(pattern="[|@#!¡·$€%&()=?¿^*;:,¨´><+]", repl="_", string=s)
    

    11). 定义从y1增加到y2的余弦上升函数

    def one_cycle(y1=0.0, y2=1.0, steps=100):
        # lambda function for sinusoidal ramp from y1 to y2
        return lambda x: ((1 - math.cos(x * math.pi / steps)) / 2) * (y2 - y1) + y1
    

    12). 从训练标签获得类权重

    def labels_to_class_weights(labels, nc=80):
        '''
        labels Array(M,N,5) [class x y w h] 猜测因为函数内有concatenate函数降维
        从训练标签获取得类权重
        weights最终权重为 每一类出现次数的倒数 占全部类别的百分比 (出现次数为0是认为 为1)
        weights 实际的物理意义为:次数出现越少的越重要 没有出现的最重要 对应weights最大
        '''
        # Get class weights (inverse frequency) from training labels
        if labels[0] is None:  # no labels loaded 如果labels为空 返回一个空tensor
            return torch.Tensor()
        # np.concatenate(array, 0) 为将数组array的高维元素合并 转换为低一维的元素
        labels = np.concatenate(labels, 0)  # labels.shape = (866643, 5) for COCO 
        # classess Array(1,N)代表Labels的类别
        classes = labels[:, 0].astype(np.int)  # labels = [class xywh]
        # weights Array(1,N) 从第0类到到nc类 每一类在classes中出现的次数
        weights = np.bincount(classes, minlength=nc)  # occurrences per class 每一种类别出现的次数
    
        # Prepend gridpoint count (for uCE training)
        # gpi = ((320 / 32 * np.array([1, 2, 4])) ** 2 * 3).sum()  # gridpoints per image
        # weights = np.hstack([gpi * len(labels)  - weights.sum() * 9, weights * 9]) ** 0.5  # prepend gridpoints to start
    
        weights[weights == 0] = 1  # replace empty bins with 1 将出现次数为0的类别 统一替换为1
        weights = 1 / weights  # number of targets per class   weights 该为类别出现次数的倒数
        weights /= weights.sum()  # normalize 每一个元素占总和的百分比
        # torch.from_numpy()函数从numpy数组构建tensor 两者共享内存
        return torch.from_numpy(weights) 
    

    13). 通过图像的label信息返回图像对应的权重

    def labels_to_image_weights(labels, nc=80, class_weights=np.ones(80)):
        # Produces image weights based on class_weights and image contents
        class_counts = np.array([np.bincount(x[:, 0].astype(np.int), minlength=nc) for x in labels])
        # 实际计算中class_weights 应由label_to_class_weights计算得到
        image_weights = (class_weights.reshape(1, nc) * class_counts).sum(1)
        # index = random.choices(range(n), weights=image_weights, k=1)  # weight image sample
        return image_weights
    

    14). 将80类的coco索引转换为91类的coco索引

    # --将80类的coco索引转换为91类的coco索引 x为80类中的每一类在91类中对应的位置 --##
    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
    

    15). Tensor/Array(N,4)转换成不同的坐标格式

    #--Tensor/Array(N,4)转换成不同的坐标格式 --#
    def xyxy2xywh(x):
        # Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] where xy1=top-left, xy2=bottom-right
        # xyxy转换成xywh: (x,y):中心点坐标 w:宽度 h:高
        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   xmax - xmin
        y[:, 3] = x[:, 3] - x[:, 1]  # height  ymax - ymin
        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
    

    16). 将坐标按照不同图像比例进行缩放 -> 从img1的尺寸缩放到img0的尺寸上

    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: 取长宽中的较小值 old/new 小 说明new/old 大 则gain为长或宽中扩大比例较大的那个
            gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])  # gain  = old / new
            # pad = a,b 格式等价于 pad = (a,b)
            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]     # 指定pad值
    
        coords[:, [0, 2]] -= pad[0]  # x padding
        coords[:, [1, 3]] -= pad[1]  # y padding
        coords[:, :4] /= gain
        clip_coords(coords, img0_shape) #
        return coords
    

    17). 将bounding box 坐标范围限制在图像尺寸中

    #--将bounding box 坐标范围限制在图像尺寸中 --#
    def clip_coords(boxes, img_shape):
        # Clip bounding xyxy bounding boxes to image shape (height, width)
        # torch.clamp_(min,max)函数能够将tensor的取值限制在(min,max)之间,超出这个范围自动归到这个范围中
        # 将bounding box坐标范围限制在图像尺寸中
        boxes[:, 0].clamp_(0, img_shape[1])  # x1
        boxes[:, 1].clamp_(0, img_shape[0])  # y1
        boxes[:, 2].clamp_(0, img_shape[1])  # x2
        boxes[:, 3].clamp_(0, img_shape[0])  # y2
    

    18). 返回Bbox1到Bbox2的IOU值

    --> box1 is 4, box2 is Nx4

    def bbox_iou(box1, box2, x1y1x2y2=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-9):
        # Returns the IoU of box1 to box2. box1 is 4, box2 is nx4
        box2 = box2.T
    
        # Get the coordinates of bounding boxes
        if x1y1x2y2:  # x1, y1, x2, y2 = box1
            b1_x1, b1_y1, b1_x2, b1_y2 = box1[0], box1[1], box1[2], box1[3]
            b2_x1, b2_y1, b2_x2, b2_y2 = box2[0], box2[1], box2[2], box2[3]
        else:  # transform from xywh to xyxy
            b1_x1, b1_x2 = box1[0] - box1[2] / 2, box1[0] + box1[2] / 2
            b1_y1, b1_y2 = box1[1] - box1[3] / 2, box1[1] + box1[3] / 2
            b2_x1, b2_x2 = box2[0] - box2[2] / 2, box2[0] + box2[2] / 2
            b2_y1, b2_y2 = box2[1] - box2[3] / 2, box2[1] + box2[3] / 2
    
        # Intersection area 计算相交的区域 (可用一维数轴理解计算过程)
        # 相交的区域一定是一个矩形 所以计算相交的x和y的长度 两者等价 下以x距离
        # 右下顶点的min(x,y) 左上顶点的max(x,y)
        
        # 当右下 - 左上 = 负数时, 代表两者没有交集,所以调用.clamp()函数直接将iou置为零
        inter = (torch.min(b1_x2, b2_x2) - torch.max(b1_x1, b2_x1)).clamp(0) * \
                (torch.min(b1_y2, b2_y2) - torch.max(b1_y1, b2_y1)).clamp(0)
    
        # Union Area 计算相并的区域 总面积 减去多余的相交的面积
        w1, h1 = b1_x2 - b1_x1, b1_y2 - b1_y1 + eps   # box1的宽和高
        w2, h2 = b2_x2 - b2_x1, b2_y2 - b2_y1 + eps   # box2的宽和高
        union = w1 * h1 + w2 * h2 - inter + eps       # box1的面积 + box2的面积 - 交集面积
    
        iou = inter / union   # 交/并
        # 以下是GIoU DIoU CIoU 的计算过程
        if GIoU or DIoU or CIoU:
            cw = torch.max(b1_x2, b2_x2) - torch.min(b1_x1, b2_x1)  # convex (smallest enclosing box) width
            ch = torch.max(b1_y2, b2_y2) - torch.min(b1_y1, b2_y1)  # convex height
            if CIoU or DIoU:  # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
                c2 = cw ** 2 + ch ** 2 + eps  # convex diagonal squared
                rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 +
                        (b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4  # center distance squared
                if DIoU:
                    return iou - rho2 / c2  # DIoU
                elif CIoU:  # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
                    v = (4 / math.pi ** 2) * torch.pow(torch.atan(w2 / h2) - torch.atan(w1 / h1), 2)
                    with torch.no_grad():
                        alpha = v / ((1 + eps) - iou + v)
                    return iou - (rho2 / c2 + v * alpha)  # CIoU
            else:  # GIoU https://arxiv.org/pdf/1902.09630.pdf
                c_area = cw * ch + eps  # convex area
                return iou - (c_area - union) / c_area  # GIoU
        else:
            return iou  # IoU
    

    19). 返回iou Tensor[N,M] 其中(n,m)坐标代表box1中的第n个框和box2中的第m个框的iou值

    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
        """
        #计算每个box的面积 
        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)
        # 相交面积N*M矩阵的每一个元素 为(右下-左上)取大于0 在对第三维做乘积
        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)
    

    20). 返回根据wh点确定的iou值

    #-- 返回根据wh点确定的iou值 --#
    def wh_iou(wh1, wh2):
        # Returns the nxm IoU matrix. wh1 is nx2, wh2 is mx2
        wh1 = wh1[:, None]  # [N,1,2]
        wh2 = wh2[None]  # [1,M,2]
        inter = torch.min(wh1, wh2).prod(2)  # [N,M]
        return inter / (wh1.prod(2) + wh2.prod(2) - inter)  # iou = inter / (area1 + area2 - inter)
    

    21). 该函数为极大值抑制算法(NMS) 用于筛选bounding box

    #--该函数为极大值抑制算法(NMS) 用于筛选bounding box --#
    def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, labels=()):
        """Performs Non-Maximum Suppression (NMS) on inference results
    
        Returns:
             detections with shape: nx6 (x1, y1, x2, y2, conf, cls)
        """
        # prediction是三维:包含了若干张图片
        # prediction.shape[0]代表的是图片的数目
        # prediction.shape[1]代表的是每一张图片的每一行box信息 shape[2]代表是列数
        nc = prediction.shape[2] - 5  # number of classes
        xc = prediction[..., 4] > conf_thres  # candidates 置信度 x是布尔数组
    
        # Settings
        min_wh, max_wh = 2, 4096  # (pixels) minimum and maximum box width and height box长宽的最小最大像素值
        max_det = 300  # maximum number of detections per image 每张照片检测的最大数量
        max_nms = 30000  # maximum number of boxes into torchvision.ops.nms() 在torchvision.ops.nms()中的box最大数目
        time_limit = 10.0  # seconds to quit after  几秒后退出
        redundant = True  # require redundant detections 是否需要redundant detections
        multi_label = nc > 1  # multiple labels per box (adds 0.5ms/img) 每个box有多个标签类别大于1?
        merge = False  # use merge-NMS 是否使用 merge-NMS
    
        t = time.time() # t为当前时间
        # 创建了一个大list 其中每一个元素 都是一个空的(0,6) tensor 该output最终作为结果输出
        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 根据置信度筛选了图像bounding box的信息
    
            # 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)  # 这里是将自动标注的标注 添加到x之中 一起进行NMS 
    
            # If none remain process next image
            # 如果该张图片没有坐标 则进行下一张图片
            if not x.shape[0]:
                continue
    
            # Compute conf 计算置信度 将cls_conf列置为 conf*cls的乘积
            x[:, 5:] *= x[:, 4:5]  # conf = obj_conf * cls_conf
    
            # Box (center x, center y, width, height) to (x1, y1, x2, y2)
            # 格式转换 Box (center x, center y, width, height) to (x1, y1, x2, y2)
            box = xywh2xyxy(x[:, :4])
    
            # Detections matrix nx6 (xyxy, conf, cls)
            # 建立检测矩阵 nx6 (xyxy, conf, cls)
            # tensor.cat()为tensor的拼接操作
            if multi_label:
                # torch.nonzero()函数返回非零值对应的值 以及对应的索引
                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]
    
            # 根据定义的classes 进行筛选
            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 检查shape (box的数量)
            n = x.shape[0]  # number of boxes 
            if not n:  # no boxes 如果box 的数量为0 检查下一张图片
                continue
            elif n > max_nms:  # excess boxes 超过了处理的数目 则根据置信度排列并取到max_nms张图片
                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 根据类进行整理
            # 直接调用了pytorchvision.ops.nms(boxes, scores, iou_thres)
            i = torchvision.ops.nms(boxes, scores, iou_thres)  # NMS
            if i.shape[0] > max_det:  # limit detections 限定检测数量
                i = i[:max_det]
            # Merge NMS 在hard-nms的基础上,增加保留框平滑策略(重叠框位置信息求解平均值), 使框的位置更加精确
            if merge and (1 < n < 3E3):  # Merge NMS (boxes merged using weighted mean)
                # 更新boxes: boxes(i,4) = weights(i,n) * boxes(n,4)
                # 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]
            # 如果超过了NMS的处理时间上限则进行报错
            if (time.time() - t) > time_limit:
                print(f'WARNING: NMS time limit {time_limit}s exceeded')
                break  # time limit exceeded
    
        return output
    

    22). 模型从优化器剥离以完成训练 或可保存为s

    # --将模型从优化器剥离以完成训练 或可保存为s --# 
    def strip_optimizer(f='weights/best.pt', s=''):  # from utils.general import *; strip_optimizer()
        # Strip optimizer from 'f' to finalize training, optionally save as 's'
        
        # x为加载训练的模型
        x = torch.load(f, map_location=torch.device('cpu'))
        # 以下为将 模型训练涉及到的若干个指定变量置空
        x['optimizer'] = None
        x['training_results'] = None
        x['epoch'] = -1
        x['model'].half()  # to FP16 转换为FP16精度
        for p in x['model'].parameters():
            p.requires_grad = False
        # 保存模型
        torch.save(x, s or f)
        mb = os.path.getsize(s or f) / 1E6  # filesize 获得filesize 并转换为MB
        print('Optimizer stripped from %s,%s %.1fMB' % (f, (' saved as %s,' % s) if s else '', mb))
    

    23). 打印超参数的结果在evolve.txt (使用这个功能需要 train.py --evolve)

    #--打印超参数的结果在evolve.txt (使用这个功能需要 train.py --evolve)--#
    def print_mutation(hyp, results, yaml_file='hyp_evolved.yaml', bucket=''):
        # Print mutation results to evolve.txt (for use with train.py --evolve)
        # 定义相关变量 并赋值 按指定格式输出
        a = '%10s' * len(hyp) % tuple(hyp.keys())  # hyperparam keys
        b = '%10.3g' * len(hyp) % tuple(hyp.values())  # hyperparam values
        c = '%10.4g' * len(results) % results  # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
        print('\n%s\n%s\nEvolved fitness: %s\n' % (a, b, c))
        # 如果大于本地, 则下载evolve.txt
        if bucket:
            url = 'gs://%s/evolve.txt' % bucket
            if gsutil_getsize(url) > (os.path.getsize('evolve.txt') if os.path.exists('evolve.txt') else 0):
                os.system('gsutil cp %s .' % url)  # download evolve.txt if larger than local
        # 打开ecolve.txt 并添加结果
        with open('evolve.txt', 'a') as f:  # append result
            f.write(c + b + '\n')
        x = np.unique(np.loadtxt('evolve.txt', ndmin=2), axis=0)  # load unique rows
        x = x[np.argsort(-fitness(x))]  # sort
        np.savetxt('evolve.txt', x, '%10.3g')  # save sort by fitness 根据fitness整理后的结果保存
    
        # Save yaml 保存yaml配置文件 为hyp_evolved.yaml“”
        for i, k in enumerate(hyp.keys()):
            hyp[k] = float(x[0, i + 7])
        with open(yaml_file, 'w') as f:
            results = tuple(x[0, :7])
            c = '%10.4g' * len(results) % results  # results (P, R, mAP@0.5, mAP@0.5:0.95, val_losses x 3)
            f.write('# Hyperparameter Evolution Results\n# Generations: %g\n# Metrics: ' % len(x) + c + '\n\n')
            yaml.dump(hyp, f, sort_keys=False)
    
        if bucket:
            os.system('gsutil cp evolve.txt %s gs://%s' % (yaml_file, bucket))  # upload
    

    24). 定义了一个二级分类器来处理yolo的输出

    def apply_classifier(x, model, img, im0):
        # applies a second stage classifier to yolo outputs
        im0 = [im0] if isinstance(im0, np.ndarray) else im0
        for i, d in enumerate(x):  # per image i为index d为代表每一张图片包含的信息
            if d is not None and len(d):
                d = d.clone()
    
                # Reshape and pad cutouts
                b = xyxy2xywh(d[:, :4])  # boxes b为xywh格式的bounding box
                b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1)  # rectangle to square
                b[:, 2:] = b[:, 2:] * 1.3 + 30  # pad 操作 1.3 和 30看起来像是随便加的
                d[:, :4] = xywh2xyxy(b).long()  # 将d前四列坐标格式转换成xyxy格式
    
                # Rescale boxes from img_size to im0 size
                # 将box根据图像size的不同进行rescale 该函数上文已经注释
                scale_coords(img.shape[2:], d[:, :4], im0[i].shape)
    
                # Classes
                # pred_cls1获得每一个预测值对应的类别
                pred_cls1 = d[:, 5].long()
                
                # ims汇总图片中所有子目标的区域
                ims = []
                for j, a in enumerate(d):  # per item
                    # 根据xyxy格式的标注对原始图像进行裁剪
                    cutout = im0[i][int(a[1]):int(a[3]), int(a[0]):int(a[2])]
                    # 用opencv进行resize操作
                    im = cv2.resize(cutout, (224, 224))  # BGR OpenCV图像储存方式为BGR
                    # cv2.imwrite('test%i.jpg' % j, cutout)
                    
                    # 将BGR格式的图像转换为RGB(3*416*416)
                    im = im[:, :, ::-1].transpose(2, 0, 1)  # BGR to RGB, to 3x416x416
                    # 将img从uint8格式转换为float 32 格式
                    im = np.ascontiguousarray(im, dtype=np.float32)  # uint8 to float32
                    im /= 255.0  # 0 - 255 to 0.0 - 1.0
                    ims.append(im)  # 添加裁剪后图像的信息
                # 用model模型进行预测
                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
    

    25). 递增路径

    # --递增路径如runs/exp --> runs/exp{sep}0, runs/exp{sep}1 etc --#
    # 如:将训练结果统一保存到/runs/train/exp 文件夹中
    # 如果该文件夹已经存在 则将路径修改为 runs/train/exp1
    def increment_path(path, exist_ok=True, sep=''):
        # Increment path, i.e. runs/exp --> runs/exp{sep}0, runs/exp{sep}1 etc.
        path = Path(path)  # os-agnostic
        if (path.exists() and exist_ok) or (not path.exists()):
            return str(path)
        else:
            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 递增数字
            return f"{path}{sep}{n}"  # update path 更新路径
    

    相关文章

      网友评论

          本文标题:YOLOv5-4.0版本源码解读--general.py

          本文链接:https://www.haomeiwen.com/subject/sybziltx.html