yolo学习

一、letterbox机制

该机制背景是:保持原尺寸的比例

在深度学习中,模型的输入size通常是正方形尺寸的,比如300 x 300这样.直接resize的话,会把图像拉的变形.通常我们希望resize以后仍然保持图片的宽高比.

 我这里给出参考链接:

opencv resize图片为正方形尺寸 - core! - 博客园 (cnblogs.com)

python 图像等比例缩放_giganticpower的博客-CSDN博客_pytorch 图像缩放

等比例缩放c++ opencv 实现 - 知乎 (zhihu.com)

深度学习图像预处理 保持原尺寸比例_老光头_ME2CS的博客-CSDN博客

数据集预处理:图像等比例缩放并填充_我是大黄同学呀的博客-CSDN博客_图片等比例填充

 例子代码1:

def ResziePadding(img, fixed_side=128):	

        h, w = img.shape[0], img.shape[1]
        scale = max(w, h)/float(fixed_side)   # 获取缩放比例
        new_w, new_h = int(w/scale), int(h/scale)
        resize_img = cv2.resize(img, (new_w, new_h))    # 按比例缩放
        
        # 计算需要填充的像素长度
        if new_w % 2 != 0 and new_h % 2 == 0:
            top, bottom, left, right = (fixed_side - new_h) // 2, (fixed_side - new_h) // 2, (fixed_side - new_w) // 2 + 1, (
                fixed_side - new_w) // 2
        elif new_w % 2 == 0 and new_h % 2 != 0:
            top, bottom, left, right = (fixed_side - new_h) // 2 + 1, (fixed_side - new_h) // 2, (fixed_side - new_w) // 2, (
                fixed_side - new_w) // 2
        elif new_w % 2 == 0 and new_h % 2 == 0:
            top, bottom, left, right = (fixed_side - new_h) // 2, (fixed_side - new_h) // 2, (fixed_side - new_w) // 2, (
                fixed_side - new_w) // 2
        else:
            top, bottom, left, right = (fixed_side - new_h) // 2 + 1, (fixed_side - new_h) // 2, (fixed_side - new_w) // 2 + 1, (
                fixed_side - new_w) // 2

        # 填充图像
        pad_img = cv2.copyMakeBorder(resize_img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=[0, 0, 0])
        
        return pad_img

再给出一个链接:

【图片resize】图片三种缩放方式/letterbox_image实现_寻找永不遗憾的博客-CSDN博客_图片的resize

这个链接整体说的蛮好的,就是有一处不太严谨如下:

yolo学习_第1张图片

 这个作者应该说明清楚,yolov5在训练时(非rect情况下)采用的还是“不变形缩放,两端填充灰边114”,而在推理阶段时,为了提速采用的是“填充最少的灰度边,不变形缩放,一端填充灰度边”,这点可以从yolov5的源码中看到:

def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32):
    # Resize and pad image while meeting stride-multiple constraints
    shape = im.shape[:2]  # current shape [height, width]
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)

    # Scale ratio (new / old)
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
    if not scaleup:  # only scale down, do not scale up (for better val mAP)
        r = min(r, 1.0)

    # Compute padding
    ratio = r, r  # width, height ratios
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]  # wh padding
    if auto:  # minimum rectangle
        dw, dh = np.mod(dw, stride), np.mod(dh, stride)  # wh padding
    elif scaleFill:  # stretch
        dw, dh = 0.0, 0.0
        new_unpad = (new_shape[1], new_shape[0])
        ratio = new_shape[1] / shape[1], new_shape[0] / shape[0]  # width, height ratios

    dw /= 2  # divide padding into 2 sides
    dh /= 2

    if shape[::-1] != new_unpad:  # resize
        im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR)
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)  # add border
    return im, ratio, (dw, dh)

yolo学习_第2张图片

 从中也可以看出:当auto为true,为minimum rectangle时,也并不是单独一端填充灰色,而是两端填充,只是此时是“minimum rectangle”,这样就不用填充成训练时的那种正方形(如2016*2016),只用变为矩形(如1216*2016)。这样就达到了推理加速的目的!

最后给出步骤:

  • 等比例缩放,然后用灰色边缘填充
    step1: 计算宽高缩放比例,选择较小的那个缩放系数;
    step2: 计算缩放后的尺寸: 原始图片的长宽都乘以较小的缩放系数;
    step3:计算短边需要填充的灰边数,将短边的两边各自填充一半的灰行即可。

