【图普科技】边界框的数据增强:对目标检测图像变换的再思考(一)

本文探讨了在深度学习目标检测任务中数据增强的重要性,特别是针对边界框的处理。介绍了随机和确定性水平翻转两种方法,并展示了如何在实际操作中应用这些技术。此外,还提供了一个GitHub仓库,包含所有相关代码和文档,供读者参考。
摘要由CSDN通过智能技术生成

【本文由图普科技编译】

当谈到深度学习任务的表现时,毋庸置疑,数据越多越好。然而,我们拥有的数据可能是有限的。数据增强是通过人为扩充数据集来解决数据短缺问题的一种方法。实际上,该技术已被证明非常成功,因而也成为深度学习系统的主要内容之一。

数据增强为什么有用?

 
理解数据增强工作原理的一种非常直接的方法是将其视为人为扩充数据集的一种方式。正如深度学习的其他应用一样,数据越多越好。

另一种理解数据增强的方法是将其视为我们数据集的附加噪声,尤其是在在线数据增强,或是随机增强将用于训练循环的每个数据样本时。

https://blog.paperspace.com/content/images/2018/09/vanila_aug.jpg

左:原始图像,右:增强的图像。

由于应用了随机数据增强技术,神经网络每次看到的相同图像时都会发现在某方面存在一定差异。这种差异可以看作是添加到我们的数据样本中的噪声,这些噪声将迫使神经网络学习广义特征,而不是过度拟合数据集。

 

GitHub Repo

 
本文中的所有内容和使用的数据增强库都可以在以下Github Repo中找到。

https://github.com/Paperspace/DataAugmentationForObjectDetection

文档

可以通过在浏览器中打开 docs/build/html/index.html或在此链接中找到此项目的文档

本系列包括4个部分。

     1部分:基本设计和水平翻转

     2部分:缩放和平移

     3部分:旋转和裁剪

     4部分:所有技术整合

目标检测的边界框

 
目前,许多深度学习库如torchvisionkerasgithub上的专用库为分类训练任务提供了数据增强支持。但是,目前仍然缺少对目标检测任务的数据增强支持。例如,进行分类任务时,水平翻转以增强图像将类似于上面的图像。

然而,对目标检测任务执行相同的增强技术还需要更新边界框,如下图。

https://blog.paperspace.com/content/images/2018/09/hflip.png 
 水平翻转时边界框的变化

这种数据增强方式,或者说是用于目标检测的主要数据增强技术,需要我们更新边界框,这些我们将在这篇文章中介绍。具体来说,以下是我们将要介绍的增强技术的确切列表。

  1. 水平翻转(如上图所示)
  2. 缩放和平移
     https://blog.paperspace.com/content/images/2018/09/rscale.png
  3. 旋转
     https://blog.paperspace.com/content/images/2018/09/vr.png
  4. 裁剪
     https://blog.paperspace.com/content/images/2018/09/shear.png
  5. 调整神经网络输入的尺寸

 

技术实现过程

 
我们将基于NumpyOpenCV建立我们的小型数据增强库。

我们将增强方法定义为类,可以调用类的实例来执行增强。我们将定义一种统一的方法来定义这些类,以便您也可以编写自己的数据增强方法。

我们还将定义一种数据增强方法,将以上的数据增强方法结合起来,以便应用于同一序列之中。

对于每种数据增强,我们将定义它的两个变体,一个是随机的,一个是确定性的。对于随机的变体,增强是随机发生的,而在确定性的变体中,增强的参数(如要旋转的角度)等是固定的。

 

数据增强示例:水平翻转

 
本文将概述实现数据增强的一般方法。我们还将介绍一些效用函数,这些函数将帮助我们将检测和其他一些过程可视化。那么,让我们开始吧。

 

标注存储格式

 
针对每个图像,我们将边界框标注存储在具有N行和5列的numpy数组中。在这里,N表示图像中目标的数量,而五列分别代表:

  1. 左上角的x坐标
  2. 左上角的y坐标
  3. 右下角的x坐标
  4. 右下角的y坐标
  5. 目标的类

https://blog.paperspace.com/content/images/2018/09/ann_form.png 
 存储边界框标注的格式

有很多数据集和标注工具可以将标注存储为其他格式存储,因此,如何将数据标注的存储格式转换为上述格式,我在这里就不详述了。

