文章目录
- 前言
- 数据增强
- 1.Mosaic(马赛克增强)
- 2.MixUp(混合增强)
- 3.Letterbox(自适应缩放技术)
- 4.Random perspective(随机透视变换)
- 5.Albumentations模块
- 5.HSV变换
- 6.Flip(翻转)
- 7.分割填补
- 8.Rectangular技术
前言
本来是为了对使用了yolov5模型的项目数据集做数据增强,但在学习的过程中发现yolov5本身具有特别多的数据增强方法,现在就来学习一下!
由于数据增强方式太多,刚开始只要记住哪种数据增强方式对应哪种效果就好
数据增强
主要是按照utils/dataloaders.py
里面的LoadImagesAndLabels()
所用到的数据增强方式,进行学习。
1.Mosaic(马赛克增强)
YOLOv5最引人注目的增强技术之一是马赛克增强,它将四张不同的图像拼接成一张图像。
思路:首先,从数据集中随机选择四张图像,然后将它们缩放、随机裁剪,并按马赛克模式拼接在一起。这种方式允许模型看到多尺度的目标,并且增强了目标的背景多样性。
步骤:
- 初始化整个背景图, 大小为(2 × image_size, 2 × image_size, 3)
- 随机取一个中心点
- 基于中心点分别将4个图放到左上,右上,左下,右下,,此部分可能会由于中心点小于4张图片的宽高
- 所以拼接的时候可能会进行裁剪重新将打标边框的偏移量计算上
什么时候启用mosaic增强?
mosaic为True表示启用mosaic数据增强,hyp[‘mosaic’]控制mosaic的概率。
# `utils/dataloaders.py`里面的`LoadImagesAndLabels`类def __getitem__(self, index):index = self.indices[index] # linear, shuffled, or image_weightshyp = self.hypmosaic = self.mosaic and random.random() < hyp['mosaic']if mosaic:# Load mosaicimg, labels = self.load_mosaic(index)shapes = None# MixUp augmentationif random.random() < hyp['mixup']:img, labels = mixup(img, labels, *self.load_mosaic(random.randint(0, self.n - 1)))
具体实现可以在utils/dataloaders.py
里面的LoadImagesAndLabels
类里面的load_mosaic()
中看到:
# 代码位置:utils/datasets.pydef load_mosaic(self, index):"""用在LoadImagesAndLabels模块的__getitem__函数 进行mosaic数据增强将四张图片拼接在一张马赛克图像中 loads images in a 4-mosaic:param index: 需要获取的图像索引:return: img4: mosaic和随机透视变换后的一张图片 numpy(640, 640, 3)labels4: img4对应的target [M, cls+x1y1x2y2]"""# labels4: 用于存放拼接图像(4张图拼成一张)的label信息(不包含segments多边形)# segments4: 用于存放拼接图像(4张图拼成一张)的label信息(包含segments多边形)labels4, segments4 = [], []s = self.img_size # 一般的图片大小# 随机初始化拼接图像的中心点坐标 [0, s*2]之间随机取2个数作为拼接图像的中心坐标yc, xc = (int(random.uniform(-x, 2 * s + x)) for x in self.mosaic_border) # mosaic center x, y# 从dataset中随机寻找额外的三张图像进行拼接 [14, 26, 2, 16] 再随机选三张图片的indexindices = [index] + random.choices(self.indices, k=3) # 3 additional image indicesrandom.shuffle(indices)# 遍历四张图像进行拼接 4张不同大小的图像 => 1张[1472, 1472, 3]的图像for i, index in enumerate(indices):# load image 每次拿一张图片 并将这张图片resize到self.size(h,w)img, _, (h, w) = self.load_image(index)# place img in img4if i == 0: # top left 原图[375, 500, 3] load_image->[552, 736, 3] hwc# 创建马赛克图像 [1472, 1472, 3]=[h, w, c]img4 = np.full((s * 2, s * 2, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles# 计算马赛克图像中的坐标信息(将图像填充到马赛克图像中) w=736 h = 552 马赛克图像:(x1a,y1a)左上角 (x2a,y2a)右下角x1a, y1a, x2a, y2a = max(xc - w, 0), max(yc - h, 0), xc, yc # xmin, ymin, xmax, ymax (large image)# 计算截取的图像区域信息(以xc,yc为第一张图像的右下角坐标填充到马赛克图像中,丢弃越界的区域) 图像:(x1b,y1b)左上角 (x2b,y2b)右下角x1b, y1b, x2b, y2b = w - (x2a - x1a), h - (y2a - y1a), w, h # xmin, ymin, xmax, ymax (small image)elif i == 1: # top right# 计算马赛克图像中的坐标信息(将图像填充到马赛克图像中)x1a, y1a, x2a, y2a = xc, max(yc - h, 0), min(xc + w, s * 2), yc# 计算截取的图像区域信息(以xc,yc为第二张图像的左下角坐标填充到马赛克图像中,丢弃越界的区域)x1b, y1b, x2b, y2b = 0, h - (y2a - y1a), min(w, x2a - x1a), helif i == 2: # bottom left# 计算马赛克图像中的坐标信息(将图像填充到马赛克图像中)x1a, y1a, x2a, y2a = max(xc - w, 0), yc, xc, min(s * 2, yc + h)# 计算截取的图像区域信息(以xc,yc为第三张图像的右上角坐标填充到马赛克图像中,丢弃越界的区域)x1b, y1b, x2b, y2b = w - (x2a - x1a), 0, w, min(y2a - y1a, h)elif i == 3: # bottom right# 计算马赛克图像中的坐标信息(将图像填充到马赛克图像中)x1a, y1a, x2a, y2a = xc, yc, min(xc + w, s * 2), min(s * 2, yc + h)# 计算截取的图像区域信息(以xc,yc为第四张图像的左上角坐标填充到马赛克图像中,丢弃越界的区域)x1b, y1b, x2b, y2b = 0, 0, min(w, x2a - x1a), min(y2a - y1a, h)# 将截取的图像区域填充到马赛克图像的相应位置 img4[h, w, c]# 将图像img的【(x1b,y1b)左上角 (x2b,y2b)右下角】区域截取出来填充到马赛克图像的【(x1a,y1a)左上角 (x2a,y2a)右下角】区域img4[y1a:y2a, x1a:x2a] = img[y1b:y2b, x1b:x2b] # img4[ymin:ymax, xmin:xmax]# 计算pad(当前图像边界与马赛克边界的距离,越界的情况padw/padh为负值) 用于后面的label映射padw = x1a - x1b # 当前图像与马赛克图像在w维度上相差多少padh = y1a - y1b # 当前图像与马赛克图像在h维度上相差多少# labels: 获取对应拼接图像的所有正常label信息(如果有segments多边形会被转化为矩形label)# segments: 获取对应拼接图像的所有不正常label信息(包含segments多边形也包含正常gt)# 在新图中更新坐标值labels, segments = self.labels[index].copy(), self.segments[index].copy()if labels.size:labels[:, 1:] = xywhn2xyxy(labels[:, 1:], w, h, padw, padh) # normalized xywh to pixel xyxy formatsegments = [xyn2xy(x, w, h, padw, padh) for x in segments]labels4.append(labels) # 更新labels4segments4.extend(segments) # 更新segments4# Concat/clip labels4 把labels4([(2, 5), (1, 5), (3, 5), (1, 5)] => (7, 5))压缩到一起labels4 = np.concatenate(labels4, 0)# 防止越界 label[:, 1:]中的所有元素的值(位置信息)必须在[0, 2*s]之间,小于0就令其等于0,大于2*s就等于2*s out: 返回for x in (labels4[:, 1:], *segments4):np.clip(x, 0, 2 * s, out=x) # clip when using random_perspective()# img4, labels4 = replicate(img4, labels4) # replicate
2.MixUp(混合增强)
调整两张图像的透明度叠加在一起。
什么时候启用MixUp增强?
mosaic为True表示启用mosaic数据增强,hyp[‘mosaic’]控制mosaic的概率。
#`utils/dataloaders.py`里面的`LoadImagesAndLabels`类def __getitem__(self, index):index = self.indices[index] # linear, shuffled, or image_weightshyp = self.hypmosaic = self.mosaic and random.random() < hyp['mosaic']if mosaic:# Load mosaicimg, labels = self.load_mosaic(index)shapes = None# MixUp augmentationif random.random() < hyp['mixup']:img, labels = mixup(img, labels, *self.load_mosaic(random.randint(0, self.n - 1)))
yolov5是在mosaic图像上应用mixup增强的,具体实现见:
# 被调用函数地址:utils/augmentations.py
def mixup(im, labels, im2, labels2):# Applies MixUp augmentation https://arxiv.org/pdf/1710.09412.pdfr = np.random.beta(32.0, 32.0) # mixup ratio, alpha=beta=32.0im = (im * r + im2 * (1 - r)).astype(np.uint8)labels = np.concatenate((labels, labels2), 0)return im, labels
3.Letterbox(自适应缩放技术)
自适应图片缩放letterbox 保持图片的宽高比例,剩下的部分用灰色填充。
自适应缩放技术Letterbox流程:
- 第一步:计算缩放比例。当原图的长宽不同时,将需要的尺寸大小除以原图的长宽,获得两种缩放比,选择较小的值作为缩放比例,因此图中选择的缩放比例为0.52。
- 第二步:分别计算缩放后的图像的长宽,原图的长宽分别乘以缩放比例,此时获得大小为 416×312。
- 第三步:计算填充的灰色像素。将需要的尺寸大小减去缩放后的短边大小,得到的值再采用 numpy 库中 np.mod 函数对 32倍取余数的方式计算,然后通过平分得到对称两边需要填充的灰色像素。之所以用 32 取余,是因为 YOLOv5s 的网络需要对图像进行 5次两倍下采样。
什么时候启用Letterbox增强?
当不启用mosaic增强时就启用Letterbox增强
#`utils/dataloaders.py`里面的`LoadImagesAndLabels`类def __getitem__(self, index):...if mosaic:...else:...# Letterbox# 根据self.rect参数判断是否使用矩形的Letterbox处理,选择最终的Letterbox形状。shape = self.batch_shapes[self.batch[index]] if self.rect else self.img_size # final letterboxed shape# 调用letterbox()函数对图像进行Letterbox处理,将图像调整到指定的形状,auto=False表示不自动调整,scaleup=self.augment表示是否放大图像。img, ratio, pad = letterbox(img, shape, auto=False, scaleup=self.augment)#保存原始图像的高宽和Letterbox处理后的相关信息,用于COCO mAP的重新缩放。shapes = (h0, w0), ((h / h0, w / w0), pad) # for COCO mAP rescaling# 将标签坐标相应地调整到适应Letterbox后的图像上labels = self.labels[index].copy()if labels.size: # normalized xywh to pixel xyxy formatlabels[:, 1:] = xywhn2xyxy(labels[:, 1:], ratio[0] * w, ratio[1] * h, padw=pad[0], padh=pad[1])
4.Random perspective(随机透视变换)
Random perspective是对mosaic整合后的图片进行随机旋转、缩放、平移、裁剪,透视变换。
如果augment为True,就将此技术用在Letterbox之后
#`utils/dataloaders.py`里面的`LoadImagesAndLabels`类def __getitem__(self, index):...if mosaic:...else:...# Letterbox...if self.augment:img, labels = random_perspective(img,labels,degrees=hyp['degrees'],translate=hyp['translate'],scale=hyp['scale'],shear=hyp['shear'],perspective=hyp['perspective'])
具体实现如下:
# utils/augments.py
def random_perspective(img, targets=(),segments=(),degrees=10,translate=0.1,scale=0.1,shear=10,perspective=0.0,border=(0, 0),
):"""这个函数会用于load_mosaic中用在mosaic操作之后随机透视变换 对mosaic整合后的图片进行随机旋转、缩放、平移、裁剪,透视变换,并resize为输入大小img_size:params img: mosaic整合后的图片img4 [2*img_size, 2*img_size]如果mosaic后的图片没有一个多边形标签 segments为空 如果有一个多边形标签则 segments不为空。:params targets: mosaic整合后图片的所有正常label标签labels4(不正常的会通过segments2boxes将多边形标签转化为正常标签) [N, cls+xyxy]:params segments: mosaic整合后图片的所有不正常label信息(包含segments多边形也包含正常gt) [m, x1y1....]:params degrees: 旋转和缩放矩阵参数:params translate: 平移矩阵参数:params scale: 缩放矩阵参数:params shear: 剪切矩阵参数:params perspective: 透视变换参数:params border: 用于确定最后输出的图片大小 一般等于[-img_size//2, -img_size//2] 那么最后输出的图片大小为 [img_size, img_size]:return img: 通过透视变换/仿射变换后的img [img_size, img_size] :return targets: 通过透视变换/仿射变换后的img对应的标签 [n, cls+x1y1x2y2] (通过筛选后的)OpenCV中的坐标系定义,如下图所示:(0,0)o_________width______________x| |height || || || |y____________________________o(w,h)"""# 设定输出图片的 H W# border=-s // 2 所以最后图片的大小直接减半 [img_size, img_size, 3]# 图片高宽(加上border边框)height = img.shape[0] + border[0] * 2 # # 最终输出图像的Hwidth = img.shape[1] + border[1] * 2 # 最终输出图像的W# ============================ 开始变换 =============================# 需要注意的是,其实opencv是实现了仿射变换的, 不过我们要先生成仿射变换矩阵M# Center 计算中心点C = np.eye(3) # 生成3*3的对角为1的对角矩阵# x 方向的中心C[0, 2] = -img.shape[1] / 2 # x translation (pixels)# y 方向的中心C[1, 2] = -img.shape[0] / 2 # y translation (pixels)# Perspective 设置透视变换矩阵P = np.eye(3) # 生成3*3的对角为1的对角矩阵# 随机生成x,y方向上的透视值P[2, 0] = random.uniform(-perspective, perspective) # x perspective (about y)P[2, 1] = random.uniform(-perspective, perspective) # y perspective (about x)# Rotation and Scale # 旋转和缩放R = np.eye(3) # 初始化R = [[1,0,0], [0,1,0], [0,0,1]] (3, 3)# a: 随机生成旋转角度 范围在(-degrees, degrees)# a += random.choice([-180, -90, 0, 90]) # add 90deg rotations to small rotationsa = random.uniform(-degrees, degrees)# a += random.choice([-180, -90, 0, 90]) # add 90deg rotations to small rotations# s: 随机生成旋转后图像的缩放比例 范围在(1 - scale, 1 + scale)# s = 2 ** random.uniform(-scale, scale)# 随机生成缩放比例s = random.uniform(1 - scale, 1 + scale)# s = 2 ** random.uniform(-scale, scale)# cv2.getRotationMatrix2D: 二维旋转缩放函数# 参数 angle:旋转角度 center: 旋转中心(默认就是图像的中心) scale: 旋转后图像的缩放比例R[:2] = cv2.getRotationMatrix2D(angle=a, center=(0, 0), scale=s)# Shear 设置剪切矩阵# 弯曲角度S = np.eye(3) # 初始化T = [[1,0,0], [0,1,0], [0,0,1]]S[0, 1] = math.tan(random.uniform(-shear, shear) * math.pi / 180) # x shear (deg)S[1, 0] = math.tan(random.uniform(-shear, shear) * math.pi / 180) # y shear (deg)# Translation 设置平移矩阵T = np.eye(3) # 初始化T = [[1,0,0], [0,1,0], [0,0,1]] (3, 3)T[0, 2] = (random.uniform(0.5 - translate, 0.5 + translate) * width) # x translation (pixels)T[1, 2] = (random.uniform(0.5 - translate, 0.5 + translate) * height) # y translation (pixels)# Combined rotation matrix @ 表示矩阵乘法 生成仿射变换矩阵MM = T @ S @ R @ P @ C # order of operations (right to left) is IMPORTANT# 将仿射变换矩阵M作用在图片上if (border[0] != 0) or (border[1] != 0) or (M != np.eye(3)).any(): # image changedif perspective:# 透视变换函数 实现旋转平移缩放变换后的平行线不再平行# 参数和下面warpAffine类似img = cv2.warpPerspective(img, M, dsize=(width, height), borderValue=(114, 114, 114))else:# 仿射变换函数 实现旋转平移缩放变换后的平行线依旧平行# image changed img [1472, 1472, 3] => [736, 736, 3]# cv2.warpAffine: opencv实现的仿射变换函数# 参数: img: 需要变化的图像 M: 变换矩阵 dsize: 输出图像的大小 flags: 插值方法的组合(int 类型!)# borderValue: (重点!)边界填充值 默认情况下,它为0。img = cv2.warpAffine(img, M[:2], dsize=(width, height), borderValue=(114, 114, 114))# Visualize 可视化# import matplotlib.pyplot as plt# ax = plt.subplots(1, 2, figsize=(12, 6))[1].ravel()# ax[0].imshow(img[:, :, ::-1]) # base# ax[1].imshow(img2[:, :, ::-1]) # warped# Transform label coordinates# 同样需要调整标签信息n = len(targets)if n:# 判断是否可以使用segment标签: 只有segments不为空时即数据集中有多边形gt也有正常gt时才能使用segment标签 use_segments=True# 否则如果只有正常gt时segments为空 use_segments=Falseuse_segments = any(x.any() for x in segments)new = np.zeros((n, 4)) # [n, 0+0+0+0]# 如果使用的是segments标签(标签中含有多边形gt)if use_segments: # warp segments# 先对segment标签进行重采样# 比如说segment坐标只有100个,通过interp函数将其采样为n个(默认1000)# [n, x1y2...x99y100] 扩增坐标-> [n, 500, 2]# 由于有旋转,透视变换等操作,所以需要对多边形所有角点都进行变换segments = resample_segments(segments)for i, segment in enumerate(segments): # segment: [500, 2] 多边形的500个点坐标xyxy = np.ones((len(segment), 3)) # [1, 1+1+1]xy[:, :2] = segment # [500, 2]# 对该标签多边形的所有顶点坐标进行透视变换 或 仿射变换xy = xy @ M.T # transform @表示矩阵乘法运算xy = (xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2]) # perspective rescale or affine# 根据segment的坐标,取xy坐标的最大最小值,得到边框的坐标 clipnew[i] = segment2box(xy, width, height) # xy [500, 2]# 不使用segments标签 使用正常的矩形的标签targetselse: # warp boxes# 直接对box透视变换 或 仿射变换# 由于有旋转,透视变换等操作,所以需要对四个角点都进行变换xy = np.ones((n * 4, 3))xy[:, :2] = targets[:, [1, 2, 3, 4, 1, 4, 3, 2]].reshape(n * 4, 2) # x1y1, x2y2, x1y2, x2y1xy = xy @ M.T # transform 每个角点的坐标xy = (xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2]).reshape(n, 8) # perspective rescale or affine# create new boxesx = xy[:, [0, 2, 4, 6]]y = xy[:, [1, 3, 5, 7]]new = (np.concatenate((x.min(1), y.min(1), x.max(1), y.max(1))).reshape(4, n).T)# clip 去除太小的target(target大部分跑到图外去了)new[:, [0, 2]] = new[:, [0, 2]].clip(0, width)new[:, [1, 3]] = new[:, [1, 3]].clip(0, height)# filter candidates 过滤target 筛选box# 计算候选框并返回# 长和宽必须大于wh_thr个像素 裁剪过小的框(面积小于裁剪前的area_thr) 长宽比范围在(1/ar_thr, ar_thr)之间的限制# 筛选结果 [n] 全是True或False 使用比如: box1[i]即可得到i中所有等于True的矩形框 False的矩形框全部删除i = box_candidates(box1=targets[:, 1:5].T * s,box2=new.T,area_thr=0.01 if use_segments else 0.10,)# 得到所有满足条件的targetstargets = targets[i]targets[:, 1:5] = new[i]return img, targets
5.Albumentations模块
如果augment
参数为True,就启用
#utilsif self.augment:# Albumentationsimg, labels = self.albumentations(img, labels)nl = len(labels) # update after albumentations
def __init__(...):self.albumentations = Albumentations() if augment else None
这使用了Albumentations开源模块,它存在于 utils/augmentations.py
里面,是里面的一个类
Albumentations开源模块中实现的诸个数据增强方法,包括了Blur、MedianBlur、ToGray、CLAHE、RandomBrightnessContrast、RandomGamma、ImageCompression,这些数据增强方法的解释如下表所示:
数据增强 | 解释 |
---|---|
Blur | 使用随机尺寸的核来模糊输入图像 |
MedianBlur | 中值滤波 |
ToGray | 将输入的RGB图像转换为灰度图 |
CLAHE | [对输入图像进行限制对比度自适应直方图均衡(Contrast Limited Adaptive histgram equalization/CLAHE1]) |
RandomBrightnessContrast | 随机地改变输入图像的亮度与对比度 |
RandomGamma | 随机灰度系数 |
ImageCompression | 对输入图像进行压缩(可以选择的压缩格式为JPEG与WEBP) |
import albumentations as A# 1. 将所有的数据增强对象装填至列表T中
T = [A.Blur(p=0.01), # Blur数据增强,使用概率为0.01 A.MedianBlur(p=0.01), # MedianBlur数据增强,使用概率为0.01A.ToGray(p=0.01), # 转换为灰度图,使用概率为0.01A.CLAHE(p=0.01), # CLAHE直方图均衡,使用概率为0.01A.RandomBrightnessContrast(p=0.0), # 随机改变亮度与对比度,不使用A.RandomGamma(p=0.0), # 随机灰度系数,不使用A.ImageCompression(quality_lower=75, p=0.0)] # 对图像进行压缩,最低质量为75,不使用
# 2. 调用Compose类,将列表T中的所有增强对象合成一个对象
transform = A.Compose(T, bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels'])) # 将列表`T`转换为一个组合数据增强对象`transform`def augment(im, labels, p=1.0):if random.random() < p:# 3. 调用transform增强,使用的概率为 pnew = transform(image=im, bboxes=labels[:, 1:], class_labels=labels[:, 0]) # 调用`transform`对图像进行变换,实现数据增强im, labels = new['image'], np.array([[c, *b] for c, b in zip(new['class_labels'], new['bboxes'])])return im, labels
拓展:如何可视化上述提及的数据增强方法?
import albumentations as A
from PIL import Image
import math
import numpy as np
import matplotlib.pyplot as pltT = [A.Blur(p=1),A.MedianBlur(p=1),A.ToGray(p=1),A.CLAHE(p=1),A.RandomBrightnessContrast(p=1),A.RandomGamma(p=1),A.ImageCompression(quality_lower=75, p=1)]
img = Image.open('lena.jpg')plt.figure(figsize=(64, 32))
plt.subplot(2, math.ceil(len(T) / 2), 1)
plt.title("original image", fontsize=50)
plt.imshow(img)
for i in range(len(T)):title = T[i].__class__.__name__plt.subplot(2, math.ceil(len(T) / 2), i + 2)plt.title(title, fontsize=50)new_img = Image.fromarray(T[i](image=np.array(img))['image'])plt.imshow(new_img)
plt.show()
在yolo5中,实际上只使用了Albumentations中的Blur,MedianBlur,CLAHE,ToGray这四种数据增强,并且使用的概率都是0.01。这些数据增强有各自的应用场景:
-
Blur与MedianBlur都是对图像进行模糊处理,但二者有细微的差别,Blur使用了均值模糊,而MedianBlur使用了中值模糊,后者相对于前者更不易受到局部极值点的影响,对异常值的敏感性更低。二者都可以模拟图像模糊的场景,有利于提升模型对于模糊图像的识别效果;
-
ToGray增强的功能为将图像从RGB转换为灰度图,这种数据增强的方法旨在令模型能够不依赖颜色信息就对图像做出预测,使用该数据增强可以促进模型从图像的内容的形状信息而非色彩信息提取信息,可以提升模型的对于色度等信息的泛化能力;
-
CLAHE的作用为对图像进行CLAHE直方图均衡,该操作可以令图像的灰度图的分布更加均衡,令对比度较低的图像得到增强,细节信息更加明显。使用该数据增强方法的作用即为对图像本身进行增强,令其内容更加明显。如果数据集中存在图像灰度普遍偏大或普遍偏小的情况,可以考虑使用该数据增强对图像进行预处理。在这里yolo5使用CLAHE的原因仅仅是为了令数据更加多变,提升模型的泛化能力。
-
上述的几种数据增强的手段的功能互相垂直,可以叠加使用,且顺序也可以任意调整。
5.HSV变换
含义:
H:Hue augmentation (fraction), 色调
S:Saturation augmentation (fraction), 饱和度
V:Value augmentation (fraction), 曝光度
#utilsif self.augment:# Albumentations...# HSV color-spaceaugment_hsv(img, hgain=hyp['hsv_h'], sgain=hyp['hsv_s'], vgain=hyp['hsv_v'])
# 调用函数的文件位置:文件位置:utils/datasets.py
# 色域空间增强Augment colorspace:H色调、S饱和度、V亮度
# 通过一些随机值改变hsv,实现数据增强
augment_hsv(img, hgain=hyp['hsv_h'], sgain=hyp['hsv_s'], vgain=hyp['hsv_v'])# 被调用的函数位置:utils/augmentations.py
def augment_hsv(im, hgain=0.5, sgain=0.5, vgain=0.5):# HSV color-space augmentationif hgain or sgain or vgain:r = np.random.uniform(-1, 1, 3) * [hgain, sgain, vgain] + 1 # random gainshue, sat, val = cv2.split(cv2.cvtColor(im, cv2.COLOR_BGR2HSV))dtype = im.dtype # uint8x = np.arange(0, 256, dtype=r.dtype)lut_hue = ((x * r[0]) % 180).astype(dtype)lut_sat = np.clip(x * r[1], 0, 255).astype(dtype)lut_val = np.clip(x * r[2], 0, 255).astype(dtype)im_hsv = cv2.merge((cv2.LUT(hue, lut_hue), cv2.LUT(sat, lut_sat), cv2.LUT(val, lut_val)))cv2.cvtColor(im_hsv, cv2.COLOR_HSV2BGR, dst=im) # no return needed
6.Flip(翻转)
# Flip up-downif random.random() < hyp['flipud']:img = np.flipud(img)if nl:labels[:, 2] = 1 - labels[:, 2]# Flip left-rightif random.random() < hyp['fliplr']:img = np.fliplr(img)if nl:labels[:, 1] = 1 - labels[:, 1]
7.分割填补
分割出图像的目标后, 需要计算该目标边框与填补图片中的所有目标边框IOU<0.3(实现参数)
# 调用函数地址:utils/datasets.py -->LoadImagesAndLabels类-->load_mosaic()的最后
# augmentation
img4, labels4, segments4 = copy_paste(img4, labels4, segments4, p=self.hyp['copy_paste'])# 被调用函数地址:utils/augmentations.py
def copy_paste(im, labels, segments, p=0.5):# Implement Copy-Paste augmentation https://arxiv.org/abs/2012.07177, labels as nx5 np.array(cls, xyxy)n = len(segments)if p and n:h, w, c = im.shape # height, width, channelsim_new = np.zeros(im.shape, np.uint8)for j in random.sample(range(n), k=round(p * n)):l, s = labels[j], segments[j]box = w - l[3], l[2], w - l[1], l[4]ioa = bbox_ioa(box, labels[:, 1:5]) # intersection over areaif (ioa < 0.30).all(): # allow 30% obscuration of existing labelslabels = np.concatenate((labels, [[l[0], *box]]), 0)segments.append(np.concatenate((w - s[:, 0:1], s[:, 1:2]), 1))cv2.drawContours(im_new, [segments[j].astype(np.int32)], -1, (255, 255, 255), cv2.FILLED)result = cv2.bitwise_and(src1=im, src2=im_new)result = cv2.flip(result, 1) # augment segments (flip left-right)i = result > 0 # pixels to replace# i[:, :] = result.max(2).reshape(h, w, 1) # act over chim[i] = result[i] # cv2.imwrite('debug.jpg', im) # debugreturn im, labels, segments
8.Rectangular技术
在常用的目标检测算法中,输入图像的分辨率大小不一,常用的方式是将输入图像的分辨率归一化为统一的图像分辨率,然后再喂给目标检测网络。
YOLOv5中是下采样是32倍,所以输入模型前图片的长宽也必须是32的倍数,但是由于有些图片并不符合这个标准,所以数据在进入模型前,数据需要resize到416×416大小。
补充:如果输入图像的尺寸不是32的倍数,在进行下采样时会产生非整数像素点,这在实际的卷积操作中是不可行的,因为卷积核无法应用于部分像素点
对于统一图像分辨率,主要有三种方法:Resize、正方形填充和矩形填充
- Resize:直接将图像重新设置为新的尺寸,对原图像的宽高都有所改变,图像会失真
- square正方形填充:目标图像为正方形,将较长的一条边缩放到目标尺寸,然后再将较短的一条边也同比例缩放,最后把较短的一条边填充到目标尺寸,图片虽然不会失真,但是引入了过多的冗余像素信息
- rectangular矩形填充:将较长的一边设置为目标尺寸的长度,然后再将较短的一条边也同比例缩放,最后把较短的一条边填充到是32的整数倍就不再继续填充。矩形填充能够保证图片不失真,同时不会引入过多的冗余像素,是前两种方法的改进。
对比结果汇总:(从左到右依次为原图、Resize处理、正方形填充和矩形填充)
参考:第十三篇—Rectangular推理(YOLOv5专题) - 哔哩哔哩 (bilibili.com)
上面关于图片的缩放,yolov5也使用了letterbox技术,对比二者,发现它们是在缩放方式上有所不同,letterbox是按宽高比小的比例进行缩放,然后填充另一边,可能以长边为基准,也可能以短边为基准;而rectangular是按长边进行缩放,然后把较短的一条边填充到是32的整数倍就不再继续填充。
什么时候使用rectangular技术?
当训练时在train.py
中参数rect被启用时,就会进行rectangular训练模型
# 文件位置:utils/datasets.py
# 6、为Rectangular Training作准备:即减少大小不同图片处理时,对于多余的黑边做到最小,实现降低计算量# 这里主要是注意shapes的生成 这一步很重要 因为如果采样矩形训练那么整个batch的形状要一样 就要计算这个符合整个batch的shape# 而且还要对数据集按照高宽比进行排序 这样才能保证同一个batch的图片的形状差不多相同 再选择一个共同的shape代价也比较小if self.rect:# 所有训练图片的shapes = self.shapes # wh# 计算高宽比ar = s[:, 1] / s[:, 0] # aspect ratioirect = ar.argsort() # 根据高宽比排序self.img_files = [self.img_files[i] for i in irect] # 获取排序后的img_filesself.label_files = [self.label_files[i] for i in irect] # 获取排序后的label_filesself.labels = [self.labels[i] for i in irect] # 获取排序后的labelsself.shapes = s[irect] # 获取排序后的whar = ar[irect] # 获取排序后的wh# 计算每个batch采用的统一尺度 Set training image shapesshapes = [[1, 1]] * nbfor i in range(nb):# 同一个batch的图片提取出来ari = ar[bi == i]mini, maxi = ari.min(), ari.max() # 获取第i个batch中,最小和最大高宽比if maxi < 1:# [H,W]如果高/宽小于1(w > h),宽大于高,矮胖型,(img_size*maxi,img_size)(保证原图像尺度不变进行缩放)shapes[i] = [maxi, 1]elif mini > 1:# [H,W]如果高/宽大于1(w < h),宽小于高,瘦高型,(img_size,img_size *(1/mini))(保证原图像尺度不变进行缩放)shapes[i] = [1, 1 / mini]# 计算每个batch输入网络的shape值(向上设置为32的整数倍)# 要求每个batch_shapes的高宽都是32的整数倍,所以要先除以32,取整再乘以32(不过img_size如果是32倍数这里就没必要了)self.batch_shapes = np.ceil(np.array(shapes) * img_size / stride + pad).astype(np.int) * stride
总结:yolov5用到的图像增强技术实在太多了,刚开始只需记住哪种技术对应的效果是什么,就够了。
参考资料:
【YOLOV5-6.x讲解】数据增强方式介绍+代码实现_数据集增强代码-CSDN博客
推荐阅读:
YOLOv5的Tricks | 【Trick12】YOLOv5使用的数据增强方法汇总-阿里云开发者社区 (aliyun.com)