码迷,mamicode.com
首页 > 其他好文 > 详细

探索 YOLO v3 源码 - 第4篇 真值

时间:2020-12-19 11:36:43      阅读:1      评论:0      收藏:0      [点我收藏+]

标签:ica   tps   target   assert   nbsp   get   位置   numpy   nump   

YOLO,即You Only Look Once的缩写,是一个基于卷积神经网络(CNN)的物体检测算法。而YOLO v3是YOLO的第3个版本(即YOLO、YOLO 9000、YOLO v3),检测效果,更准更强。

YOLO v3的更多细节,可以参考YOLO的官网。

技术图片

YOLO是一句美国的俗语,You Only Live Once,你只能活一次,即人生苦短,及时行乐。

本文主要分享,如何实现YOLO v3的算法细节,Keras框架。这是第4篇,数据和y_true,其中包含随机生成图片数据,和设置真值y_true,技巧性非常强??。当然还有第5篇,至第n篇,毕竟,这是一个完整版 :)。

本文的GitHub源码:https://github.com/SpikeKing/keras-yolo3-detection

已更新:

第1篇 训练

https://mp.weixin.qq.com/s/T9LshbXoervdJDBuP564dQ

第2篇 模型

https://mp.weixin.qq.com/s/N79S9Qf1OgKsQ0VU5QvuHg

第3篇 网络

https://mp.weixin.qq.com/s/hC4P7iRGv5JSvvPe-ri_8g

欢迎关注,微信公众号 深度算法 (ID: DeepAlgorithm) ,了解更多深度技术!


1. fit_generator

在训练中,模型调用fit_generator方法,按批次创建数据,输入模型,进行训练。其中,数据生成器wrapper是data_generator_wrapper,用于验证数据格式,最终调用data_generator,输入参数是:

  • annotation_lines:标注数据的行,每行数据包含图片路径,和框的位置信息;

  • batch_size:批次数,每批生成的数据个数;

  • input_shape:图像输入尺寸,如(416, 416);

  • anchors:anchor box列表,9个宽高值;

  • num_classes:类别的数量;

在data_generator_wrapper中,验证输入参数是否正确,再调用data_generator,这也是wrapper函数的常见用法。

实现:

data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes)

def data_generator_wrapper(annotation_lines, batch_size, input_shape, anchors, num_classes):    n = len(annotation_lines)  # 标注图片的行数    if n == 0 or batch_size <= 0: return None    return data_generator(annotation_lines, batch_size, input_shape, anchors, num_classes)    
def data_generator(annotation_lines, batch_size, input_shape, anchors, num_classes):

2. 数据生成器

在数据生成器data_generator中,数据的总行数是n,循环输出固定批次数batch_size的图片数据image_data和标注框数据box_data。

在第0次时,将数据洗牌shuffle,调用get_random_data解析annotation_lines[i],生成图片image和标注框box,添加至各自的列表image_data和box_data中。

索引值递增i+1,当完成n个一轮之后,重新将i置0,再次调用shuffle洗牌数据。

将image_data和box_data都转换为np数组,其中:

image_data: (16, 416, 416, 3)
box_data: (16, 20, 5) # 每个图片最多含有20个框

接着,将框的数据box_data、输入图片尺寸input_shape、anchor box列表anchors和类别数num_classes转换为真值y_true,其中y_true是3个预测特征的列表:

[(16, 13, 13, 3, 6), (16, 26, 26, 3, 6), (16, 52, 52, 3, 6)]

最终输出:图片数据image_data、真值y_true、每个图片的损失值np.zeros。不断循环while True,生成的批次数据,与epoch步数相同,即steps_per_epoch。

实现如下:

def data_generator(annotation_lines, batch_size, input_shape, anchors, num_classes):
    ‘‘‘data generator for fit_generator‘‘‘
    n = len(annotation_lines)
    i = 0
    while True:
        image_data = []
        box_data = []
        for b in range(batch_size):
            if i == 0:
                np.random.shuffle(annotation_lines)
            image, box = get_random_data(annotation_lines[i], input_shape, random=True)  # 获取图片和框
            image_data.append(image)  # 添加图片
            box_data.append(box)  # 添加框
            i = (i + 1) % n
        image_data = np.array(image_data)
        box_data = np.array(box_data)
        y_true = preprocess_true_boxes(box_data, input_shape, anchors, num_classes)  # 真值
        yield [image_data] + y_true, np.zeros(batch_size)

