一、前言
大家好,我之前做过图像分类或识别时,经常感慨数据集不够大,导致模型的准确度不够高,虽然用过一些图像增强的方法,也见过别人用过一些数据增强的方法,接下来主要统计一些常见的图像增强的方法。
作为一种深度学习中的常用手段,图像增加对模型的泛化性和准确性都有帮助。数据增加的具体使用方式一般有两种:
- 一种是实时增加,比如在Caffe中加入数据扰动层,每次图像都先经过扰动操作,再去训练,这样训练经过几代(epoch)之后,就等效于数据增加。
- 一种是更加直接简单一些的,就是在训练之前就通过图像处理手段对数据样本进行扰动和增加
二、图像增强(data augmentation):
1.随机裁剪
2.随机旋转
3.随机颜色/明暗。
4.仿射变换
5. 图像翻转(镜像:x轴,y轴,xy轴)
2.1 随机裁剪
在裁剪的时候考虑图像宽高比的扰动:宽高比扰动相当于对物体的横向和纵向进行了缩放,这样除了物体的位置扰动,又多出了一项扰动。只要变化范围控制合适,目标物体始终在画面内,这种扰动是有助于提升泛化性能的。

第二图的左上角区域内随机采一点作为裁剪区域的左上角,就实现了如图中位置随机,且宽高比也随机的裁剪。
2.2 随机旋转
旋转:旋转中心,旋转角度
cv2.getRotationMatrix2D()
"""
第一个参数是旋转中心,
第二个参数是逆时针旋转角度,
第三个参数是缩放倍数,对于只是旋转的情况下这个值是1,返回值就是做仿射变换的矩阵。
"""
cv2.warpAffine()
"""
旋转之后在缺失区域会出现黑边
"""
2.3 随机的颜色和明暗
随机的颜色以及明暗的方法相对简单很多,就是给HSV空间的每个通道,分别加上一个微小的扰动。
其中对于色调: 从 - 到 之间按均匀采样,获取一个随机数 作为要扰动的值,然后新的像素值 为原始像素值 ;对于其他两个空间则是新像素值 为原始像素值 的
因为明暗度并不会对图像的直方图相对分布产生大的影响,所以在HSV扰动基础上,考虑再加入一个Gamma扰动,方法是设定一个大于1的Gamma值的上限 ,因为这个值通常会和1是一个量级,再用均匀采样的近似未必合适,所以从 到 之间均匀采样一个值 ,然后用
"""
image_augmentation.py
"""
import numpy as np
import cv2
'''
定义裁剪函数,四个参数分别是:
左上角横坐标x0
左上角纵坐标y0
裁剪宽度w
裁剪高度h
'''
crop_image = lambda img, x0, y0, w, h: img[y0:y0+h, x0:x0+w]
'''
随机裁剪
area_ratio为裁剪画面占原画面的比例
hw_vari是扰动占原高宽比的比例范围
'''
def random_crop(img, area_ratio, hw_vari):
h, w = img.shape[:2]
hw_delta = np.random.uniform(-hw_vari, hw_vari)
hw_mult = 1 + hw_delta
# 下标进行裁剪,宽高必须是正整数
w_crop = int(round(w*np.sqrt(area_ratio*hw_mult)))
# 裁剪宽度不可超过原图可裁剪宽度
if w_crop > w:
w_crop = w
h_crop = int(round(h*np.sqrt(area_ratio/hw_mult)))
if h_crop > h:
h_crop = h
# 随机生成左上角的位置
x0 = np.random.randint(0, w-w_crop+1)
y0 = np.random.randint(0, h-h_crop+1)
return crop_image(img, x0, y0, w_crop, h_crop)
'''
定义旋转函数:
angle是逆时针旋转的角度
crop是个布尔值,表明是否要裁剪去除黑边
'''
def rotate_image(img, angle, crop):
h, w = img.shape[:2]
# 旋转角度的周期是360°
angle %= 360
# 用OpenCV内置函数计算仿射矩阵
M_rotate = cv2.getRotationMatrix2D((w/2, h/2), angle, 1)
# 得到旋转后的图像
img_rotated = cv2.warpAffine(img, M_rotate, (w, h))
# 如果需要裁剪去除黑边
if crop:
angle_crop = angle % 180 # 对于裁剪角度的等效周期是180°
if angle_crop > 90: # 并且关于90°对称
angle_crop = 180 - angle_crop
theta = angle_crop * np.pi / 180.0 # 转化角度为弧度
hw_ratio = float(h) / float(w) # 计算高宽比
tan_theta = np.tan(theta) # 计算裁剪边长系数的分子项
numerator = np.cos(theta) + np.sin(theta) * tan_theta
r = hw_ratio if h > w else 1 / hw_ratio # 计算分母项中和宽高比相关的项
denominator = r * tan_theta + 1 # 计算分母项
crop_mult = numerator / denominator # 计算最终的边长系数
w_crop = int(round(crop_mult*w)) # 得到裁剪区域
h_crop = int(round(crop_mult*h))
x0 = int((w-w_crop)/2)
y0 = int((h-h_crop)/2)
img_rotated = crop_image(img_rotated, x0, y0, w_crop, h_crop)
return img_rotated
'''
随机旋转
angle_vari是旋转角度的范围[-angle_vari, angle_vari)
p_crop是要进行去黑边裁剪的比例
'''
def random_rotate(img, angle_vari, p_crop):
angle = np.random.uniform(-angle_vari, angle_vari)
crop = False if np.random.random() > p_crop else True
return rotate_image(img, angle, crop)
'''
定义hsv变换函数:
hue_delta是色调变化比例
sat_delta是饱和度变化比例
val_delta是明度变化比例
'''
def hsv_transform(img, hue_delta, sat_mult, val_mult):
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype(np.float)
img_hsv[:, :, 0] = (img_hsv[:, :, 0] + hue_delta) % 180
img_hsv[:, :, 1] *= sat_mult
img_hsv[:, :, 2] *= val_mult
img_hsv[img_hsv > 255] = 255
return cv2.cvtColor(np.round(img_hsv).astype(np.uint8), cv2.COLOR_HSV2BGR)
'''
随机hsv变换
hue_vari是色调变化比例的范围
sat_vari是饱和度变化比例的范围
val_vari是明度变化比例的范围
'''
def random_hsv_transform(img, hue_vari, sat_vari, val_vari):
hue_delta = np.random.randint(-hue_vari, hue_vari)
sat_mult = 1 + np.random.uniform(-sat_vari, sat_vari)
val_mult = 1 + np.random.uniform(-val_vari, val_vari)
return hsv_transform(img, hue_delta, sat_mult, val_mult)
'''
定义gamma变换函数:
gamma就是Gamma
'''
def gamma_transform(img, gamma):
gamma_table = [np.power(x / 255.0, gamma) * 255.0 for x in range(256)]
gamma_table = np.round(np.array(gamma_table)).astype(np.uint8)
return cv2.LUT(img, gamma_table)
'''
随机gamma变换
gamma_vari是Gamma变化的范围[1/gamma_vari, gamma_vari)
'''
def random_gamma_transform(img, gamma_vari):
log_gamma_vari = np.log(gamma_vari)
alpha = np.random.uniform(-log_gamma_vari, log_gamma_vari)
gamma = np.exp(alpha)
return gamma_transform(img, gamma)
主程序里首先定义三个子模块:
- 定义一个函数
通过Python的argparse模块定义了各种输入参数和默认值。需要注意的是这里用argparse来输入所有参数是因为参数总量并不是特别多,如果增加了更多的扰动方法,更合适的参数输入方式可能是通过一个配置文件。parse_arg()
- 定义一个生成待处理图像列表的函数
,根据输入中要增加图片的数量和并行进程的数目尽可能均匀地为每个进程生成了需要处理的任务列表。执行随机扰动的代码定义在augment_images()中,这个函数是每个进程内进行实际处理的函数,执行顺序是镜像裁剪旋转。需要注意的是镜像generate_image_list()
- 定义一个
函数进行调用,代码如下:定义一个main函数进行调用,代码如下:main
"""
run_augmentation.py
"""
import os
import argparse
import random
import math
from multiprocessing import Process
from multiprocessing import cpu_count
import cv2
# 导入image_augmentation.py为一个可调用模块
import image_augmentation as ia
# 利用Python的argparse模块读取输入输出和各种扰动参数
def parse_args():
parser = argparse.ArgumentParser(
description='A Simple Image Data Augmentation Tool',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('input_dir',
help='Directory containing images')
parser.add_argument('output_dir',
help='Directory for augmented images')
parser.add_argument('num',
help='Number of images to be augmented',
type=int)
parser.add_argument('--num_procs',
help='Number of processes for paralleled augmentation',
type=int, default=cpu_count())
parser.add_argument('--p_mirror',
help='Ratio to mirror an image',
type=float, default=0.5)
parser.add_argument('--p_crop',
help='Ratio to randomly crop an image',
type=float, default=1.0)
parser.add_argument('--crop_size',
help='The ratio of cropped image size to original image size, in area',
type=float, default=0.8)
parser.add_argument('--crop_hw_vari',
help='Variation of h/w ratio',
type=float, default=0.1)
parser.add_argument('--p_rotate',
help='Ratio to randomly rotate an image',
type=float, default=1.0)
parser.add_argument('--p_rotate_crop',
help='Ratio to crop out the empty part in a rotated image',
type=float, default=1.0)
parser.add_argument('--rotate_angle_vari',
help='Variation range of rotate angle',
type=float, default=10.0)
parser.add_argument('--p_hsv',
help='Ratio to randomly change gamma of an image',
type=float, default=1.0)
parser.add_argument('--hue_vari',
help='Variation of hue',
type=int, default=10)
parser.add_argument('--sat_vari',
help='Variation of saturation',
type=float, default=0.1)
parser.add_argument('--val_vari',
help='Variation of value',
type=float, default=0.1)
parser.add_argument('--p_gamma',
help='Ratio to randomly change gamma of an image',
type=float, default=1.0)
parser.add_argument('--gamma_vari',
help='Variation of gamma',
type=float, default=2.0)
args = parser.parse_args()
args.input_dir = args.input_dir.rstrip('/')
args.output_dir = args.output_dir.rstrip('/')
return args
'''
根据进程数和要增加的目标图片数,
生成每个进程要处理的文件列表和每个文件要增加的数目
'''
def generate_image_list(args):
# 获取所有文件名和文件总数
filenames = os.listdir(args.input_dir)
num_imgs = len(filenames)
# 计算平均处理的数目并向下取整
num_ave_aug = int(math.floor(args.num/num_imgs))
# 剩下的部分不足平均分配到每一个文件,所以做成一个随机幸运列表
# 对于幸运的文件就多增加一个,凑够指定的数目
rem = args.num - num_ave_aug*num_imgs
lucky_seq = [True]*rem + [False]*(num_imgs-rem)
random.shuffle(lucky_seq)
# 根据平均分配和幸运表策略,
# 生成每个文件的全路径和对应要增加的数目并放到一个list里
img_list = [
(os.sep.join([args.input_dir, filename]), num_ave_aug+1 if lucky else num_ave_aug)
for filename, lucky in zip(filenames, lucky_seq)
]
# 文件可能大小不一,处理时间也不一样,
# 所以随机打乱,尽可能保证处理时间均匀
random.shuffle(img_list)
# 生成每个进程的文件列表,
# 尽可能均匀地划分每个进程要处理的数目
length = float(num_imgs) / float(args.num_procs)
indices = [int(round(i * length)) for i in range(args.num_procs + 1)]
return [img_list[indices[i]:indices[i + 1]] for i in range(args.num_procs)]
# 每个进程内调用图像处理函数进行扰动的函数
def augment_images(filelist, args):
# 遍历所有列表内的文件
for filepath, n in filelist:
img = cv2.imread(filepath)
filename = filepath.split(os.sep)[-1]
dot_pos = filename.rfind('.')
# 获取文件名和后缀名
imgname = filename[:dot_pos]
ext = filename[dot_pos:]
print('Augmenting {} ...'.format(filename))
for i in range(n):
img_varied = img.copy()
# 扰动后文件名的前缀
varied_imgname = '{}_{:0>3d}_'.format(imgname, i)
# 按照比例随机对图像进行镜像
if random.random() < args.p_mirror:
# 利用numpy.fliplr(img_varied)也能实现
img_varied = cv2.flip(img_varied, 1)
varied_imgname += 'm'
# 按照比例随机对图像进行裁剪
if random.random() < args.p_crop:
img_varied = ia.random_crop(
img_varied,
args.crop_size,
args.crop_hw_vari)
varied_imgname += 'c'
# 按照比例随机对图像进行旋转
if random.random() < args.p_rotate:
img_varied = ia.random_rotate(
img_varied,
args.rotate_angle_vari,
args.p_rotate_crop)
varied_imgname += 'r'
# 按照比例随机对图像进行HSV扰动
if random.random() < args.p_hsv:
img_varied = ia.random_hsv_transform(
img_varied,
args.hue_vari,
args.sat_vari,
args.val_vari)
varied_imgname += 'h'
# 按照比例随机对图像进行Gamma扰动
if random.random() < args.p_gamma:
img_varied = ia.random_gamma_transform(
img_varied,
args.gamma_vari)
varied_imgname += 'g'
# 生成扰动后的文件名并保存在指定的路径
output_filepath = os.sep.join([
args.output_dir,
'{}{}'.format(varied_imgname, ext)])
cv2.imwrite(output_filepath, img_varied)
# 主函数
def main():
# 获取输入输出和变换选项
args = parse_args()
params_str = str(args)[10:-1]
# 如果输出文件夹不存在,则建立文件夹
if not os.path.exists(args.output_dir):
os.mkdir(args.output_dir)
print('Starting image data augmentation for {}\n'
'with\n{}\n'.format(args.input_dir, params_str))
# 生成每个进程要处理的列表
sublists = generate_image_list(args)
# 创建进程
processes = [Process(target=augment_images, args=(x, args, )) for x in sublists]
# 并行多进程处理
for p in processes:
p.start()
for p in processes:
p.join()
print('\nDone!')
if __name__ == '__main__':
main()
终端运行:
python run_augmentation.py -h