import matplotlib.pyplot as plt
from PIL import Image

# ------------------------------------------------------------------------#
#   对输入图像进行resize,他人测试发现,不用letterbox_image直接resize的效果更好
# ------------------------------------------------------------------------#
def resize_image(image, size, letterbox_image):
    iw, ih  = image.size
    w, h    = size      # w=200, h=300
    if letterbox_image:
        scale   = min(w/iw, h/ih)
        nw      = int(iw*scale)
        nh      = int(ih*scale)

        image   = image.resize((nw,nh), Image.BICUBIC)
        # 新建一张image,第二个参数表示尺寸,第三个参数表示颜色
        new_image = Image.new('RGB', size, (128,128,128))       
        
        # --------------------------------------------------------------#
        # 	不变形resize,两端填充灰边
        #   image.paste函数表示将一张图片覆盖到另一张图片的指定位置去
        #   a.paste(b, (50,50))   将b的左上顶点贴到a的坐标为(50,50)的位置,
        #	左上顶点为(0,0), b超出a的部分会被自动舍弃
        # --------------------------------------------------------------#
        # new_image.paste(image, ((w-nw)//2, (h-nh)//2))   
        
        # ---------------------------------------------------#
        # 	不变形resize,一端填充灰边
        # ---------------------------------------------------#
        new_image.paste(image, (0, 0))      
    else:
        new_image = image.resize((w, h), Image.BICUBIC)
    return new_image


img_PIL = Image.open("Avatar.jpg")
# ---------------------------------------------------#
# 第二参数表示目标尺寸,第三参数表示是否使用letterbox
# ---------------------------------------------------#
img = resize_image(img_PIL, (200, 300), True)   

plt.imshow(img)
plt.show()

二、yolo的数据集cache机制

(1)

支持的数据格式
yolov5支持直接将图片或者视频的数据转换到dataloader当中去.
文件类型包括:
‘bmp’, ‘jpg’, ‘jpeg’, ‘png’, ‘tif’, ‘tiff’, ‘dng’, ‘webp’, ‘mpo’
‘mov’, ‘avi’, ‘mp4’, ‘mpg’, ‘mpeg’, ‘m4v’, ‘wmv’, ‘mkv’

快速加载数据集标签的技巧
通常情况下,目标检测数据集都会非常大,同时,yolo采用的是txt格式保存的标签数据,那么加载数据可能会很慢,对于一个10万级别的数据集,可能花费1小时左右的时间,如果多次训练此数据集,显然是一笔很大的时间开销.因此,yolov5用来一种相当巧妙的方式,只会在第一次读取数据集的时候,将标签文件处理成一个cache文件,这样,再重新读取数据集的时候,直接搜索数据集中是否存在cache文件即可。
同时,相对于原始的txt标签文件,cache还可以保存很多的额外信息,比如说,数据集的大小,未找到标签文件对应的图片数量…
当然,细心的人会发现,如果使用一个损坏的cache文件存放到数据集中,yolov5会不会读取异常? 答案是不会,因为对于每一个数据集的cache文件中,会生成一个Hash Code,这个code是通过数据集的大小计算得到的,因此在读取cache之前会检查Hash Code是否匹配

在detect实际场景中数据加载的技巧
yolov5为我们提供了一个很完善的数据加载解决方案,它蕴含这很多的技巧,同时,核心代码数量只在1000行左右,便于使用者阅读.
对于train和test数据的加载,那么显然是需要从数据集中加载得到.yolov5提供了一个create_dataloader函数,可以在其参数中指定使用哪些加载技巧.然后只需要拿到加载数据集的迭代器就可以了
在检测阶段,如果你需要检测某一个文件夹中所有的图片,或者是从摄像头里面拉取视屏流,亦或者是从http协议中拉取视频流,yolov5都提供了一整套解决方案.相比于其他检测器的代码,你会发现,yolov5对于实际场景的检测做了很多的支持,不需要我们再单独部署模型.同时如果我们想将其部署到C++中,也可以参考其加载数据的代码技巧

参考:Yolov5系列(4)-dataloader模块

yolo学习_第3张图片

 从这个图中也可以看到:try中的两个assert,不满足这两个assert,那么就会跳入except中。

步骤是:先判断数据集有无对应的cache文件路径存在(上图前两行),