3. 图片和标注框

在get_random_data中,分离图片image和标注框box,输入:

  • 数据annotation_line:图片地址和框的位置类别;

  • 图片尺寸input_shape:如(416, 416);

  • 数据random:随机开关;

方法如下:

image, box = get_random_data(annotation_lines[i], input_shape, random=True)

def get_random_data(        annotation_line, input_shape, random=True,        max_boxes=20, jitter=.3, hue=.1, sat=1.5,        val=1.5, proc_img=True):

第1步,解析annotation_line数据:

  • 将annotation_line按空格分割为line列表;

  • 使用PIL读取图片image;

  • 图片的宽和高,iw和ih;

  • 输入尺寸的高和宽,h和w;

  • 图片中的标注框box,box是5维,4个点和1个类别;

实现:

line = annotation_line.split()
image = Image.open(line[0])
iw, ih = image.size
h, w = input_shape
box = np.array([np.array(list(map(int, box.split(‘,‘)))) for box in line[1:]])

第2步,如果是非随机,即if not random:

  • 将图片等比例转换为416x416的图片,其余用灰色填充,即(128, 128, 128),同时颜色值转换为0~1之间,即每个颜色值除以255;

  • 将边界框box等比例缩小,再加上填充的偏移量dx和dy,因为新的图片部分用灰色填充,影响box的坐标系,box最多有max_boxes个,即20个。

实现:

scale = min(float(w) / float(iw), float(h) / float(ih))
nw = int(iw * scale)
nh = int(ih * scale)
dx = (w - nw) // 2
dy = (h - nh) // 2
image_data = 0
if proc_img:  # 图片    image = image.resize((nw, nh), Image.BICUBIC)    new_image = Image.new(‘RGB‘, (w, h), (128, 128, 128))    new_image.paste(image, (dx, dy))    image_data = np.array(new_image) / 255.

# 标注框
box_data = np.zeros((max_boxes, 5))
if len(box) > 0:    np.random.shuffle(box)    if len(box) > max_boxes: box = box[:max_boxes]  # 最多只取20个    box[:, [0, 2]] = box[:, [0, 2]] * scale + dx    box[:, [1, 3]] = box[:, [1, 3]] * scale + dy    box_data[:len(box)] = box

return image_data, box_data

第3步,如果是随机:

通过jitter参数,随机计算new_ar和scale,生成新的nh和nw,将原始图像随机转换为nw和nh尺寸的图像,即非等比例变换图像。

实现:

new_ar = w / h * rand(1 - jitter, 1 + jitter) / rand(1 - jitter, 1 + jitter)
scale = rand(.25, 2.)
if new_ar < 1:    nh = int(scale * h)    nw = int(nh * new_ar)
else:    nw = int(scale * w)    nh = int(nw / new_ar)
image = image.resize((nw, nh), Image.BICUBIC)

将变换后的图像,转换为416x416的图像,其余部分用灰色值填充。

实现:

dx = int(rand(0, w - nw))
dy = int(rand(0, h - nh))
new_image = Image.new(‘RGB‘, (w, h), (128, 128, 128))
new_image.paste(image, (dx, dy))
image = new_image

根据随机数flip,随机左右翻转FLIP_LEFT_RIGHT图片。

实现:

flip = rand() < .5
if flip: image = image.transpose(Image.FLIP_LEFT_RIGHT)

在HSV坐标域中,改变图片的颜色范围,hue值相加,sat和vat相乘,先由RGB转为HSV,再由HSV转为RGB,添加若干错误判断,避免范围过大。

实现:

hue = rand(-hue, hue)
sat = rand(1, sat) if rand() < .5 else 1 / rand(1, sat)
val = rand(1, val) if rand() < .5 else 1 / rand(1, val)
x = rgb_to_hsv(np.array(image) / 255.)
x[..., 0] += hue
x[..., 0][x[..., 0] > 1] -= 1
x[..., 0][x[..., 0] < 0] += 1
x[..., 1] *= sat
x[..., 2] *= val
x[x > 1] = 1
x[x < 0] = 0
image_data = hsv_to_rgb(x)  # numpy array, 0 to 1

