opencv点仿射变换 opencv仿射变换提取小图_opencv 仿射变换


从几何上来讲,图像可以被理解为像素的二维平面,平面上最简单的变换是线性变换,在图像上我们通常叫它们为仿射变换,仿射变换通常由一个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(高乘以宽,按照存储序)


opencv点仿射变换 opencv仿射变换提取小图_仿射变换_02

原始图像


opencv点仿射变换 opencv仿射变换提取小图_opencv 仿射变换_03

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


结果如下,十分一致


opencv点仿射变换 opencv仿射变换提取小图_opencv点仿射变换_04

使用仿射变换实现的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的区域下来


opencv点仿射变换 opencv仿射变换提取小图_仿射变换_05

两种方法的对比结果,上面是使用仿射变换的结果

综上所述,图像仿射变换能完成我们需要的常用图像几何操作,并且使用了统一的api,更容易理解,一般的如果要在目标检测任务上做图像增强,可以直接在平移和缩放矩阵的参数上做微小的调整,而且只用了一次变换还能节省计算量。

有时间会介绍如果使用spatial transformer来实现这些变化,实现一个可学习的仿射变换,a learned affine transformation.