仿射变换原理
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控制了图片的平移变换,剩下的四个参数则控制线性变换’‘了吧。并且我们也可以知道仿射矩阵应该怎么求,那就是求方程组。
先在图中选取三个两两互不共线的点,再提供希望这三个点映射到的三个点。以向上和向右平移两个像素为例
由这六个点一一对应可以写出六个方程,通过解方程组就可以得到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
# 学习笔记,欢迎各位大佬指正