同时,出于示范目的,我们将使用梅西与尼日利亚队比赛时进球的图像,如上所示。

 

文件组织


我们将代码保存在2个文件中,data_aug.py bbox_util.py。第一个文件包含实现增强的代码,而第二个文件则包含辅助函数的代码。

这两个文件都将存储于 data_aug 文件夹中

我们假定你必须在训练循环中必须使用这些数据增强。我会介绍如何提取你所需的图像,并确保标注的格式正确。

但是,为了更简单明了,我们一次只会使用一张图像。您可以轻松地在训练循环时内部变换此代码,或者使用数据提取函数来扩展功能。

复制github repo文件夹中包含训练代码的文件,或者需要进行增强的文件。

git clone https://github.com/Paperspace/DataAugmentationForObjectDetection

 

随机水平翻转


首先,我们必须导入所有必要的东西,并确保添加了路径,即使我们是从包含文件的文件夹外部调用的函数。将以下代码添加到文件 data_aug.py

import random
import numpy as np
import cv2
import matplotlib.pyplot as plt
import sys
import os

lib_path = os.path.join(os.path.realpath("."), "data_aug")
sys.path.append(lib_path)

 

此处的数据增强是通过随机水平翻转方法来实现的以一定的概率p水平翻转图像

我们首先使用类的 __ init __ 方法。init方法包含增强参数。对于水平翻转增强方法,参数指的是每个图像被翻转的概率。对于像旋转这样的其他增强方法,参数可以是目标旋转的角度等。

class RandomHorizontalFlip(object):
    
    """Randomly horizontally flips the Image with the probability *p*
    
    Parameters
    ----------
    p: float
        The probability with which the image is flipped
        
        
    Returns
    -------
    
    numpy.ndaaray
        Flipped image in the numpy format of shape `HxWxC`
    
    numpy.ndarray
        Tranformed bounding box co-ordinates of the format `n x 4` where n is 
        number of bounding boxes and 4 represents `x1,y1,x2,y2` of the box
        
    """

    def __init__(self, p=0.5):
        self.p = p

函数的docstringNumpy docstring格式编写。这对于使用Sphinx生成文档很有用。

每个函数的 __ init __ 方法用于定义增强方法的所有参数。但是,增强方法的实际逻辑是在 __ call __ 函数中定义。

当从类实例调用时,call函数有两个参数,imgbboxes。其中img是包含像素值的OpenCV numpy数组,bboxes 是包含边界框标注的numpy数组。

 __ call __ 函数也返回相同的参数,这有助于我们将一系列增强方法链接在一起以应用于序列中。

def __call__(self, img, bboxes):
        img_center = np.array(img.shape[:2])[::-1]/2
        img_center = np.hstack((img_center, img_center))
        if random.random() < self.p:
            img =  img[:,::-1,:]
            bboxes[:,[0,2]] += 2*(img_center[[0,2]] - bboxes[:,[0,2]])
            
            box_w = abs(bboxes[:,0] - bboxes[:,2])
             
            bboxes[:,0] -= box_w
            bboxes[:,2] += box_w
            
        return img, bboxes

 

让我们一点一点地剖析这部分内容。

在水平翻转中,我们围绕穿过其中心的垂直线旋转图像。

然后可以将每个角的新坐标描述为穿过图像中心的垂直线中对应角的镜像。从数学的角度来讲,穿过中心的垂直线将是原始角和新的、变换过的角的连接线的垂直平分线。

要更好地了解这一过程,请观察以下图像。变换图像的右半部分的像素和原始图像的左半部分的像素是彼此关于中心线的镜像。

https://blog.paperspace.com/content/images/2018/09/path7907-2.png 
 上述内容是通过以下代码完成的。

img_center = np.array(img.shape[:2])[::-1]/2
img_center = np.hstack((img_center, img_center))
if random.random() < self.p:
   img =  img[:,::-1,:]
   bboxes[:,[0,2]] += 2*(img_center[[0,2]] - bboxes[:,[0,2]])

 

注意,img = img[:,::-1,:] 这一行基本上采用包含图像的数组并反转第一维中的元素,或者说是存储像素值x坐标的维数。

但是,必须注意左上角的镜像是更新后边界框的右上角。实际上,结果坐标是边界框的右上角和左下角坐标。但是,我们需要的是原始图像的左上角和右下角格式。