然后在try中的两个assert分别是判断当cache文件存在时,cache的版本是否是6.0的版本;以及在读取cache之前会检查Hash Code是否匹配。如果两个assert中有一个不成立,则进入except中,重新生成标签文件的cache。

 def cache_labels(self, path=Path('./labels.cache'), prefix=''):
        # Cache dataset labels, check images and read shapes
        x = {}  # dict
        nm, nf, ne, nc, msgs = 0, 0, 0, 0, []  # number missing, found, empty, corrupt, messages
        desc = f"{prefix}Scanning '{path.parent / path.stem}' images and labels..."
        with Pool(NUM_THREADS) as pool:
            pbar = tqdm(pool.imap(verify_image_label, zip(self.img_files, self.label_files, repeat(prefix))),
                        desc=desc, total=len(self.img_files))
            for im_file, l, shape, segments, nm_f, nf_f, ne_f, nc_f, msg in pbar:
                nm += nm_f
                nf += nf_f
                ne += ne_f
                nc += nc_f
                if im_file:
                    x[im_file] = [l, shape, segments]
                if msg:
                    msgs.append(msg)
                pbar.desc = f"{desc}{nf} found, {nm} missing, {ne} empty, {nc} corrupted"

        pbar.close()
        if msgs:
            logging.info('\n'.join(msgs))
        if nf == 0:
            logging.info(f'{prefix}WARNING: No labels found in {path}. See {HELP_URL}')
        x['hash'] = get_hash(self.label_files + self.img_files)
        x['results'] = nf, nm, ne, nc, len(self.img_files)
        x['msgs'] = msgs  # warnings
        x['version'] = self.cache_version  # cache version
        try:
            np.save(path, x)  # save cache for next time
            path.with_suffix('.cache.npy').rename(path)  # remove .npy suffix
            logging.info(f'{prefix}New cache created: {path}')
        except Exception as e:
            logging.info(f'{prefix}WARNING: Cache directory {path.parent} is not writeable: {e}')  # path not writeable
        return x

注:上图中有些代码的语法我觉得很有必要记录一下。

语法一:

 with Pool(NUM_THREADS) as pool:
            pbar = tqdm(pool.imap(verify_image_label, zip(self.img_files, self.label_files, repeat(prefix))),
                        desc=desc, total=len(self.img_files))

这个是两个函数:pool.imap()以及tqdm()嵌套的。我们先看pool.imap

进程池Pool的imap方法解析 - 简书 (jianshu.com)

 pool.imap_梦否的博客-CSDN博客_pool.imap

yolo学习_第4张图片

 首先明白了imap返回是一个iter(迭代器),其次看出使用with方式,方便在不用imap进程时自动退出,最后我们明白repeat(prefix)就是chunksize。

再看tqdm:

python进度条库tqdm详解 - 知乎 (zhihu.com)

yolo学习_第5张图片

 我们明白了:

iterable: 可迭代的对象,这里就是imap的返回的迭代器

desc:字符串, 左边进度条描述文字,在v5代码中即:desc = f"{prefix}Scanning '{path.parent /                                                                                            path.stem}' images and labels..."

total:总的项目数,在这里是len(self.img_files),即训练或者验证图像的总数目

(2)

此外,这里我对下面代码中的zip(self.img_files, self.label_files, repeat(prefix))有必要解释说明一下

 with Pool(NUM_THREADS) as pool:
            pbar = tqdm(pool.imap(verify_image_label, zip(self.img_files, self.label_files, repeat(prefix))),
                        desc=desc, total=len(self.img_files))

具体地,我们可以看到zip(self.img_files, self.label_files, repeat(prefix))这里的zip在python3.x中是打包为一个元组的列表对象,可以通过*zip进行解压,获取二维矩阵式(链接)

最为关键的是这个repeat(prefix)我一直没搞明白,那我们先看prefix来源:

yolo学习_第6张图片

我画了一个追踪路径如下:

yolo学习_第7张图片

我在下面代码中打印一下:

yolo学习_第8张图片

结果显示:

yolo学习_第9张图片

 我看到这个结果我是一脸蒙圈的,看不懂这个repeat(prefix)的结果。

print("repeat(prefix): ", repeat(prefix))

repeat(prefix):  repeat('\x1b[34m\x1b[1mtrain: \x1b[0m')

因此才有了前面的追朔这个结果的由来:

yolo学习_第10张图片

echo命令_Linux echo命令:显示文字并给文字添加颜色 (biancheng.net)

 明显\033是八进制的,那么如何转为16进制呢,根据以下链接:

