[翻系列]检测框的数据增强1:重看目标检测中的图像旋转
想要得到一个高性能的深度学习模型么?更多的数据将带来更多的可能性!但是很可惜的是,一般,我们只有这大千世界的一丢丢数据。
所以我们需要数据增强!手动地来扩充我们的数据,很幸运的是,数据增强在众多实验中都得到了有效的验证,成为了深度学习系统中不可或缺的一部分。
前言
数据增强成功的背后
很简单的一个原因是数据增强扩充了我们的数据集,也就是我们主动地向大千世界迈进一步。
另外我们可以从噪声和鲁棒性的角度看待数据增强,利用噪声使模型能更好地面对数据。对于相同id的图片,由于数据增强,模型看到总是略有不同。这些不同恰能看成噪声来让模型提取更泛化的特征而不是出现过拟合的情况。
左图 原始图像, 右图 增强后的图像
代码&文档
此系列中的代码我都放在下面这个链接里,可以安心使用!
https://github.com/Paperspace/
而系列的文档则可以用你的浏览器打开docs/build/html/index.html
或者点击链接
系列共有四个部分
1. Part 1: Basic Design and Horizontal Flipping2. Part 2: Scaling and Translation3. Part 3: Rotation and Shearing4. Part 4: Baking augmentation into input pipelines
将实现的方法
常用的深度学习库如torchvision
, keras
等等都提供了分类任务的数据增强。但是,对于目标检测的数据增强还是确实的,其中重点是要对检测框也进行变换,比如说左右翻转,如下图
左右变换同时改变检测框的位置
本系列主要实现以下方法
- 左右翻转(像上图所示)
- 大小变换
- 旋转
- 剪切
技术细节
实现方法将基于numpy
和OpenCV
我们将把增强方法定义为类,利用实例来实现数据增强。同时各个类的函数定义都是统一以便于你自己DIY属于自己的数据增强。
当然,我们也提供了数据增强用于sequence
的方法以及 随机和固定增强的方案。
在随机模式中,增强将随机的发生,而固定模式中则参数都是相对不变的,如旋转角度等等。
总而言之,各类模式应有尽有!任君想用!
标签形式
对于每张图片的所有标签我们以N行5列的numpy矩阵进行保存,N代表了图片中的目标个数,而5列的含义如下,图片或许能更简单地解释这个问题:
- 左上角x的坐标
- 左上角y的坐标
- 右下角x的坐标
- 右下角y的坐标
- 目标类别
标签形式图解
我清楚有许多数据集或者标注工提供了不同的数据形式,这里希望你能动动手将其转换为上述的格式。
作者将使用了梅西一记漂亮的进球作为参考,译者祝他喜欢的球队越来越好!
正文
文件组成
代码主要由两部分组成,分别是data_aug.py
和bbox_util.py
。第一个文件为主程序,第二个文件则包含了各类辅助的程序
所有代码文件都保存在data_aug
文件夹中
偷个懒,我们只考虑以循环的形式实现数据增强且一次循环使用一张图片,相信你一定能办到批量的处理。
代码可以通过以下代码快速获取哦
git clone https://github.com/Paperspace/DataAugmentationForObjectDetection
代码&解析
如果你是一名python的初学者,我建议你慢慢阅读以下的内容,因为其中包含了python中类的定义。如果你对类已经足够熟悉,我更建议你直接看代码快速地阅读。
首先我们import所有必须的库和路径,下面所有代码来自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)
由类RandomHorizontalFlip
以p
概率实现左右的翻转
首先我们来定义这个类并且初始化概率p
,在这里决定了以多大的概率进行左右翻转,在其他数据增强中则表示以多大概率旋转,变化大小等等。
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
我们的代码的文档字符串参照numpy
文档格式编写,这对于使用Sphinx来生成文档十分有用,想让你的代码让更多人使用,让文档写的更标准吧!
多说一句,__init__
方法用来定义和初始化用到的参数,而我们在__call__
声明了数据增强的逻辑。
__call__
通过实例调用时需要输入两个参数img
和bboxes
,其中img
是以OpenCV numpy array的形式保存图像的像素值,bboxes
则包含了检测框的数值。__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
这里,让我们慢下来,把各个细节都解释清楚
左右变换,也就是将图片沿着一条垂直于中心的线进行变化。
然后可以将每个角的新坐标描述为穿过图像中心的垂直线中角的镜像。 对于数学上倾斜的情况,穿过中心的垂直线将是连接原始角点和新的变换角点的线的垂直平分线。
是不是晕了?看看下面两张图
即是以图像中心点为标准进行翻转,对应了下列代码
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
到此,左右翻转的解析全部搞定。
我们利用p
来随机地发生数据增强,当然了如果你把概率p
设置为1或者去掉if判断,那就成了一个固定模式。
测试
我们来创建一个test
文件并且读取所需要的库。
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
函数把检测框画在图片一并输出来看看是否有效
这就必须使用OpenCV和numpy了
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
轻松地把检测框画了上去!接下来打印一下
plt.imshow(draw_rect(img, bboxes))
整个结果如下,先是读取到的图片
然后使用各个函数
hor_flip = RandomHorizontalFlip(1)
img, bboxes = hor_flip(img, bboxes)
plt.imshow(draw_rect(img, bboxes))
就得到了
恭喜!
说在最后
本文到这就全部结束啦,感谢阅读。在下一篇文章中,我们将讨论Scale和Translate扩充。 鉴于存在更多的参数(缩放比例和转换因子),它们不仅是更复杂的变换,而且还带来了一些我们在HorizontalFlip变换中不必处理的挑战。 一个示例是在增强之后,如果检测框的一部分在图像之外,则是否保留该检测框呢?