图像仿射变换OpenCV API与自行代码实现

OpenCV相关API接口梳理

  • M = cv2.getRotationMatrix2D(rot_center, theta, scale)计算二维变换矩阵
  • 输入:旋转中心 rot_center、逆时针旋转角度 theta、缩放系数 scale
  • 输出:仿射变换矩阵 M
  • img_warpaffine = cv2.warpAffine(img, M, (out_w, out_h))根据变换矩阵 opencv 伽马校正_opencv
  • 输入:待变换的图像 img、变换矩阵 M、输出图像尺寸 (out_w, out_h)
  • 输出:变换后的图像 img_warpaffine
  • M_inv = cv2.invertAffineTransform(M)计算变换矩阵的反矩阵
  • 输入:变换矩阵 M
  • 输出:M 的你矩阵 M_inv

相关API自行实现

getRotationMatrix2D

图像的几何变换 给出了在一般情况下,二维仿射变换矩阵的计算方法:
opencv 伽马校正_opencv_02
其中 opencv 伽马校正_opencv 伽马校正_03 是旋转中心的坐标,opencv 伽马校正_opencv_04 是缩放的尺度,opencv 伽马校正_opencv_05 是逆时针旋转的角度, opencv 伽马校正_计算机视觉_06

据此,我们可以自己实现 getRotationMatrix2D 方法:

def myGetRotationMatrix2d(rot_center, theta, scale):
    rad = np.radians(theta)
    alpha = scale * np.cos(rad)
    beta = scale * np.sin(rad)
    M = np.array([
        [alpha, beta, (1 - alpha) * rot_center[0] - beta * rot_center[1]],
        [-beta, alpha, beta * rot_center[0] + (1 - alpha) * rot_center[1]]
    ])
    return M

验证正确性:

# 计算变换矩阵
M = cv2.getRotationMatrix2D(rot_center, theta, scale)
my_M = myGetRotationMatrix2d(rot_center, theta, scale)
print(np.isclose(M, my_M))

输出为全 True,可知我们自己计算的变换矩阵 opencv 伽马校正_仿射变换_07

warpAffine

本文中就不再介绍推导过程,而直接给出代码。

def pyWarpAffine(image, M, dst_size, constant=(0, 0, 0)):

    # 注意输入的M矩阵格式,是Origin->Dst
    # 而这里需要的是Dst->Origin,所以要取逆矩阵
    M = cv2.invertAffineTransform(M)
    constant = np.array(constant)
    ih, iw   = image.shape[:2]
    dw, dh   = dst_size
    dst      = np.full((dh, dw, 3), constant, dtype=np.uint8)
    irange   = lambda p: p[0] >= 0 and p[0] < iw and p[1] >= 0 and p[1] < ih

    for y in range(dh):
        for x in range(dw):

            homogeneous = np.array([[x, y, 1]]).T
            ox, oy = M @ homogeneous

            low_ox = int(np.floor(ox))
            low_oy = int(np.floor(oy))
            high_ox = low_ox + 1
            high_oy = low_oy + 1

            # p0     p1
            #
            # p2     p3

            pos = ox - low_ox, oy - low_oy
            p0_area = (1 - pos[0]) * (1 - pos[1])
            p1_area = pos[0] * (1 - pos[1])
            p2_area = (1 - pos[0]) * pos[1]
            p3_area = pos[0] * pos[1]

            p0 = low_ox, low_oy
            p1 = high_ox, low_oy
            p2 = low_ox, high_oy
            p3 = high_ox, high_oy
            p0_value = image[p0[1], p0[0]] if irange(p0) else constant
            p1_value = image[p1[1], p1[0]] if irange(p1) else constant
            p2_value = image[p2[1], p2[0]] if irange(p2) else constant
            p3_value = image[p3[1], p3[0]] if irange(p3) else constant
            dst[y, x] = p0_area * p0_value + p1_area * p1_value + p2_area * p2_value + p3_area * p3_value
    return dst

测试结果如下:

opencv 伽马校正_计算机视觉_08

左侧和右侧分别是 OpenCV 的 warpAffine 函数的变换结果与自行实现的 warpAffine 函数的变换结果。可以看到,基本是一致的。

反变换与无损的仿射变换

反变换

我们可以通过cv2.invertAffineTransform 计算仿射变换矩阵的反变换,从而将变换后的图像再变换回来:

# 反变换
M_inv = cv2.invertAffineTransform(M)
cat_cv_inv = cv2.warpAffine(cat_cv, M_inv, (h, w))
cv2.imwrite("opencv_res_inv.jpg", cat_cv_inv)

结果如下:

opencv 伽马校正_仿射变换_09

明显可以看到,虽然图片旋转回来了,大小也变回来了,但是由于超出了输出图像的指定大小,在变换的过程中有些边角的信息丢失掉了。那么,我们能否实现无损的图像旋转呢?

无损的仿射变换

代码如下:

def opencv_full_rotate(img, angle):
    h, w = img.shape[: 2]
    center = (w / 2, h / 2)
    scale = 1.0
    M = cv2.getRotationMatrix2D(center, angle, scale)
    # 2.2 新的宽高,radians(angle) 把角度转为弧度 sin(弧度)
    rad = np.radians(angle)
    new_H = int(w * abs(np.sin(rad)) + h * abs(np.cos(rad)))
    new_W = int(h * abs(np.sin(rad)) + w * abs(np.cos(rad)))
    # 2.3 平移
    M[0, 2] += (new_W - w) / 2
    M[1, 2] += (new_H - h) / 2
    rotate = cv2.warpAffine(img, M, (new_W, new_H), borderValue=(0, 0, 0))
    return rotate

就是要保证变换后的图像都在输出尺寸范围内,测试结果如下:

opencv 伽马校正_仿射变换_10

可以看到,实现了无损的图像仿射变换。

Ref

  1. 图像的几何变换
  2. 图像旋转:getRotationMatrix2D详解–无损失旋转图片