入门小菜鸟,希望像做笔记记录自己学的东西,也希望能帮助到同样入门的人,更希望大佬们帮忙纠错啦~侵权立删。
目录
一、原理分析
二、代码分析
1、主体部分——load_mosaic
2、load_image函数
3、random_perspective()函数(详见代码解析)
YOLOv5采用和YOLOv4一样的Mosaic数据增强。
主要原理:它将一张选定的图片和随机的3张图片进行随机裁剪,再拼接到一张图上作为训练数据。
这样可以丰富图片的背景,而且四张图片拼接在一起变相提高了batch_size,在进行batch normalization(归一化)的时候也会计算四张图片。
这样让YOLOv5对本身batch_size不是很依赖。
labels4, segments4 = [], []
s = self.img_size #获取图像尺寸
yc, xc = (int(random.uniform(-x, 2 * s + x)) for x in self.mosaic_border) # mosaic center x, y
#random.uniform随机生成上述范围的实数(即一半图像尺寸到1.5倍图像尺寸)
#这里是随机生成mosaic中心点
先初始化标注列表为空,然后获取图像尺寸s
根据图像尺寸利用random.uniform()随机生成mosaic中心点,范围在(即一半图像尺寸到1.5倍图像尺寸)
indices = [index] + random.choices(self.indices, k=3) # 3 additional image indices
#随机生成另外3张图片的索引
#random.choices——随机生成3个图片总数内的索引
#然后一起把索引,连同原先选定的图片打包进indices
random.shuffle(indices)
#对这些索引值随机排序
利用random.choices()随机生成另外3张图片的索引,将这4张图片的索引填进indices列表,然后利用random.shuffle()对这些索引值随机排序
for i, index in enumerate(indices): #循环遍历这些图片
# Load image
img, _, (h, w) = load_image(self, index)#加载图片和高宽
循环遍历这4张图片,并且调用load_image()函数加载图片和对应高宽
接下来就是如何放置这4张图啦~
# place img in img4
if i == 0: # top left(左上角)
img4 = np.full((s * 2, s * 2, img.shape[2]), 114, dtype=np.uint8) # base image with 4 tiles
#先生成背景图
x1a, y1a, x2a, y2a = max(xc - w, 0), max(yc - h, 0), xc, yc # xmin, ymin, xmax, ymax (large image)
#设置大图上的位置(要么原图大小,要么放大)(w,h)或(xc,yc)(新生成的那张大图)
x1b, y1b, x2b, y2b = w - (x2a - x1a), h - (y2a - y1a), w, h # xmin, ymin, xmax, ymax (small image)
#选取小图上的位置(原图)
第一张图片放在左上角
img4首先用np.full()函数填充初始化大图,尺寸是4张图那么大
然后分别设置大图上该图片的位置,以及相应的在原图(即小图)上截取的位置坐标
elif i == 1: # top right(右上角)
x1a, y1a, x2a, y2a = xc, max(yc - h, 0), min(xc + w, s * 2), yc
x1b, y1b, x2b, y2b = 0, h - (y2a - y1a), min(w, x2a - x1a), h
elif i == 2: # bottom left(左下角)
x1a, y1a, x2a, y2a = max(xc - w, 0), yc, xc, min(s * 2, yc + h)
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)
x1b, y1b, x2b, y2b = 0, 0, min(w, x2a - x1a), min(y2a - y1a, h)
剩下3张如法炮制
img4[y1a:y2a, x1a:x2a] = img[y1b:y2b, x1b:x2b] # img4[ymin:ymax, xmin:xmax]
#大图上贴上对应的小图
大图上贴上小图的对应部分
padw = x1a - x1b
padh = y1a - y1b
#计算小图到大图上时所产生的偏移,用来计算mosaic增强后的标签的位置
计算小图到大图上时所产生的偏移,用来计算mosaic增强后的标签的位置
# Labels
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 format
#将xywh(百分比那些值)标准化为像素xy格式
segments = [xyn2xy(x, w, h, padw, padh) for x in segments]
#转为像素段
labels4.append(labels)
segments4.extend(segments)
#填进列表
对label标注进行初始化操作:
先读取对应图片的label,然后将xywh格式的label标准化为像素xy格式的。
segments转为像素段格式
然后统统填进之前准备的标注列表
# Concat/clip labels
labels4 = np.concatenate(labels4, 0) #完成数组拼接
for x in (labels4[:, 1:], *segments4):
np.clip(x, 0, 2 * s, out=x) # clip when using random_perspective()
#np.clip截取函数,固定值在0到2s内
# img4, labels4 = replicate(img4, labels4) # replicate
先把label列表进行数组拼接,转化好格式,方便下面的处理,并且把数据截取在0到2倍图片尺寸
# Augment
#进行mosaic的时候将四张图片整合到一起之后shape为[2*img_size,2*img_size]
#对mosaic整合的图片进行随机旋转、平移、缩放、裁剪,并resize为输入大小img_size
img4, labels4, segments4 = copy_paste(img4, labels4, segments4, p=self.hyp['copy_paste'])
img4, labels4 = random_perspective(img4, labels4, segments4,
degrees=self.hyp['degrees'],
translate=self.hyp['translate'],
scale=self.hyp['scale'],
shear=self.hyp['shear'],
perspective=self.hyp['perspective'],
border=self.mosaic_border) # border to remove
进行mosaic的时候将四张图片整合到一起之后shape为[2*img_size,2*img_size]
并且对mosaic整合的图片进行随机旋转、平移、缩放、裁剪,并resize为输入大小img_size
return img4, labels4
最后返回处理好的图片和相应的label
load_image函数:加载图片并根据设定的输入大小与图片原大小的比例ratio进行resize
首先获取该索引的图片
def load_image(self, i):
#load_image加载图片并根据设定的输入大小与图片原大小的比例ratio进行resize
# loads 1 image from dataset index 'i', returns im, original hw, resized hw
im = self.imgs[i]#获取该索引的图片
判断一下图片是否有缓存,即有没有缩放处理过(这里不太确定这样理解对不对,如果错了麻烦在评论区跟我说一下下,谢谢啦~)
如果没有:
先去对应文件夹中找
如果能找到:加载这张图片
如果找不到:读取这张图的路径,然后报错找不到对应路径的这张图片
读取这张图的原始高宽以及设定resize比例
如果这个比例不等于1,那我们就resize一下进行一个缩放
最后返回这张图片,原始高宽和缩放后的高宽
if im is None: # not cached in ram
#图片如果没有缓存(就是还没有任何缩放处理过)
npy = self.img_npy[i] #去文件夹中找
if npy and npy.exists(): # load npy
im = np.load(npy) #找到了我们就加载这张图片
else: # read image
path = self.img_files[i] #找不到图片就读取原本这张图的路径
im = cv2.imread(path) # BGR
assert im is not None, f'Image Not Found {path}' #报错找不到这张图
h0, w0 = im.shape[:2] # orig hw
#读取这张图的原始高宽
r = self.img_size / max(h0, w0) # ratio
#设定resize比例
if r != 1: # if sizes are not equal
im = cv2.resize(im, (int(w0 * r), int(h0 * r)),
interpolation=cv2.INTER_AREA if r < 1 and not self.augment else cv2.INTER_LINEAR)#实现缩放
return im, (h0, w0), im.shape[:2] # im, hw_original, hw_resized
如果有
那就直接返回这张图片,原始高宽和缩放后的高宽啦~
else:
return self.imgs[i], self.img_hw0[i], self.img_hw[i] # im, hw_original, hw_resized
随机变换
计算方法:坐标向量和变换矩阵的乘积
首先获得加上边框后的图片高宽
def random_perspective(im, targets=(), segments=(), degrees=10, translate=.1, scale=.1, shear=10, perspective=0.0,
border=(0, 0)):
# torchvision.transforms.RandomAffine(degrees=(-10, 10), translate=(0.1, 0.1), scale=(0.9, 1.1), shear=(-10, 10))
# targets = [cls, xyxy]
#图片高宽(加上border边框)
height = im.shape[0] + border[0] * 2 # shape(h,w,c)
width = im.shape[1] + border[1] * 2
然后计算中心点
# Center
C = np.eye(3)#生成3*3的对角为1的对角矩阵
#x方向的中心
C[0, 2] = -im.shape[1] / 2 # x translation (pixels)
#y方向的中心
C[1, 2] = -im.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)#生成3*3的对角为1的对角矩阵
a = random.uniform(-degrees, degrees)#随机生成范围内的角度
# a += random.choice([-180, -90, 0, 90]) # add 90deg rotations to small rotations
s = random.uniform(1 - scale, 1 + scale) #随机生成缩放比例
# s = 2 ** random.uniform(-scale, scale)
R[:2] = cv2.getRotationMatrix2D(angle=a, center=(0, 0), scale=s)#图片旋转得到仿射变化矩阵赋给R的前两行
# Shear
#弯曲角度
S = np.eye(3)#生成3*3的对角为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[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
#组合旋转矩阵
M = T @ S @ R @ P @ C # order of operations (right to left) is IMPORTANT
#通过矩阵乘法组合
if (border[0] != 0) or (border[1] != 0) or (M != np.eye(3)).any(): # image changed
#没有边框或者没有任何变换
if perspective:#如果透视
im = cv2.warpPerspective(im, M, dsize=(width, height), borderValue=(114, 114, 114))
#cv2.warpPerspective透视变换函数,可保持直线不变形,但是平行线可能不再平行
else: # affine
im = cv2.warpAffine(im, M[:2], dsize=(width, height), borderValue=(114, 114, 114))
#cv2.warpAffine放射变换函数,可实现旋转,平移,缩放,并且变换后的平行线依旧平行
然后是变换标签的坐标
# Transform label coordinates
#变换标签坐标
n = len(targets)#目标个数
if n:#如果有目标
use_segments = any(x.any() for x in segments)#判断segments是否为空或是否全为0(目标像素段)
new = np.zeros((n, 4))#初始化信息矩阵,每个目标4个信息xywh
if use_segments: # warp segments(变形segments)
#如果不是空的
segments = resample_segments(segments) # upsample
#上采样
for i, segment in enumerate(segments):
xy = np.ones((len(segment), 3))
xy[:, :2] = segment#前两列是目标中心的像素段
xy = xy @ M.T # transform转化
xy = xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2] # perspective rescale or affine
#透视处理,重新缩放或者仿射
#xy的最后一列全为1是为了和M.T矩阵相乘时,只会与最后M.T的最后一行相乘,而M.T的最后一行则为P当时设定的透视值
# clip修建
new[i] = segment2box(xy, width, height)
else: # 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, x2y1
xy = xy @ M.T # transform
xy = (xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2]).reshape(n, 8) # perspective rescale or affine
# create new boxes
x = 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
#去除进行上面一系列操作后被裁剪过小的框
new[:, [0, 2]] = new[:, [0, 2]].clip(0, width)
new[:, [1, 3]] = new[:, [1, 3]].clip(0, height)
最后是计算候选框并返回
# filter candidates
i = box_candidates(box1=targets[:, 1:5].T * s, box2=new.T, area_thr=0.01 if use_segments else 0.10)#计算候选框
targets = targets[i]
targets[:, 1:5] = new[i]
return im, targets
欢迎大家在评论区批评指正,谢谢~