图像数据增强(自适应锚框)

相信很多小伙伴在训练卷积神经网络的时候,总是感觉自己的数据集太少了。通常能拍几百张照片已经算是很不错了,那么应该怎么扩充我们的数据集呢。方法有很多种,最直接的方法就是再多拍些照片呗(开个玩笑)。当然肯定是还有很多其他的方法的,比如爬虫呀,下载别人的数据集进行提取呀,等等。其中一种就是在已有的数据集的基础上进行数据增强。对于神经网络来说,一张图片有遮挡,没有遮挡,目标物体在左上角,在右上角,图像的清晰程度等等,都是一张需要重新学习的图像。

通常,一张图像的增强会有扭曲、翻转、色域变换、添加噪声、移动图像位置等等。那么对于目标检测来说,这里就会有一个小问题,将一张图像进行增强之后,我确实获取了一张图片,但是我之前标注的锚框没有了,我又需要重新标注了。如果我只生成了几张、几十张照片来说,这些都还是小问题啦,无非就是麻烦一点。但是如果是将原来几百张的数据集进行翻倍呢,那工作量是不是太大了。那么,有没有一种方法不需要重新标注锚框呢,直接运行程序,刷刷刷,就生成了一堆图片和锚框

方法当然是有的啦。我们对图像进行的数据增强,只有当对图像进行移动,扭曲的时候,锚框会跟着变化。那么只需要在增强的同时变换锚框的位置信息了。这条博客呢,就给出了一种数据增强,并且会自动生成锚框(.mxl文件)的方法

在运行程序前,需要生成2007_train.txt文件,文件来源可以参考(Bubbliiiing的博客_CSDN博客-神经网络学习小记录,睿智的目标检测,有趣的数据结构算法领域博主)咱们导师博客中的任意一个训练自己数据集的目标检测项目。通常是需要运行voc_annotation.py文件获得

接下来呢,是在我的数据增强文件中提到的数据增强方法,流程分别为:裁剪---添加高斯噪声---随即遮挡---对图像进行缩放并且进行长和宽的扭曲---移动图像---翻转图像---色域扭曲。以上流程,均为随机生成。以下为随机裁剪的代码:(以下代码中iw,ih分别为原始图像的大小,nw,nh为新图像的大小)

# 随机裁剪
image = np.asarray(image)
x_min, y_min = min(box[:, 0]), min(box[:, 1])  # 获取box中xy最小值和最大值
x_max, y_max = max(box[:, 2]), max(box[:, 3])
rx_min, ry_min = abs(int(rand(0, x_min))), abs(int(rand(0, y_min)))
rx_max, ry_max = abs(int(rand(0, ih - x_max))), abs(int(rand(0, iw - y_max)))
image = image[ry_min:(y_max + ry_max), rx_min: (x_max + rx_max)]
image = Image.fromarray(image)

随机裁剪需要在各个锚框的范围以外进行裁剪,避免裁剪到锚框。(这里可能会有小伙伴有疑问,裁剪到锚框,行不行呢。其实也可以,但是不好把握裁剪的范围,裁剪多了,人也很难分辨出物体的)

裁剪之后,一定要对锚框位置进行修改。这一点很容易,只需要减去,随机生成的xy的最小值(也就是裁剪后图像左上角在原来图像中的坐标),代码段:

# 随机裁剪后,改变box中的值
box[:, 0] = box[:, 0] - rx_min
box[:, 1] = box[:, 1] - ry_min
box[:, 2] = box[:, 2] - rx_min
box[:, 3] = box[:, 3] - ry_min
iw, ih = image.size  # 更新图像大小

接下来的操作都是在裁剪后的图像上进行的。下面这段代码是用来添加高斯噪声的。这里也可以使用其他噪声。代码:

# 添加高斯噪声
image = np.asarray(image)/255  # 图片灰度标准化
mean = rand()*0.5
sigma = rand()*0.5
noise = np.random.normal(mean, sigma, image.shape).astype(dtype=np.float32)  # 产生高斯噪声
output = image + noise
output = np.clip(output, 0, 1)
image = np.uint8(output*255)
image = Image.fromarray(image)