八进制转换成二进制_百度知道 (baidu.com)

 八进制转十进制| 8进制转10进制 | 在线进制转换 (sojson.com)

二进制与十六进制在线转换器,在线计算,在线计算器,计算器在线计算 (osgeo.cn)

二进制转十六进制算法(举例)_百度知道 (baidu.com)

二进制数与十六进制数之间如何互相转换-百度经验 (baidu.com)

 二进制、八进制、十进制、十六进制的互相转换 - 活着的虫子 - 博客园 (cnblogs.com)

8进制和16进制怎么转换_百度知道 (baidu.com)

因此这里就可以看到\033是八进制的,那么转为16进制就是\x1b

yolo学习_第11张图片

因此

''.join(colors[x] for x in args) + f'{string}' + colors['end']

结果就是:

 '\x1b[34m\x1b[1mtrain: \x1b[0m'

三、anchor聚类

(1)

好了,现在先让我们回到v5的自动聚类上面,

yolo学习_第12张图片

上面说明:当bpr小于0.98的时候才会重新计算anchor。

那么就进去看看如何计算的anchor:kmean_anchors(...)

def kmean_anchors(dataset='./data/coco128.yaml',
                  n=9,
                  img_size=640,
                  thr=4.0,
                  gen=1000,
                  verbose=True):
    """ Creates kmeans-evolved anchors from training dataset

        Arguments:
            dataset: path to data.yaml, or a loaded dataset
            n: number of anchors
            img_size: image size used for training
            thr: anchor-label wh ratio threshold hyperparameter hyp['anchor_t'] used for training, default=4.0
            gen: generations to evolve anchors using genetic algorithm
            verbose: print all results

        Return:
            k: kmeans evolved anchors

        Usage:
            from utils.autoanchor import *; _ = kmean_anchors()
    """
    from scipy.cluster.vq import kmeans

    thr = 1. / thr
    prefix = colorstr('autoanchor: ')

    def metric(k, wh):  # compute metrics
        r = wh[:, None] / k[None]
        x = torch.min(r, 1. / r).min(2)[0]  # ratio metric
        # x = wh_iou(wh, torch.tensor(k))  # iou metric
        return x, x.max(1)[0]  # x, best_x

    def anchor_fitness(k):  # mutation fitness
        _, best = metric(torch.tensor(k, dtype=torch.float32), wh)
        return (best * (best > thr).float()).mean()  # fitness

    def print_results(k):
        k = k[np.argsort(k.prod(1))]  # sort small to large
        x, best = metric(k, wh0)
        bpr, aat = (best > thr).float().mean(), (
            x > thr).float().mean() * n  # best possible recall, anch > thr
        print(
            f'{prefix}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr'
        )
        print(
            f'{prefix}n={n}, img_size={img_size}, metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best, '
            f'past_thr={x[x > thr].mean():.3f}-mean: ',
            end='')
        for i, x in enumerate(k):
            print('%i,%i' % (round(x[0]), round(x[1])),
                  end=',  ' if i < len(k) - 1 else '\n')  # use in *.cfg
        return k

    if isinstance(dataset, str):  # *.yaml file
        with open(dataset, errors='ignore') as f:
            data_dict = yaml.safe_load(f)  # model dict
        from utils.datasets import LoadImagesAndLabels
        dataset = LoadImagesAndLabels(data_dict['train'],
                                      augment=True,
                                      rect=True)

    # Get label wh
    print(dataset.shapes)
    shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)
    wh0 = np.concatenate(
        [l[:, 3:5] * s for s, l in zip(shapes, dataset.labels)])  # wh

    # Filter,即过滤
    i = (wh0 < 3.0).any(1).sum()  #统计有多少个面积w或者h长度小于3个像素的
    if i:
        print(
            f'{prefix}WARNING: Extremely small objects found. {i} of {len(wh0)} labels are < 3 pixels in size.'
        )
    wh = wh0[(wh0 >= 2.0).any(1)]  # filter > 2 pixels,过滤大于2个像素的
    # wh = wh * (np.random.rand(wh.shape[0], 1) * 0.9 + 0.1)  # multiply by random scale 0-1

    # Kmeans calculation
    print(f'{prefix}Running kmeans for {n} anchors on {len(wh)} points...')
    s = wh.std(0)  # sigmas for whitening
    k, dist = kmeans(wh / s, n, iter=30)  # points, mean distance
    assert len(
        k
    ) == n, f'{prefix}ERROR: scipy.cluster.vq.kmeans requested {n} points but returned only {len(k)}'
    k *= s
    wh = torch.tensor(wh, dtype=torch.float32)  # filtered
    wh0 = torch.tensor(wh0, dtype=torch.float32)  # unfiltered
    k = print_results(k)

    # Plot
    # k, d = [None] * 20, [None] * 20
    # for i in tqdm(range(1, 21)):
    #     k[i-1], d[i-1] = kmeans(wh / s, i)  # points, mean distance
    # fig, ax = plt.subplots(1, 2, figsize=(14, 7), tight_layout=True)
    # ax = ax.ravel()
    # ax[0].plot(np.arange(1, 21), np.array(d) ** 2, marker='.')
    # fig, ax = plt.subplots(1, 2, figsize=(14, 7))  # plot wh
    # ax[0].hist(wh[wh[:, 0]<100, 0],400)
    # ax[1].hist(wh[wh[:, 1]<100, 1],400)
    # fig.savefig('wh.png', dpi=200)

    # Evolve
    npr = np.random
    f, sh, mp, s = anchor_fitness(
        k), k.shape, 0.9, 0.1  # fitness, generations, mutation prob, sigma
    pbar = tqdm(range(gen),
                desc=f'{prefix}Evolving anchors with Genetic Algorithm:'
                )  # progress bar
    for _ in pbar:
        v = np.ones(sh)
        while (v == 1
               ).all():  # mutate until a change occurs (prevent duplicates)
            v = ((npr.random(sh) < mp) * random.random() * npr.randn(*sh) * s +
                 1).clip(0.3, 3.0)
        kg = (k.copy() * v).clip(min=2.0)
        fg = anchor_fitness(kg)
        if fg > f:
            f, k = fg, kg.copy()
            pbar.desc = f'{prefix}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}'
            if verbose:
                print_results(k)

    return print_results(k)

