Bootstrap

OpenCV-仿射变换原理

仿射变换原理

1.基本介绍

对仿射变换的解释这里就直接转载了,末尾有链接:

仿射变换(Affine Transformation)其实是另外两种简单变换的叠加:一个是线性变换,一个是平移变换

仿射变换变化包括缩放(Scale、平移(transform)、旋转(rotate)、反射(reflection,对图形照镜子)、错切(shear mapping,感觉像是一个图形的倒影),原来的直线仿射变换后还是直线,原来的平行线经过仿射变换之后还是平行线,这就是仿射

仿射变换中集合中的一些性质保持不变:
(1)凸性
(2)共线性:若几个点变换前在一条线上,则仿射变换后仍然在一条线上
(3)平行性:若两条线变换前平行,则变换后仍然平行
(4)共线比例不变性:变换前一条线上两条线段的比例,在变换后比例不变

2.数学表示

简单来说,仿射变换就是:

 用数学公式表示就是:

对应的齐次坐标矩阵表示为:

 为了更直观的理解仿射变换,给出一张图

我们先不管仿射矩阵是怎么来的,先看看对应的a~f会对图像产生什么作用 ,先取出图中所示的三个两两互不共线的点

(1)无变化

 这三个点用矩阵表示就是:

把它作为仿射矩阵就是无改变,即原图

(2)平移

将c和f设为x, y,可以看到图像平移了原本的(0, 0)点移动到了(x, y),其余点也相应的改变

(3)缩放

将a和e设为H, W,可以看到图像的高度和宽度发生了改变,其余点也相应的改变

剩下的图片就不再赘述,因为通过这三个例子后面的应该都能看懂

我们大致可以感觉到,参数c和f控制了图片的平移变换,剩下的四个参数则控制线性变换。

这里给出线性变换和平移变换的定义:

线性变换定义

线性变换保持向量的加法和标量乘法的性质。在二维空间中,线性变换可以通过一个2x2的矩阵来表示。这个矩阵描述了原图像中点的坐标如何变换到目标图像中的新坐标。线性变换包括旋转、缩放和错切(或称为剪切)。

  • 旋转:图像围绕某一点旋转一定的角度。
  • 缩放:图像在x轴和y轴方向上分别进行放大或缩小。
  • 错切:图像在x轴或y轴方向上发生倾斜。

平移变换定义

平移变换是指图像在x轴和y轴方向上分别移动一定的距离。平移变换不能通过2x2矩阵单独表示,因为它涉及到坐标的加法,而不是线性变换中的乘法。因此,在仿射变换中,我们通常使用一个2x3的矩阵来表示线性变换和平移变换的组合。

那么,为什么呢?开篇所展示的矩阵可以转化成以下方程组:

 看到这个公式可以理解为什么‘‘参数c和f控制了图片的平移变换,剩下的四个参数则控制线性变换’‘了吧。并且我们也可以知道仿射矩阵应该怎么求,那就是求方程组。

先在图中选取三个两两互不共线的点,再提供希望这三个点映射到的三个点。以向上和向右平移两个像素为例

 \Rightarrow

由这六个点一一对应可以写出六个方程,通过解方程组就可以得到a~f。

比如点(1, 0)和点(3, 2),可以写出:

得到的矩阵就是我们需要的仿射矩阵 

可以看出这是通过三个两两互不共线的点的映射得到的参数,如果映射的点与原图像的三个的不在同一个平面,就等于将图像映射到另一个平面。

getAffineTransform()和getRotationMatrix2D()

这种计算方式对应了OpenCV中的getAffineTransform(),OpenCV还提供了一种用于计算二维旋转变换矩阵的函数getRotationMatrix2D()。我们就顺便分析一下吧。

之前介绍的计算方法需要提供(x, y)和(x', y'),这个方法需要我们提供旋转中心C和旋转角度θ,采用逆时针旋转。上图有点小问题,CA和CA'的长度应该是一样的,图上不一样,懒得重新画了,凑合看吧。CA=CA'=L,可以得出:

把(3)和(4)代入(1)(2)中即可消除L和α:

 即可提取矩阵:

代码实现

1.OpenCV中的仿射变换

(1)cv.getAffineTransform()

cv.getPerspectiveTransform(src, dst, solveMethod) 

src:源图像中的四个点的坐标。这个参数是一个包含三个点坐标的二维数组或列表,格式为 [[x1, y1], [x2, y2], [x3, y3]],其中每个 [xi, yi] 表示一个点的坐标。

dst:目标图像中相应的四个点的坐标。这个参数也是一个包含三个点坐标的二维数组或列表,格式同样为 [[x1, y1], [x2, y2], [x3, y3]]

solveMethod:这是一个可选参数,用于指定求解透视变换矩阵的方法。在 OpenCV 中,默认的求解方法是 DECOMP_LU,它使用 LU 分解法来求解线性方程组。通常情况下,你可以使用默认值,除非你有特定的需求或优化要求。

示例代码:

import cv2 as cv
import numpy as np

pts1 = np.array([[1, 0], [0, 1], [0, 0]], np.float32)
pts2 = np.array([[3, 2], [2, 3], [2, 2]], np.float32)

M = cv.getAffineTransform(pts1, pts2)
print(M)

输出:

[[1. 0. 2.]
 [0. 1. 2.]]

(2)cv.getRotationMatrix2D()

cv.getRotationMatrix2D(center, angle, scale)

center:旋转的中心点。这是一个二元组 (x, y),表示旋转操作的中心点坐标。例如,如果 center 是 [100, 100],那么图像将围绕 (100, 100) 这个点进行旋转。

angle:旋转的角度。这是一个实数,表示图像需要顺时针旋转的角度,单位是度。例如,如果 angle 是 90,则图像将顺时针旋转 90 度。

scale:缩放因子。这是一个实数,表示在旋转图像时的缩放比例。如果 scale 是 1.0,则图像在旋转时保持原始大小。如果 scale 小于 1.0,则图像在旋转时缩小;如果 scale 大于 1.0,则图像在旋转时放大。

示例代码:

import cv2 as cv
import numpy as np

pts1 = np.array([[1, 0], [0, 1], [0, 0]], np.float32)
pts2 = np.array([[3, 2], [2, 3], [2, 2]], np.float32)

M = cv.getRotationMatrix2D((100, 100), 30, 1)
print(M)

输出:

[[  0.8660254    0.5        -36.60254038]
 [ -0.5          0.8660254   63.39745962]]

(3)cv.warpAffine()

cv.warpAffine(src, M, dsize, dst, flags, borderMode, borderValue) 

src:输入图像,必须是单通道或三通道的8位或32位浮点型图像。

dst:输出图像,用于存储仿射变换后的结果。其大小和类型与输入图像相同。

M:2x3的变换矩阵。这个矩阵定义了仿射变换的类型和参数。

dsize:输出图像的大小。如果这个参数为默认值,则输出图像的大小将与输入图像相同。

flags:插值方法,用于指定在计算输出图像像素值时应使用的插值方法。默认为INTER_LINEAR,即双线性插值。其他可用的选项包括INTER_NEAREST(最近邻插值)、INTER_AREA(区域插值)、INTER_CUBIC(三次样条插值)等。

borderMode:像素外推法,用于指定当变换后的像素坐标超出输入图像边界时应如何处理。默认为BORDER_CONSTANT,即使用常数值填充边界。

borderValue:用于填充边界的常数值,当borderMode为BORDER_CONSTANT时有效。

 示例代码:

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

pts1 = np.array([[1, 0], [0, 1], [0, 0]], np.float32)
pts2 = np.array([[3, 2], [2, 3], [2, 2]], np.float32)

M1 = cv.getAffineTransform(pts1, pts2)
M2 = cv.getRotationMatrix2D((100, 100), 30, 1)

img = cv.imread(r'F:\Python\2024_last\image\2.jpg')
h, w = img.shape[:2]
dst1 = cv.warpAffine(img, M1, (h, w))
dst2 = cv.warpAffine(img, M2, (h, w))
fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 8), dpi=100)
axes[0].imshow(img[::-1, ::-1, ::-1])
axes[0].set_title('原图')
axes[0].set_xlim(0, 800)
axes[0].set_ylim(0, 800)
axes[1].imshow(dst1[::-1, ::-1, ::-1])
axes[1].set_title('M1')
axes[1].set_xlim(0, 800)
axes[1].set_ylim(0, 800)
axes[2].imshow(dst2[::-1, ::-1, ::-1])
axes[2].set_title('M2')
axes[2].set_xlim(0, 800)
axes[2].set_ylim(0, 800)
plt.show()