随机遮挡的代码:

# 随机遮挡
n = int(rand(0, 5))
for num in range(n):
    n_image = image.copy()
    color_r = int(rand(0, 255))
    color_g = int(rand(0, 255))
    color_b = int(rand(0, 255))
    img = Image.new('RGB', (iw, ih), (color_r, color_g, color_b))
    # i,j为遮挡块大小
    i = int(rand(0, 100))
    j = int(rand(0, 100))
    img2 = img.crop((0, 0, i, j))
    y1 = int(rand(0, iw - 180))
    y2 = int(rand(0, ih - 180))
    n_image.paste(img2, (y1, y2))
    image = n_image

接下来的操作,需要对裁剪后图像进行缩放和长和宽方向上进行扭曲,代码:

# 对图像进行缩放并且进行长和宽的扭曲
new_ar = w / h * rand(1 - jitter, 1 + jitter) / rand(1 - jitter, 1 + jitter)
scale = rand(.2, 3)
if new_ar < 1:
    nh = int(scale * h)
    nw = int(nh * new_ar)
else:
    nw = int(scale * w)
    nh = int(nw / new_ar)
if nh >= nw:
    image = image.resize((int(nw * w / nh), h), Image.BICUBIC)  # 重新设定图片的长和宽
else:
    image = image.resize((w, int(nh * h / nw)), Image.BICUBIC)  # 重新设定图片的长和宽

下面是移动图像的代码,需要注意的是移动图像时,重新生成的锚框的最小值不能小于0,最大值不能大于设定图像的大小。代码:

# 将图像多余的部分加上灰条
while True:
    dx = int(rand(0, w - nw))
    dy = int(rand(0, h - nh))
    if nh >= nw:
        xmax = max(box[:, 2]) * int(nw * w / nh) / iw + dx  # 获取新的真实框的x最大值
        ymax = max(box[:, 3]) * h / ih + dy  # 获取新的真实框的y最大值
        xmin = min(box[:, 0]) * int(nw * w / nh) / iw + dx  # 获取新的真实框的x最小值
        ymin = min(box[:, 1]) * h / ih + dy  # 获取新的真实框的y最小值
    else:
        xmax = max(box[:, 2]) * w / iw + dx  # 获取新的真实框的x最大值
        ymax = max(box[:, 3]) * int(nh * w / nw) / ih + dy  # 获取新的真实框的y最大值
        xmin = min(box[:, 0]) * w / iw + dx  # 获取新的真实框的x最小值
        ymin = min(box[:, 1]) * int(nh * w / nw) / ih + dy  # 获取新的真实框的y最小值
    if xmax <= w and ymax <= h and xmin >= 0 and ymin >= 0:
        break
new_image = Image.new('RGB', (w, h), (128, 128, 128))
new_image.paste(image, (dx, dy))
image = new_image

翻转和色域扭曲就比较简单了,两个就写到一起了。但是如果翻转了,不要忘记后面改变锚框的位置哈。代码:

# 翻转图像
flip = rand() < .5
if flip:
    image = image.transpose(Image.FLIP_LEFT_RIGHT)

# 色域扭曲
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

下面是完整代码。有几处需要修改路径,第157行(打开2007_train.txt的位置)、175行(生成.xml文件中位置信息)、210行(保存增强后图像的位置)。

from PIL import Image, ImageOps
import numpy as np
from matplotlib.colors import rgb_to_hsv, hsv_to_rgb


def rand(a=0, b=1):  # 随机生成一个数
    return np.random.rand() * (b - a) + a