yolo学习_第13张图片

 那我们可以看到一个关键的类的实例对象dataset,

我们通过调试,发现如下dataset的属性如下:

yolo学习_第14张图片

 yolo学习_第15张图片

 yolo学习_第16张图片

 yolo学习_第17张图片

 yolo学习_第18张图片

yolo学习_第19张图片

yolo学习_第20张图片

我们发现信息如下:

信息1):dataset的labels为(nums, 5),比如(7668, 5)。可以看到分别是yolo格式,即归一化后的(类别id,cx,cy, w, h)

dataset的shapes为(nums, 2),比如(7668, 2)

信息2):我们看到有的居然是空的,我自己猜测是它是按照图片路径加载的相应的标签文件,而有的ok图是没有对应标签的,因此自然就是对应的标签numpy array就是空的。

为了想探究这些空的是怎么来的,因此要向上追朔dataset了,首先我们看到dataset是哪来的:

yolo学习_第21张图片

因此我们再进入LoadImagesAndLabels这个类中:

yolo学习_第22张图片

我们主要关注这个cache_label函数:

def cache_labels(self, path=Path('./labels.cache'), prefix=''):
        # Cache dataset labels, check images and read shapes
        x = {}  # dict
        nm, nf, ne, nc, msgs = 0, 0, 0, 0, [
        ]  # number missing, found, empty, corrupt, messages
        desc = f"{prefix}Scanning '{path.parent / path.stem}' images and labels..."
        with Pool(NUM_THREADS) as pool:
            a = repeat(prefix)
            print("repeat(prefix): ", a)
            pbar = tqdm(pool.imap(
                verify_image_label,
                zip(self.img_files, self.label_files, repeat(prefix))),
                        desc=desc,
                        total=len(self.img_files))
            for im_file, l, shape, segments, nm_f, nf_f, ne_f, nc_f, msg in pbar:
                nm += nm_f
                nf += nf_f
                ne += ne_f
                nc += nc_f
                if im_file:
                    x[im_file] = [l, shape, segments]
                if msg:
                    msgs.append(msg)
                pbar.desc = f"{desc}{nf} found, {nm} missing, {ne} empty, {nc} corrupted"

        pbar.close()
        if msgs:
            logging.info('\n'.join(msgs))
        if nf == 0:
            logging.info(
                f'{prefix}WARNING: No labels found in {path}. See {HELP_URL}')
        x['hash'] = get_hash(self.label_files + self.img_files)
        x['results'] = nf, nm, ne, nc, len(self.img_files)
        x['msgs'] = msgs  # warnings
        x['version'] = self.cache_version  # cache version
        try:
            np.save(path, x)  # save cache for next time
            path.with_suffix('.cache.npy').rename(path)  # remove .npy suffix
            logging.info(f'{prefix}New cache created: {path}')
        except Exception as e:
            logging.info(
                f'{prefix}WARNING: Cache directory {path.parent} is not writeable: {e}'
            )  # path not writeable
        return x