输出:

 2.源码重现

(1)cv.getAffineTransform()

代码:

from scipy import linalg
import numpy as np
import cv2 as cv

def mygetAffineTransform(pts1, pts2):
    A = np.hstack((pts1, np.array([1, 1, 1]).reshape(-1, 1)))
    b1 = pts2[:, 0]
    b2 = pts2[:, 1]
    r1 = linalg.solve(A, b1)
    r2 = linalg.solve(A, b2)
    M = np.array([
        r1.T,
        r2.T
    ])
    return M

pts1 = np.array([[1, 0], [0, 1], [0, 0]], np.float32)
pts2 = np.array([[3, 2], [2, 3], [2, 2]], np.float32)
M1 = mygetAffineTransform(pts1, pts2)
M2 = cv.getAffineTransform(pts1, pts2)
print(M1)
print('------------------')
print(M2)

输出:


[[1. 0. 2.]
 [0. 1. 2.]]
------------------
[[1. 0. 2.]
 [0. 1. 2.]]

(2)cv.getRotationMatrix2D()

代码: 

import math
import cv2 as cv
import numpy as np

def mygetRotationMatrix2D(center, angle, scale):
    cos_angle = math.cos(angle / 180.0 * math.pi) * scale
    sin_angle = math.sin(angle / 180.0 * math.pi) * scale
    M = np.array([
        [cos_angle,  sin_angle, center[0] * (1 - cos_angle) - center[1] * sin_angle],
        [-sin_angle, cos_angle, center[1] * (1 - cos_angle) + center[0] * sin_angle]
    ])
    return M