def get_random_data(annotation_line, input_shape, random=True, max_boxes=20, jitter=.5, hue=.1, sat=1.5, val=1.5,
                    proc_img=True):
    '''random preprocessing for real-time data augmentation'''
    line = annotation_line.split()
    image = Image.open(line[0])  # 打开图片
    image = ImageOps.exif_transpose(image)
    img_name = line[0].split('/')
    img_name = img_name[-1].split('.')
    img_name = img_name[0]  # 获取图片名
    iw, ih = image.size  # 获取图片宽高
    h, w = input_shape  # 设定图片的大小
    box = np.array([np.array(list(map(int, box.split(',')))) for box in line[1:]])  # 图片中所有锚框值

    # 随机裁剪
    image = np.asarray(image)
    x_min, y_min = min(box[:, 0]), min(box[:, 1])  # 获取box中xy最小值和最大值
    x_max, y_max = max(box[:, 2]), max(box[:, 3])
    rx_min, ry_min = abs(int(rand(0, x_min))), abs(int(rand(0, y_min)))
    rx_max, ry_max = abs(int(rand(0, ih - x_max))), abs(int(rand(0, iw - y_max)))
    image = image[ry_min:(y_max + ry_max), rx_min: (x_max + rx_max)]
    image = Image.fromarray(image)

    # 随机裁剪后,改变box中的值
    box[:, 0] = box[:, 0] - rx_min
    box[:, 1] = box[:, 1] - ry_min
    box[:, 2] = box[:, 2] - rx_min
    box[:, 3] = box[:, 3] - ry_min
    iw, ih = image.size  # 更新图像大小

    # 添加高斯噪声
    image = np.asarray(image)/255  # 图片灰度标准化
    mean = rand()*0.5
    sigma = rand()*0.5
    noise = np.random.normal(mean, sigma, image.shape).astype(dtype=np.float32)  # 产生高斯噪声
    output = image + noise
    output = np.clip(output, 0, 1)
    image = np.uint8(output*255)
    image = Image.fromarray(image)

    # 随机遮挡
    n = int(rand(0, 5))
    for num in range(n):
        n_image = image.copy()
        color_r = int(rand(0, 255))
        color_g = int(rand(0, 255))
        color_b = int(rand(0, 255))
        img = Image.new('RGB', (iw, ih), (color_r, color_g, color_b))
        # i,j为遮挡块大小
        i = int(rand(0, 100))
        j = int(rand(0, 100))
        img2 = img.crop((0, 0, i, j))
        y1 = int(rand(0, iw - 180))
        y2 = int(rand(0, ih - 180))
        n_image.paste(img2, (y1, y2))
        image = n_image

    # 对图像进行缩放并且进行长和宽的扭曲
    new_ar = w / h * rand(1 - jitter, 1 + jitter) / rand(1 - jitter, 1 + jitter)
    scale = rand(.2, 3)
    if new_ar < 1:
        nh = int(scale * h)
        nw = int(nh * new_ar)
    else:
        nw = int(scale * w)
        nh = int(nw / new_ar)
    if nh >= nw:
        image = image.resize((int(nw * w / nh), h), Image.BICUBIC)  # 重新设定图片的长和宽
    else:
        image = image.resize((w, int(nh * h / nw)), Image.BICUBIC)  # 重新设定图片的长和宽

    # 将图像多余的部分加上灰条
    while True:
        dx = int(rand(0, w - nw))
        dy = int(rand(0, h - nh))
        if nh >= nw:
            xmax = max(box[:, 2]) * int(nw * w / nh) / iw + dx  # 获取新的真实框的x最大值
            ymax = max(box[:, 3]) * h / ih + dy  # 获取新的真实框的y最大值
            xmin = min(box[:, 0]) * int(nw * w / nh) / iw + dx  # 获取新的真实框的x最小值
            ymin = min(box[:, 1]) * h / ih + dy  # 获取新的真实框的y最小值
        else:
            xmax = max(box[:, 2]) * w / iw + dx  # 获取新的真实框的x最大值
            ymax = max(box[:, 3]) * int(nh * w / nw) / ih + dy  # 获取新的真实框的y最大值
            xmin = min(box[:, 0]) * w / iw + dx  # 获取新的真实框的x最小值
            ymin = min(box[:, 1]) * int(nh * w / nw) / ih + dy  # 获取新的真实框的y最小值
        if xmax <= w and ymax <= h and xmin >= 0 and ymin >= 0:
            break
    new_image = Image.new('RGB', (w, h), (128, 128, 128))
    new_image.paste(image, (dx, dy))
    image = new_image

    # 翻转图像
    flip = rand() < .5
    if flip:
        image = image.transpose(Image.FLIP_LEFT_RIGHT)

    # 色域扭曲
    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((len(box), 5))
    if len(box) > 0:
        np.random.shuffle(box)
        if nh >= nw:
            box[:, [0, 2]] = box[:, [0, 2]] * int(nw * w / nh) / iw + dx  # 获取新的真实框的宽
            box[:, [1, 3]] = box[:, [1, 3]] * h / ih + dy  # 获取新的真实框的高
        else:
            box[:, [0, 2]] = box[:, [0, 2]] * w / iw + dx  # 获取新的真实框的宽
            box[:, [1, 3]] = box[:, [1, 3]] * int(nh * w / nw) / 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)]  # 丢弃无效框,即长宽小于1个像素的图片
        if len(box) > max_boxes:
            box = box[:max_boxes]  # 一张图片限制20个框
        box_data[:len(box)] = box

    return image_data, box_data, img_name