将所有的图片变换,增加至检测框中,并且包含若干异常处理,避免变换之后的值过大或过小,去除异常的box。

实现:

box_data = np.zeros((max_boxes, 5))
if len(box) > 0:    np.random.shuffle(box)    box[:, [0, 2]] = box[:, [0, 2]] * nw / iw + dx    box[:, [1, 3]] = box[:, [1, 3]] * nh / ih + dy    if flip: box[:, [0, 2]] = w - box[:, [2, 0]]    box[:, 0:2][box[:, 0:2] < 0] = 0    box[:, 2][box[:, 2] > w] = w    box[:, 3][box[:, 3] > h] = h    box_w = box[:, 2] - box[:, 0]    box_h = box[:, 3] - box[:, 1]    box = box[np.logical_and(box_w > 1, box_h > 1)]  # discard invalid box    if len(box) > max_boxes: box = box[:max_boxes]    box_data[:len(box)] = box

最终,返回图像数据image_data和边框数据box_data。box的4个值是(xmin, ymin, xmax, ymax),第5位不变,是标注框的类别,如0~n。


4. 真值y_true

在preprocess_true_boxes中,输入:

  • true_boxes:检测框,批次数16,最大框数20,每个框5个值,4个边界点和1个类别序号,如(16, 20, 5);

  • input_shape:图片尺寸,如(416, 416);

  • anchors:anchor box列表;

  • num_classes:类别的数量;

如:

def preprocess_true_boxes(true_boxes, input_shape, anchors, num_classes):

检测类别序号是否小于类别数,避免异常数据,如:

assert (true_boxes[..., 4] < num_classes).all(), ‘class id must be less than num_classes‘

每一层anchor box的数量num_layers;预设anchor box的掩码anchor_mask,第1层678,第2层345,第3层012,倒序排列。

实现:

num_layers = len(anchors) // 3  # default setting
anchor_mask = [[6, 7, 8], [3, 4, 5], [0, 1, 2]] if num_layers == 3 else [[3, 4, 5], [1, 2, 3]]

计算true_boxes:

  • true_boxes:真值框,左上和右下2个坐标值和1个类别,如[184, 299, 191, 310, 0.0],结构是(16, 20, 5),16是批次数,20是框的最大数,5是框的5个值;

  • boxes_xy:xy是box的中心点,结构是(16, 20, 2);

  • boxes_wh:wh是box的宽和高,结构也是(16, 20, 2);

  • input_shape:输入尺寸416x416;

true_boxes:第0和1位设置为xy,除以416,归一化,第2和3位设置为wh,除以416,归一化,如[0.449, 0.730, 0.016, 0.026, 0.0]。

实现:

true_boxes = np.array(true_boxes, dtype=‘float32‘)
input_shape = np.array(input_shape, dtype=‘int32‘)
boxes_xy = (true_boxes[..., 0:2] + true_boxes[..., 2:4]) // 2
boxes_wh = true_boxes[..., 2:4] - true_boxes[..., 0:2]
true_boxes[..., 0:2] = boxes_xy / input_shape[::-1]
true_boxes[..., 2:4] = boxes_wh / input_shape[::-1]

设置y_true的初始值:

  • m是批次16;

  • grid_shape是input_shape等比例降低,即[[13,13], [26,26], [52,52]];

  • y_true是全0矩阵(np.zeros)列表,即[(16,13,13,3,6), (16,26,26,3,6), (16,52,52,3,6)]

实现:

m = true_boxes.shape[0]
grid_shapes = [input_shape // {0: 32, 1: 16, 2: 8}[l] for l in range(num_layers)]
y_true = [np.zeros((m, grid_shapes[l][0], grid_shapes[l][1], len(anchor_mask[l]), 5 + num_classes),                   dtype=‘float32‘) for l in range(num_layers)]

设置anchors的值:

  • 将anchors增加1维expand_dims,由(9,2)转为(1,9,2);

  • anchor_maxes,是anchors值除以2;

  • anchor_mins,是负的anchor_maxes;

  • valid_mask,将boxes_wh中宽w大于0的位,设为True,即含有box,结构是(16,20);

valid_mask:

技术图片

实现:

anchors = np.expand_dims(anchors, 0)
anchor_maxes = anchors / 2.
anchor_mins = -anchor_maxes
valid_mask = boxes_wh[..., 0] > 0

循环m处理批次中的每个图像和标注框:

  • 只选择存在标注框的wh,例如:wh的shape是(7,2);

  • np.expand_dims(wh, -2)是wh倒数第2个添加1位,即(7,2)->(7,1,2);

  • box_maxes和box_mins,与anchor_maxes和anchor_mins的操作类似。

实现:

for b in range(m):
    # Discard zero rows.
    wh = boxes_wh[b, valid_mask[b]]
    if len(wh) == 0: continue
    # Expand dim to apply broadcasting.
    wh = np.expand_dims(wh, -2)
    box_maxes = wh / 2.
    box_mins = -box_maxes

计算标注框box与anchor box的iou值,计算方式很巧妙:

  • box_mins的shape是(7,1,2),anchor_mins的shape是(1,9,2),intersect_mins的shape是(7,9,2),即两两组合的值;

  • intersect_area的shape是(7,9);box_area的shape是(7,1);anchor_area的shape是(1,9);

  • iou的shape是(7,9);

IoU数据,即anchor box与检测框box,两两匹配的iou值。

技术图片

实现:

intersect_mins = np.maximum(box_mins, anchor_mins)
intersect_maxes = np.minimum(box_maxes, anchor_maxes)
intersect_wh = np.maximum(intersect_maxes - intersect_mins, 0.)
intersect_area = intersect_wh[..., 0] * intersect_wh[..., 1]
box_area = wh[..., 0] * wh[..., 1]
anchor_area = anchors[..., 0] * anchors[..., 1]
iou = intersect_area / (box_area + anchor_area - intersect_area)

接着,选择IoU最大的anchor索引,即:

best_anchor = np.argmax(iou, axis=-1)

设置y_true的值:

  • t是box的序号;n是最优anchor的序号;l是层号;

  • 如果最优anchor在层l中,则设置其中的值,否则默认为0;

  • true_boxes是(16, 20, 5),即批次、box数、框值;

  • true_boxes[b, t, 0],其中b是批次序号、t是box序号,第0位是x,第1位是y;

  • grid_shapes是3个检测图的尺寸,将归一化的值,与框长宽相乘,恢复为具体值;

  • k是在anchor box中的序号;

  • c是类别,true_boxes的第4位;

  • 将xy和wh放入y_true中,将y_true的第4位框的置信度设为1,第5~n位的类别设为1;

 

实现:

 

for t, n in enumerate(best_anchor):
    for l in range(num_layers):
        if n in anchor_mask[l]:
            i = np.floor(true_boxes[b, t, 0] * grid_shapes[l][1]).astype(‘int32‘)
            j = np.floor(true_boxes[b, t, 1] * grid_shapes[l][0]).astype(‘int32‘)
            k = anchor_mask[l].index(n)
            c = true_boxes[b, t, 4].astype(‘int32‘)
            y_true[l][b, j, i, k, 0:4] = true_boxes[b, t, 0:4]
            y_true[l][b, j, i, k, 4] = 1
            y_true[l][b, j, i, k, 5 + c] = 1

y_true的第0和1位是中心点xy,范围是(0~13/26/52),第2和3位是宽高wh,范围是0~1,第4位是置信度1或0,第5~n位是类别为1其余为0。


补充1. 矩阵相加

NumPy支持不同维度的矩阵相加,如(1, 2) + (2, 1) = (2, 2),如:

import numpy as np

a = np.array([[1, 2]])
print(a.shape)  # (1, 2)
b = np.array([[1], [2]])
print(b.shape)  # (2, 1)
c = a + b
print(c.shape)  # (2, 2)
print(c)
"""[[2 3] [3 4]]"""

OK, that‘s all! Enjoy it!

By C. L. Wang @ 美图 视觉技术部

探索 YOLO v3 源码 - 第4篇 真值

标签:ica   tps   target   assert   nbsp   get   位置   numpy   nump   

原文地址:https://www.cnblogs.com/shuimuqingyang/p/14132271.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!