M1 = mygetRotationMatrix2D((100, 100), 30, 2)
M2 = cv.getRotationMatrix2D((100, 100), 30, 2)
print(M1)
print('------------------')
print(M2)

 输出:

[[   1.73205081    1.         -173.20508076]
 [  -1.            1.73205081   26.79491924]]
------------------
[[   1.73205081    1.         -173.20508076]
 [  -1.            1.73205081   26.79491924]]

(3)cv.warpAffine()

代码:

import numpy as np
import cv2 as cv
from numpy.linalg import inv
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

def _invertAffineTransform(matrix):
    """cv.invertAffineTransform(). 本质是求逆

    :param matrix: shape[2, 3]. float32
    :return: shape[2, 3]
    """
    matrix = np.concatenate([matrix, np.array([0, 0, 1], dtype=matrix.dtype)[None]])  # for求逆
    return inv(matrix)[:2]

def _warpAffine(x, matrix, dsize=None, flags=None):
    """cv.warpAffine(borderMode=None, borderValue=(114, 114, 114))

    :param x: shape[H, W, C]. uint8
    :param matrix: 仿射矩阵. shape[2, 3]. float32
    :param dsize: Tuple[W, H]. 输出的size
    :param flags: cv.WARP_INVERSE_MAP. 唯一可选参数
    :return: shape[dsize[1], dsize[0], C]. uint8
    """
    dsize = dsize or (x.shape[1], x.shape[0])  # 输出的size
    borderValue = np.array((114, 114, 114), dtype=x.dtype)  # 背景填充
    if flags is None or flags & cv.WARP_INVERSE_MAP == 0:  # flags无cv.WARP_INVERSE_MAP参数
        matrix = _invertAffineTransform(matrix)
    grid_x, grid_y = np.meshgrid(np.arange(dsize[0]), np.arange(dsize[1]))  # np.int32
    src_x = (matrix[0, 0] * grid_x + matrix[0, 1] * grid_y + matrix[0, 2]).round().astype(np.int32)  # X
    src_y = (matrix[1, 0] * grid_x + matrix[1, 1] * grid_y + matrix[1, 2]).round().astype(np.int32)  # Y
    src_x_clip = np.clip(src_x, 0, x.shape[1] - 1)  # for索引合法
    src_y_clip = np.clip(src_y, 0, x.shape[0] - 1)
    output = np.where(((0 <= src_x) & (src_x < x.shape[1]) & (0 <= src_y) & (src_y < x.shape[0]))[:, :, None],
                      x[src_y_clip, src_x_clip], borderValue[None, None])  # 广播机制
    return output

pts1 = np.array([[1, 0], [0, 1], [0, 0]], np.float32)
pts2 = np.array([[3, 2], [2, 3], [2, 2]], np.float32)

M1 = cv.getAffineTransform(pts1, pts2)
M2 = cv.getRotationMatrix2D((100, 100), 30, 1)
img = cv.imread(r'F:\Python\2024_last\image\2.jpg')
dst = _warpAffine(img, M2)
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 8), dpi=100)
axes[0].imshow(img[::-1, ::-1, ::-1])
axes[0].set_title('原图')
axes[0].set_xlim(0, 800)
axes[0].set_ylim(0, 800)
axes[1].imshow(dst[::-1, ::-1, ::-1])
axes[1].set_title('M2')
axes[1].set_xlim(0, 800)
axes[1].set_ylim(0, 800)

输出:

​参考文献:


原文链接:https://blog.csdn.net/u011681952/article/details/98942207

原文链接:https://blog.csdn.net/qq_36694133/article/details/131240306

原文链接:https://blog.csdn.net/qq_40939814/article/details/117966835

# 学习笔记,欢迎各位大佬指正

;