def normal_(annotation_line, input_shape):
    '''random preprocessing for real-time data augmentation'''
    line = annotation_line.split()
    img_name = line[0].split('/')
    img_name = img_name[-1].split('.')
    image = Image.open(line[0])
    box = np.array([np.array(list(map(int, box.split(',')))) for box in line[1:]])
    return image, box, img_name # 返回图片+真实框+图片名字


if __name__ == "__main__":
    i = 0
    while True:
        with open("./set/2007_train.txt") as f:  # 需要修改路径
            lines = f.readlines()
        a = np.random.randint(0, len(lines))
        line = lines[a]  # 随机取一张图片的地址

        img_shape = 416  # 设置增强后图片的大小

        # 下一步,通过循环自动生成xml文件。
        image_data, box_data, img_name = get_random_data(line, [img_shape, img_shape])
        img = Image.fromarray((image_data * 255).astype(np.uint8))
        img_xml = open('./set/ann/%s.xml' % img_name)                # 原xml文件地址
        save_xml = open('./set/ann1/%s%s.xml' % (i, img_name), 'w')    # 储存xml文件地址

        xml_lines = img_xml.readlines()[:]  # 读取原xml文件中的数据
        start_index = []

        xml_lines[2] = '\t<filename>%s%s.jpg</filename>\n' % (i, img_name)
        # -------------路径需要修改--------------
        xml_lines[3] = '\t<path>E:\program/yolov4\VOCdevkit\VOC2007\JPEGImages/%s%s.jpg</path>\n' % (i, img_name)

        xml_lines[8] = '\t\t<width>%d</width>\n' % img_shape
        xml_lines[9] = '\t\t<height>%d</height>\n' % img_shape

        while '\t\t<bndbox>\n' in xml_lines:
            b = xml_lines.index('\t\t<bndbox>\n')
            start_index.append(b)
            xml_lines[b] = 'delete'

        box_data1 = []
        for j in range(len(box_data)):
                box_data1.append(box_data[j])
                for x in range(len(box_data1)):
                    size = []
                    left, top, right, bottom = box_data1[x][0:4]
                    e = '\t\t<bndbox>\n'
                    a = '\t\t\t<xmin>%s</xmin>\n' % int(left)
                    b = '\t\t\t<ymin>%s</ymin>\n' % int(top)
                    c = '\t\t\t<xmax>%s</xmax>\n' % int(right)
                    d = '\t\t\t<ymax>%s</ymax>\n' % int(bottom)
                    f = '\t\t</bndbox>\n'
                    size.append(e)
                    size.append(a)
                    size.append(b)
                    size.append(c)
                    size.append(d)
                    size.append(f)

                    xml_lines[start_index[x]: start_index[x]+6] = size[:]

        for a in xml_lines:
            save_xml.write(a)

        # 储存数据增强后的结果
        img.save('./set/img1/%s%s.jpg' % (i, img_name))  # 需要修改路径

        i += 1
        if i >= 1:  # i为生成图片数量
            break

简单放几张图,对比一下:

上面是原图和球体的位置信息,用手机拍摄的,图像比较大。下面是生成的图像和球体的位置信息(图像大小为416x416):

OK,完结撒花。如果有问题请先仔细再看一遍该博客,还是有问题的话,可以私信交流,看到必回。

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值