yolo学习_第23张图片

接下来我们要再进入verify_image_label函数看看:

def verify_image_label(args):
    # Verify one image-label pair
    im_file, lb_file, prefix = args
    nm, nf, ne, nc, msg, segments = 0, 0, 0, 0, '', [
    ]  # number (missing, found, empty, corrupt), message, segments
    try:
        # verify images
        im = Image.open(im_file)
        im.verify()  # PIL verify
        shape = exif_size(im)  # image size
        assert (shape[0] > 9) & (shape[1] >
                                 9), f'image size {shape} <10 pixels'
        assert im.format.lower(
        ) in IMG_FORMATS, f'invalid image format {im.format}'
        if im.format.lower() in ('jpg', 'jpeg'):
            with open(im_file, 'rb') as f:
                f.seek(-2, 2)
                if f.read() != b'\xff\xd9':  # corrupt JPEG
                    Image.open(im_file).save(im_file,
                                             format='JPEG',
                                             subsampling=0,
                                             quality=100)  # re-save image
                    msg = f'{prefix}WARNING: {im_file}: corrupt JPEG restored and saved'

        # verify labels
        if os.path.isfile(lb_file):
            nf = 1  # label found
            with open(lb_file, 'r') as f:
                l = [
                    x.split() for x in f.read().strip().splitlines() if len(x)
                ]
                if any([len(x) > 8 for x in l]):  # is segment
                    classes = np.array([x[0] for x in l], dtype=np.float32)
                    segments = [
                        np.array(x[1:], dtype=np.float32).reshape(-1, 2)
                        for x in l
                    ]  # (cls, xy1...)
                    l = np.concatenate(
                        (classes.reshape(-1, 1), segments2boxes(segments)),
                        1)  # (cls, xywh)
                l = np.array(l, dtype=np.float32)
            nl = len(l)
            if nl:
                assert l.shape[
                    1] == 5, f'labels require 5 columns, {l.shape[1]} columns detected'
                assert (l >= 0).all(), f'negative label values {l[l < 0]}'
                assert (l[:, 1:] <= 1).all(
                ), f'non-normalized or out of bounds coordinates {l[:, 1:][l[:, 1:] > 1]}'
                l = np.unique(l, axis=0)  # remove duplicate rows
                if len(l) < nl:
                    segments = np.unique(segments, axis=0)
                    msg = f'{prefix}WARNING: {im_file}: {nl - len(l)} duplicate labels removed'
            else:
                ne = 1  # label empty
                l = np.zeros((0, 5), dtype=np.float32)
        else:
            nm = 1  # label missing
            l = np.zeros((0, 5), dtype=np.float32)
        return im_file, l, shape, segments, nm, nf, ne, nc, msg
    except Exception as e:
        nc = 1
        msg = f'{prefix}WARNING: {im_file}: ignoring corrupt image/label: {e}'
        return [None, None, None, None, nm, nf, ne, nc, msg]

yolo学习_第24张图片

这里就可以看到了上面提到的空标签(空的numpy array)是从这里来的:

l = np.zeros((0, 5), dtype=np.float32)

此外这里也看出来了,pool.imap返回的pbar这个迭代器,每次迭代返回的是:

 for im_file, l, shape, segments, nm_f, nf_f, ne_f, nc_f, msg in pbar:

yolo学习_第25张图片

yolo学习_第26张图片

yolo学习_第27张图片

这样也就明白了!

yolo学习_第28张图片

 yolo学习_第29张图片

 

从而就可以在autoanchor.py文件中看到:

shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)

是怎么计算的:

dataset.shapes的size为:(7668,2),即有7668张图,2列特征(分别为归一化的w和h),

那么dataset.shapes.max(1, keepdims=True)表示的是按列(即轴1)进行求取一行中两列之间的最大值,且保持维度,因此dataset.shapes.max(1, keepdims=True)的如果的size为:

(7668,1)而不是(7668)。

举列子:

dataset.shapes

([[1189,2000],
  [1189,2000],
  [1200,2000], 
])

dataset.shapes.max(1, keepdims=True)的结果为:

([[2000],
  [2000],
  [2000], 
])

那假如dataset.shapes.max(1, keepdims=False),那么其结果为:

([2000,2000,2000])

那么回过头来看shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)

表示啥意思呢?

答:这个其实就是letterbox中的等比例缩放。

我们推导理一下:假设现在我们原始图像宽高为1189*2000,而我们训练入口大小为:2016*2016

1)那么按照letterbox步骤2,其等比缩放后的宽高计算步骤:

ratio1 = 2016/1189

ratio2 = 2016/2000

选择较小的:ratio2 = 2016/2000

new_w = 1189 * ratio2 = 1189 * (2016/2000) = 1198.5

new_h = 2000*ratio2 = 2000 * (2016*2000) = 2016.0

2)那么按照shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)计算过程为:

dataset.shapes:(1189,2000)

img_size : (2016, 2016)

dataset.shapes.max(1, keepdims=True)的结果为(2000)

dataset.shapes / dataset.shapes.max(1, keepdims=True) = 1189/ 2000, 2000/2000

img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True) = 2016 * (1189/2000), 

2016*(2000/2000) = 1189 * (2016/2000), 2000 * (2016*2000) = 1189.5, 2016.0

从上面的1)和2)明显可以看到:二者计算结果是相同的,验证了:

shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)就是letterbox中的等比例缩放!

(4)接下来就可以分析以下图中的代码了:

yolo学习_第30张图片

实际是在进行聚类之前做的一些预处理:

具体地:

1)第一:

wh0 = np.concatenate([l[:, 3:5] * s for s, l in zip(shapes, dataset.labels)])  # wh

取labels(classid,cx,cy, w,  h)中的w和h出来,然后分别乘上上面经过:

shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)这个等比例缩放后的结果,也就是要将原来在1189*2000图上归一化的w和h,现在映射到2016*2016的图上,且不进行归一化的w和h:

从下面的图中就可以看到都不是归一化后的那种小数点:

yolo学习_第31张图片

 

2)第二:

# Filter,即过滤
    i = (wh0 < 3.0).any(1).sum()  #统计有多少个面积w或者h长度小于3个像素的
    if i:
        print(
            f'{prefix}WARNING: Extremely small objects found. {i} of {len(wh0)} labels are < 3 pixels in size.'
        )
    wh = wh0[(wh0 >= 2.0).any(1)]  # filter > 2 pixels,过滤大于2个像素的

这两句话其实都是用于过滤(Filter)作用的,

i = (wh0 < 3.0).any(1).sum()  #统计有多少个面积w或者h长度小于3个像素的

wh = wh0[(wh0 >= 2.0).any(1)]  # filter > 2 pixels,过滤大于2个像素的

也就是当一个gt框box的宽或者高任意有小于3个像素长度的,就统计出来,打印出来给使用者warning告知

只取一个gt框box的宽或者高任意大于或等于2个像素长度的,才进入,否则小于2个像素的直接被过滤掉,不会放入后续的聚类中!

3)第三:

 

# Kmeans calculation
    print(f'{prefix}Running kmeans for {n} anchors on {len(wh)} points...')
    s = wh.std(0)  # sigmas for whitening
    k, dist = kmeans(wh / s, n, iter=30)  # points, mean distance

这步就是k均值聚类,可以看到yolov5的作者还是使用的欧式距离作为距离度量值。并没有使用d = 1-iou这种方式!

对每一个数据做一个标准差归一化处理(除以标准差)。scipy.cluster.vq.kmeans() 函数输入的数据就是必须是白化后的数据。相应的输出的anchor k也是白化后的anchor,所以需要将anchor k 都乘以标准差恢复。如下:

yolo学习_第32张图片

 

参考:

(1条消息) YOLOV5 修改K-means聚类方法并生成anchors_Lucky dog123的博客-CSDN博客_yolov5 聚类

(1条消息) 使用k-means聚类anchors_太阳花的小绿豆的博客-CSDN博客_anchor kmeans聚类

(1条消息) 【YOLOV5-5.x 源码解读】autoanchor.py_满船清梦压星河HK的博客-CSDN博客 

你可能感兴趣的:(yolov3,YOLOV5,pytroch,学习,yolo)