从几何上来讲,图像可以被理解为像素的二维平面,平面上最简单的变换是线性变换,在图像上我们通常叫它们为仿射变换,仿射变换通常由一个2x3的矩阵,之所以用2x3的矩阵,而不由2x2方阵来描述,是考虑到了平移,任意仿射变换都可以分解为以下四类变换的叠加:平移,放缩(尺度变换),旋转和切变。
更一般的,在图像几何变换中我们更常用的一般是旋转,裁剪和resize,它们都是仿射变换的具体类型。在用深度学习做目标检测的时候,我们通常需要把检测框检测到的物体,裁剪出来,如果检测框有旋转,我们还需要旋转检测框,并缩放到同一尺寸。这里就不得不提到lettebox变换了,这个一个被忽视的很重要的变换,我们在数据集上训练网络的时候,通常需要把数据集变换到同一尺寸,但是通常的resize函数会破环图像的纵横比,aspect ratio,做过检测任务的同学都知道,aspect ratio对于检测的效果非常重要,letterbox就是在保持纵横比的前提下对图像做resize,先resize然后按需要在周围pad上0像素。
使用opencv来实现letterbox变换的代码如下:
def cv2_letterbox_image(image, expected_size):
ih, iw = image.shape[0:2]
ew, eh = expected_size
scale = min(eh / ih, ew / iw)
nh = int(ih * scale)
nw = int(iw * scale)
image = cv2.resize(image, (nw, nh), interpolation=cv2.INTER_CUBIC)
top = (eh - nh) // 2
bottom = eh - nh - top
left = (ew - nw) // 2
right = ew - nw - left
new_img = cv2.copyMakeBorder(image, top, bottom, left, right, cv2.BORDER_CONSTANT)
return new_img
使用效果如下,原始图像大小是225x225,这里resize到150x300(高乘以宽,按照存储序)
原始图像 letterbox变换结果事实上,这个操作可以用一个尺度变换和一个平移变换实现
def cv2_letterbox_image_by_warp(img, expected_size):
ih, iw = img.shape[0:2]
ew, eh = expected_size
scale = min(eh / ih, ew / iw)
nh = int(ih * scale)
nw = int(iw * scale)
smat = np.array([[scale, 0, 0], [0, scale, 0], [0, 0, 1]], np.float32)
top = (eh - nh) // 2
bottom = eh - nh - top
left = (ew - nw) // 2
right = ew - nw - left
tmat = np.array([[1, 0, left], [0, 1, top], [0, 0, 1]], np.float32)
amat = np.dot(tmat, smat)
amat = amat[:2, :]
dst = cv2.warpAffine(img, amat, expected_size)
return dst
结果如下,十分一致
使用仿射变换实现的letterbox变换更一般的,我们通常需要将检测到的目标变换到需要的尺寸,一般做法就是先裁剪,再做letterbox变换,代码如下
def cv2_crop_and_letterbox(img, crop_corner, crop_size, expected_size):
cropped_img = img[crop_corner[1]:crop_corner[1] + crop_size[1], crop_corner[0]:crop_corner[0] + crop_size[0], ...]
return cv2_letterbox_image(cropped_img, expected_size)
其中crop_corner表示裁剪区域的左上角,crop_size表示需要裁剪区域的尺寸,expected_size表示需要的尺寸
同样,第一步的裁剪工作其实是可以用一个平移变换实现的,跟letterbox叠加,这个操作可以用一个仿射变换来完成
def cv2_crop_and_letterbox_by_warp(img, crop_corner, crop_size, expected_size):
cmat = np.array([[1, 0, -crop_corner[0]], [0, 1, -crop_corner[1]], [0, 0, 1]], np.float32)
ih, iw = crop_size[0:2]
ew, eh = expected_size
scale = min(eh / ih, ew / iw)
nh = int(ih * scale)
nw = int(iw * scale)
smat = np.array([[scale, 0, 0], [0, scale, 0], [0, 0, 1]], np.float32)
top = (eh - nh) // 2
bottom = eh - nh - top
left = (ew - nw) // 2
right = ew - nw - left
tmat = np.array([[1, 0, left], [0, 1, top], [0, 0, 1]], np.float32)
amat = np.dot(tmat, smat)
amat = np.dot(amat, cmat)
amat = amat[:2, :]
dst = cv2.warpAffine(img, amat, expected_size)
return dst
对比结果如下,我们从(50,50)开始裁剪100x100的区域下来
两种方法的对比结果,上面是使用仿射变换的结果综上所述,图像仿射变换能完成我们需要的常用图像几何操作,并且使用了统一的api,更容易理解,一般的如果要在目标检测任务上做图像增强,可以直接在平移和缩放矩阵的参数上做微小的调整,而且只用了一次变换还能节省计算量。
有时间会介绍如果使用spatial transformer来实现这些变化,实现一个可学习的仿射变换,a learned affine transformation.