https://blog.paperspace.com/content/images/2018/09/cordi_gyan.png 
 我们的代码的副作用

以下代码负责实现这种转换。

box_w = abs(bboxes[:,0] - bboxes[:,2])
bboxes[:,0] -= box_w
bboxes[:,2] += box_w

 

我们最终返回图像和包含边界框的数组。

确定性的水平翻转


上面的代码以概率p 随机地实现了图像变换。但是,如果我们想构建一个确定性的图像变换,我们只需简单地将传递给参数 p 的值设置1。或者我们可以另外定义一个类,该类根本不包含参数 p ,并根据下面代码实现 __ call __ 函数。

 
def __call__(self, img, bboxes):
        img_center = np.array(img.shape[:2])[::-1]/2
        img_center = np.hstack((img_center, img_center))
        
        img =  img[:,::-1,:]
        bboxes[:,[0,2]] += 2*(img_center[[0,2]] - bboxes[:,[0,2]])
        
        box_w = abs(bboxes[:,0] - bboxes[:,2])
         
        bboxes[:,0] -= box_w
        bboxes[:,2] += box_w
        
    return img, bboxes

 

实际操作表现


现在让我们假定您必须使用水平翻转方法来实现图像变换。我们将在一张图片上使用它,但您可以在任何数量的图像上使用它。首先,我们创建一个文件 test.py 。我们首先导入所有需要的东西:

 
from data_aug.data_aug import *
import cv2 
import pickle as pkl
import numpy as np 
import matplotlib.pyplot as plt

 

然后,我们导入图像并加载标注:

img = cv2.imread("messi.jpg")[:,:,::-1] #OpenCV uses BGR channels
bboxes = pkl.load(open("messi_ann.pkl", "rb"))

#print(bboxes)   #visual inspection

 

为了看看我们的增强方法是否真的有效我们定义了一个辅助函数 draw_rect 它接收 img bboxes 并返回一个numpy图像数组在该图像上绘制有边界框。

让我们创建一个文件 bbox_utils.py 并导入需要的东西。

 
import cv2 
import numpy as np

 

现在我们定义函数  draw_rect

def draw_rect(im, cords, color = None):
    """Draw the rectangle on the image
    
    Parameters
    ----------
    
    im : numpy.ndarray
        numpy image 
    
    cords: numpy.ndarray
        Numpy array containing bounding boxes of shape `N X 4` where N is the 
        number of bounding boxes and the bounding boxes are represented in the
        format `x1 y1 x2 y2`
        
    Returns
    -------
    
    numpy.ndarray
        numpy image with bounding boxes drawn on it
        
    """
    
    im = im.copy()
    
    cords = cords.reshape(-1,4)
    if not color:
        color = [255,255,255]
    for cord in cords:
        
        pt1, pt2 = (cord[0], cord[1]) , (cord[2], cord[3])
                
        pt1 = int(pt1[0]), int(pt1[1])
        pt2 = int(pt2[0]), int(pt2[1])
    
        im = cv2.rectangle(im.copy(), pt1, pt2, color, int(max(im.shape[:2])/200))
return im

 

完成后让我们回到 test.py 文件并绘制原始边界框。

plt.imshow(draw_rect(img, bboxes))

结果如下所示:

https://blog.paperspace.com/content/images/2018/09/Vanilla.jpg 
 让我们看看我们变换的效果。

hor_flip = RandomHorizontalFlip(1)  
img, bboxes = hor_flip(img, bboxes)
plt.imshow(draw_rect(img, bboxes))

 

你应该得到以下类似图像:

https://blog.paperspace.com/content/images/2018/09/hori.jpg

总结

  • 边界框标注应存储在大小为N x 5numpy数组中,其中N是目标的数量,每个框由具有5个属性的行表示;  左上角的坐标,右下角的坐标和目标的类。
  • 每种数据增强方法都定义为一个类,其中 __ init __ 方法用于定义增强参数,而 __ call __ 方法描述增强方法的实际逻辑。它需要两个参数,图像 img 和边界框标注 bboxes 并返回转换后的值。

这是本文的内容。在下一篇文章中我们将讨论缩放和平移增强方法。考虑到这些方法有更多参数(缩放和平移因子),它们的变换不仅更加复杂,同时还带来了一些我们在水平翻转变换中无需处理的挑战,例如在增强之后如果边界框的部分在图像之外,那么是否需保留该边界框呢?

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值