文章目录
前言
本章主要讲解图像的一些基本运算及仿射变换以及透视变换。
一、图像运算
1. 加减乘除
-
图像相加
#图像直接相加 imgC = imgA + imgB #opencv方式 cv2.add(imgA, imgB) cv2.addWeighted(imgA, alpha, imgB, beta, gamma)
imgA + imgB
:当其和大于一个字节时, 大于一个字节的位数将被丢失,类似于取模。
( A + B ) % 256 (A + B) \% 256 (A+B)%256cv2.add(imgA, imgB)
:当数值超过255
时,取值为255
m i n ( A + B , 255 ) min(A + B, 255) min(A+B,255)cv2.addWeighted(imgA, alpha, imgB, beta, gamma)
:
m i n ( r o u n d ( A ∗ α + B ∗ β + γ ) , 255 ) min(round(A * \alpha + B * \beta + \gamma), 255) min(round(A∗α+B∗β+γ),255)
图像相加的应用:针对降噪的带噪图像相加(平均)可参看 数字图像处理(第三版)—冈萨雷斯
-
图像相减
与相加类似#图像直接相减 imgC = imgA - imgB #opencv方式 sub = cv2.subtract(img2, img1)
-
imgA - imgB
:通常图像的类型为8位无符号整型,当涉及到一个小数减一个大数时,会运用到补码的相关运算,并最终转换成一个无符号八位整型 -
cv2.subtract(img2, img1)
:当数值小于0时,取值为0
m a x ( A − B , 0 ) max(A - B, 0) max(A−B,0)
图像相减的应用:增强差别的图像相减 可参看 数字图像处理(第三版)—冈萨雷斯
-
-
图像相乘
图像相乘有点乘与叉乘
在计算机视觉中,图像的点乘和叉乘是两种不同的运算方式。-
图像的点乘(也称为哈达玛积或按位乘)
点乘是一种逐像素地乘以两张图像的相应像素所得到的结果,并表示为
Dst(I)=Src1(I)⋅Src2(I)
,其中Dst
表示输出图像,Src1
和Src2
表示输入图像,I表示图像中的一个像素位置。在OpenCV中,可以使用 cv2.multiply() 函数来实现图像的点乘运算。下面是一段代码示例:
import cv2 # 加载两个图像 img1 = cv2.imread('image1.jpg') img2 = cv2.imread('image2.jpg') # 点乘运算 res = cv2.multiply(img1, img2) # 显示结果 cv2.imshow('Result', res) cv2.waitKey(0) cv2.destroyAllWindows()
逐元素乘积常常用于图像的调整和处理。例如,可以将两张图像的对应像素值进行逐元素乘积,得到两张图像的混合效果。另外,逐元素乘积也可以用于进行图像的掩模操作,即使用一个掩模矩阵对图像矩阵进行加权处理,从而达到一些特定的效果
-
矩阵的叉乘(也称为向量积或“交叉乘积”)
假设有两个矩阵 A A A 和 B B B,它们的维度分别为 m × n m \times n m×n 和 n × p n \times p n×p,则它们的乘积 C = A B C = AB C=AB 的维度为 m × p m \times p m×p。矩阵乘法的计算公式为: C i j = ∑ k = 1 n A i k B k j C_{ij} = \sum_{k=1}^n A_{ik} B_{kj} Cij=k=1∑nAikBkj其中 C i j C_{ij} Cij 是矩阵 C C C 的第 i i i 行、第 j j j 列的元素, A i k A_{ik} Aik 是矩阵 A A A 的第 i i i 行、第 k k k 列的元素, B k j B_{kj} Bkj 是矩阵 B B B 的第 k k k 行、第 j j j 列的元素。在图像处理中,矩阵乘法常常被用于进行变换操作,例如将图像进行平移、缩放、旋转等操作。为了进行这些操作,我们可以构建变换矩阵,然后将这个矩阵乘以图像矩阵,得到变换后的图像矩阵。
-
总的来说,矩阵乘法和逐元素乘积都在图像处理中有着重要的作用。但是,在对图像进行几何变换方面,我们通常使用矩阵乘法,而在进行像素-level的调整和处理方面,我们通常使用逐元素乘积。
-
图像除法
图像相除也是一种常见的图像处理操作,它通常用于图像增强或去除伪影。图像相除的基本原理是将一张图像的像素值除以另一张图像的像素值,从而得到一个新的图像矩阵。具体来说,假设有两张相同大小的图像 A A A 和 B B B,它们的对应像素值分别为 A i j A_{ij} Aij 和 B i j B_{ij} Bij,则它们的像素相除得到的新图像的像素值为: C i j = A i j B i j C_{ij} = \frac{A_{ij}}{B_{ij}} Cij=BijAij
需要注意的是,当除数的像素值为0时,相应位置的结果将可能会出现不确定或非数值的情况。因此,在进行图像相除时,需要注意避免除数为0的情况。
使用图像相除可以有效地去除图像中的光照变化或噪声等影响,从而得到更加清晰、准确的图像信息。例如,在医学图像处理中,可以通过将两张X光片的像素相除得到更加清晰的影像,进而提高对病情的诊断准确度。
2. 位运算
# 与运算
bitwise_and(src1:image, src2:image[, dst[, mask]]) -> dst
# 或运算
bitwise_or(src1:image, src2:image[, dst[, mask]]) -> dst
# 异或运算
bitwise_xor(src1:image, src2:image[, dst[, mask]]) -> dst
# 非
bitwise_not(src1:image[, dst[, mask]]) -> dst
- 与、或、异或: 实质就是两个图像数组,相同位置的数据直接进行与、或、异或运算
- 与: 图片整体会变暗,与运算会将值变小,不超过255
- 或: 图片亮度会整体变量,或运算会将值变大,不超过255
- 非: 与程序中按位取反不一样, OpenCV中实现的是对颜色的反转
d s t = 255 − s r c dst = 255 - src dst=255−src
二、翻转与旋转
1.翻转
# 翻转
# flip(src, flipCode[, dst]) -> dst
# flipCode = 0:垂直翻转
# flipCode < 0:垂直 + 水平翻转
# flipCode > 0:水平翻转
imgVertical = cv2.flip(img, 0)
imgVAndH = cv2.flip(img, -1)
imgHorizontal = cv2.flip(img, 1)
res = np.hstack((img, imgVertical, imgHorizontal, imgVAndH))
2.旋转
作用: 以图片为中心, 对图片进行旋转,角度只能为180, 顺时针90,逆时针90。
# 旋转
# rotate(src, rotateCode[, dst]) -> dst
# roteCode:cv2.ROTATE_
imgClockWise90 = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
imgCounterClockWise90 = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)
imgRotate180 = cv2.rotate(img, cv2.ROTATE_180)
注:翻转不会改变原来图片的
np.ndarray.shape
,旋转会改变。
三、仿射变换
1. 介绍
仿射变换是一种二维坐标变换方法,它能够将平面上的点进行线性变换和平移,从而得到一个新的图形。常见的仿射变换包括缩放、旋转、平移、翻转
等变换。
在仿射变换中,原图形的每个点都被映射到新的图形中,并按照一定的规则进行变换。这些规则由仿射矩阵来描述,仿射矩阵包括平移、旋转、缩放和剪裁等操作。
在计算机视觉和图像处理领域中,仿射变换被广泛应用于图像的变形、畸变校正、目标跟踪和图像配准等方面。
仿射变换中的一些性质保持不变:
- 共线性: 若几个点在变换前在一条直线上,则变换后仍在一条直线上。
- 平行性: 若某两条直线在变换前是平行的,则变换后仍是平行的。
- 共线比例不变性: 变换前在一条线上的两条线段的比例在变换前后不变。
2. 变换的数学表达
2.1 平移
将原图的每一个像素坐标进行移动,其数学表达就为:
{
x
=
x
c
+
t
x
y
=
y
c
+
t
y
\left\{\begin{aligned} x = x_c + t_x \\y = y_c + t_y\end{aligned}\right.
{x=xc+txy=yc+ty
(
x
c
,
y
c
)
(x_c, y_c)
(xc,yc)为原图像素的坐标,
(
x
,
y
)
(x, y)
(x,y)为变换后的像素坐标,
t
x
,
t
y
t_x, t_y
tx,ty表示坐标x与坐标y移动的距离。将上面公式改写成矩阵形式:
[
x
y
]
=
[
1
0
0
1
]
[
x
c
y
c
]
+
[
t
x
t
y
]
\begin{bmatrix} x\\ y\end{bmatrix}=\begin{bmatrix} 1 & 0\\ 0&1\end{bmatrix}\begin{bmatrix} x_c\\ y_c\end{bmatrix}+\begin{bmatrix} t_x\\ t_y\end{bmatrix}
[xy]=[1001][xcyc]+[txty]
2.2 缩放
将原图的像素坐标进行缩放,其数学表达式就是:
{
x
=
x
c
⋅
α
x
y
=
y
c
⋅
α
y
\left\{\begin{aligned} x = x_c \cdot \alpha_x \\y = y_c \cdot \alpha_y\end{aligned}\right.
{x=xc⋅αxy=yc⋅αy
其用矩阵可表示为:
[
x
y
]
=
[
α
x
0
0
α
y
]
[
x
c
y
c
]
\begin{bmatrix} x\\ y\end{bmatrix}=\begin{bmatrix} \alpha_x & 0\\ 0&\alpha_y\end{bmatrix}\begin{bmatrix} x_c\\ y_c\end{bmatrix}
[xy]=[αx00αy][xcyc]
2.3 旋转
由几何关系可得方程组:
{
x
=
c
o
s
(
α
+
θ
)
⋅
r
y
=
s
i
n
(
α
+
θ
)
⋅
r
x
c
=
c
o
s
α
⋅
r
y
c
=
s
i
n
α
⋅
r
\left\{\begin{aligned} &x = cos(\alpha + \theta) \cdot r& \\ &y = sin(\alpha + \theta) \cdot r& \\ &x_c = cos\alpha \cdot r&\\ &y_c = sin\alpha \cdot r& \end{aligned}\right.
⎩
⎨
⎧x=cos(α+θ)⋅ry=sin(α+θ)⋅rxc=cosα⋅ryc=sinα⋅r
化简解得:
{
x
=
x
c
⋅
c
o
s
θ
−
y
c
⋅
s
i
n
θ
y
=
x
c
⋅
s
i
n
θ
+
y
c
⋅
c
o
s
θ
\left\{\begin{aligned} &x = x_c \cdot cos\theta - y_c \cdot sin\theta& \\ &y = x_c \cdot sin\theta + y_c \cdot cos\theta\\\end{aligned}\right.
{x=xc⋅cosθ−yc⋅sinθy=xc⋅sinθ+yc⋅cosθ
矩阵形式:
[
x
y
]
=
[
c
o
s
θ
−
s
i
n
θ
s
i
n
θ
c
o
n
θ
]
[
x
c
y
c
]
\begin{bmatrix} x\\ y\end{bmatrix} = \begin{bmatrix} cos\theta & -sin\theta \\ sin\theta & con\theta \end{bmatrix}\begin{bmatrix} x_c \\ y_c\end{bmatrix}
[xy]=[cosθsinθ−sinθconθ][xcyc]
3. 变换矩阵
上一节中我们推导了平移、缩放以及旋转的数学表达公式并写了矩阵形式:
[
x
y
]
=
[
1
0
0
1
]
[
x
c
y
c
]
+
[
t
x
t
y
]
\begin{bmatrix} x\\ y\end{bmatrix}=\begin{bmatrix} 1 & 0\\ 0&1\end{bmatrix}\begin{bmatrix} x_c\\ y_c\end{bmatrix}+\begin{bmatrix} t_x\\ t_y\end{bmatrix}
[xy]=[1001][xcyc]+[txty]
[
x
y
]
=
[
α
x
0
0
α
y
]
[
x
c
y
c
]
\begin{bmatrix} x\\ y\end{bmatrix}=\begin{bmatrix} \alpha_x & 0\\ 0&\alpha_y\end{bmatrix}\begin{bmatrix} x_c\\ y_c\end{bmatrix}
[xy]=[αx00αy][xcyc]
[
x
y
]
=
[
c
o
s
θ
−
s
i
n
θ
s
i
n
θ
c
o
n
θ
]
[
x
c
y
c
]
\begin{bmatrix} x\\ y\end{bmatrix} = \begin{bmatrix} cos\theta & -sin\theta \\ sin\theta & con\theta \end{bmatrix}\begin{bmatrix} x_c \\ y_c\end{bmatrix}
[xy]=[cosθsinθ−sinθconθ][xcyc]
从中可以看出缩放和旋转都能通过一个2×2的矩阵进行变换,而平移带有一个偏移项,为了能够统一变换矩阵形式,就可以将平移改写成为:
[
x
y
1
]
=
[
1
0
t
X
0
1
t
X
0
0
1
]
[
x
c
y
c
1
]
\begin{bmatrix} x \\ y \\ 1\end{bmatrix} = \begin{bmatrix} 1 & 0 & t_X \\ 0 & 1 & t_X \\0 & 0 & 1 \\\end{bmatrix}\begin{bmatrix} x_c \\ y_c \\ 1\end{bmatrix}
xy1
=
100010tXtX1
xcyc1
同理,也可将旋转和缩放改写为3×3矩阵形式:
[
x
y
1
]
=
[
α
x
0
0
0
α
y
0
0
0
1
]
[
x
c
y
c
1
]
\begin{bmatrix} x\\ y\\ 1\end{bmatrix}=\begin{bmatrix} \alpha_x & 0 & 0\\ 0&\alpha_y & 0 \\ 0 & 0 & 1\end{bmatrix}\begin{bmatrix} x_c\\ y_c\\ 1\end{bmatrix}
xy1
=
αx000αy0001
xcyc1
[
x
y
1
]
=
[
c
o
s
θ
−
s
i
n
θ
0
s
i
n
θ
c
o
s
θ
0
0
0
1
]
[
x
c
y
c
1
]
\begin{bmatrix} x\\ y \\ 1\end{bmatrix} = \begin{bmatrix} cos\theta & -sin\theta & 0 \\ sin\theta & cos\theta & 0\\ 0 & 0 & 1 \end{bmatrix}\begin{bmatrix} x_c \\ y_c \\ 1\end{bmatrix}
xy1
=
cosθsinθ0−sinθcosθ0001
xcyc1
综上,旋转、平移、缩放都可以通过一个3×3
的变换矩阵实现坐标变换。该变换矩阵还能实现切变、对称以及翻转操作。
4. 变换叠加
上午所述变换都是实现图像的单步变换,通过变换矩阵就可以实现更便捷的多步变换。假设旋转的变换矩阵为R,平移的变换矩阵为M,缩放的变换矩阵为S,那么旋转、平移以及缩放的组合变换就能写为:
[
x
y
1
]
=
M
k
R
j
S
i
⋅
⋅
⋅
M
1
R
1
S
1
[
x
c
y
c
1
]
\begin{bmatrix} x\\ y \\ 1\end{bmatrix} = M_kR_jS_i \cdot \cdot \cdot M_1R_1S_1 \begin{bmatrix} x_c \\ y_c \\ 1\end{bmatrix}
xy1
=MkRjSi⋅⋅⋅M1R1S1
xcyc1
组合变换的变换矩阵顺序为从右往左
, 这样就能首先计算出整体的变换矩阵,然后才进行像素的坐标变换。
5. 变换矩阵的逆推
逆推两张相同图片之间的仿射变换矩阵,最少需要3组对应像素的坐标点。
变换矩阵的形式可以表示为:
[
x
y
1
]
=
[
a
11
a
12
a
13
a
21
a
22
a
23
0
0
1
]
[
x
c
y
c
1
]
\begin{bmatrix} x\\ y\\ 1\end{bmatrix}=\begin{bmatrix} a_{11} & a_{12} & a_{13}\\ a_{21}&a_{22} & a_{23} \\ 0 & 0 & 1\end{bmatrix}\begin{bmatrix} x_c\\ y_c\\ 1\end{bmatrix}
xy1
=
a11a210a12a220a13a231
xcyc1
总共6个
未知数,至少需要6个方程才能解出,然而1对
坐标点对应2个
方程,因此最少需要3组
对应坐标点才能求出变换矩阵。
6.OpenCV实现以及手动实现
6.1 手动实现
-
平移实现:
import cv2 import numpy as np canvas = np.zeros(shape=(400, 400, 3), dtype=np.uint8) img = cv2.imread("F:\MyOpenCV\hello.jpg") img = cv2.resize(img, dsize=(200, 200)) offsetH = (canvas.shape[0] - img.shape[0]) // 2 offsetW = (canvas.shape[1] - img.shape[1]) // 2 canvas[offsetH : offsetH + img.shape[0], offsetW : offsetW + img.shape[1]] = img def move(tx, ty, img): canvasNew = np.zeros(shape=(400, 400, 3), dtype=np.uint8) #定义一个新的画布 C = np.array([[1, 0, tx], [0, 1, ty], [0, 0, 1]]) #定义变换矩阵 for xc in range(img.shape[0]): #遍历原画布每一个像素 for yc in range(img.shape[1]): a = np.array([xc, yc, 1]) #构造向量 (x, y, t) = C @ a #叉积 if x >= 400 or y >= 400: #移除出的丢弃 continue canvasNew[x, y] = img[xc, yc] #更新画布 return canvasNew imgNew = move(200, 0, canvas) cv2.imshow("img", canvas) cv2.imshow("imgNew", imgNew) cv2.waitKey(0)
执行结果:
这里暂时不太清楚 为什么平移的方向与预想的相反。
-
旋转实现:
这里仅实现饶左上角的逆时针旋转,即(0,0)点为旋转点。
import cv2 import numpy as np import math canvas = np.zeros(shape=(400, 400, 3), dtype=np.uint8) img = cv2.imread("F:\MyOpenCV\hello.jpg") img = cv2.resize(img, dsize=(200, 200)) offsetH = (canvas.shape[0] - img.shape[0]) // 2 offsetW = (canvas.shape[1] - img.shape[1]) // 2 canvas[offsetH : offsetH + img.shape[0], offsetW : offsetW + img.shape[1]] = img def rotate(de, img): canvasNew = np.zeros(shape=(400, 400, 3), dtype=np.uint8) # 定义一个新的画布 C = np.array( [[math.cos(de), -math.sin(de), 0], [math.sin(de), math.cos(de), 0], [0, 0, 1]] ) # 定义变换矩阵 for xc in range(img.shape[0]): # 遍历原画布每一个像素 for yc in range(img.shape[1]): a = np.array([xc, yc, 1]) # 构造向量 (x, y, t) = C @ a # 叉积 if x >= 400 or y >= 400: # 移除出的丢弃 continue canvasNew[int(x), int(y)] = img[xc, yc] # 更新画布 return canvasNew imgNew = rotate(math.pi / 6, canvas) cv2.imshow("img", canvas) cv2.imshow("imgNew", imgNew) cv2.waitKey(0)
疑惑:不太明白为啥会有黑点。。
-
缩放实现:
import cv2 import numpy as np import math canvas = np.zeros(shape=(400, 400, 3), dtype=np.uint8) img = cv2.imread("F:\MyOpenCV\hello.jpg") img = cv2.resize(img, dsize=(200, 200)) offsetH = (canvas.shape[0] - img.shape[0]) // 2 offsetW = (canvas.shape[1] - img.shape[1]) // 2 canvas[offsetH : offsetH + img.shape[0], offsetW : offsetW + img.shape[1]] = img def zoom(fx, fy, img): canvasNew = np.zeros(shape=(400, 400, 3), dtype=np.uint8) # 定义一个新的画布 C = np.array([[fx, 0, 0], [0, fy, 0], [0, 0, 1]]) # 定义变换矩阵 for xc in range(img.shape[0]): # 遍历原画布每一个像素 for yc in range(img.shape[1]): a = np.array([xc, yc, 1]) # 构造向量 (x, y, t) = C @ a # 叉积 if x >= 400 or y >= 400: # 移除出的丢弃 continue canvasNew[int(x), int(y)] = img[xc, yc] # 更新画布 return canvasNew imgNew = rotate(1.5, 1.5, canvas) cv2.imshow("img", canvas) cv2.imshow("imgNew", imgNew) cv2.waitKey(0)
6.2OpenCV实现
import cv2
import numpy as np
import math
# 逆时针旋转转变换矩阵 用于产生旋转矩阵
# center,旋转中心
# angle,逆时针旋转角度
# scale,图片缩放值
# cv2.getRotationMatrix2D: (center: tuple, angle, scale) -> dst
# M :仿射变换矩阵
# dsize :输出图片的大小
# flags:图片的插值算法,默认算法就不错
# borderMode:查看 图像边界扩展 小节
# cv2.warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue]]]]) -> dst
# 仿射变换矩阵逆推
# src:3x2 的 numpy.ndarray 矩阵,数据类型为 np.float
# dst:3x2 的 numpy.ndarray 矩阵,数据类型为 np.float
# cv2.getAffineTransform(src, dst) -> M
canvas = np.zeros(shape=(400, 400, 3), dtype=np.uint8)
img = cv2.imread("F:\MyOpenCV\hello.jpg")
img = cv2.resize(img, dsize=(200, 200))
offsetH = (canvas.shape[0] - img.shape[0]) // 2
offsetW = (canvas.shape[1] - img.shape[1]) // 2
canvas[offsetH : offsetH + img.shape[0], offsetW : offsetW + img.shape[1]] = img
dsize = (400, 400)
def move():
C = np.array([[1, 0, 200], [0, 1, 0]], dtype=np.float32) # 旋转矩阵 旋转矩阵为2乘3
img = cv2.warpAffine(canvas, C, dsize)
cv2.imshow("imgMove", img)
def rotate():
de = 30
C = cv2.getRotationMatrix2D((0, 0), de, scale=1)
img = cv2.warpAffine(canvas, C, dsize)
cv2.imshow("imgRotate", img)
def zoom():
C = np.array([[1.5, 0, 0], [0, 1.5, 0]]) # 旋转矩阵
img = cv2.warpAffine(canvas, C, dsize)
cv2.imshow("imgZoom", img)
move()
rotate()
zoom()
cv2.waitKey(0)
7.向量空间----补充
考研线代学过向量空间 与这个有点相关在此总结一下。
名词解释及相关定理:
- 向量空间:全体n维向量连同向量的加法和数乘运算合成为n维向量空间。
- 如果向量空间V中的m个向量
α
1
,
α
2
,
⋅
⋅
⋅
,
α
m
\alpha_1,\alpha_2,\cdot\cdot\cdot,\alpha_m
α1,α2,⋅⋅⋅,αm满足
- α 1 , α 2 , ⋅ ⋅ ⋅ , α m \alpha_1,\alpha_2,\cdot\cdot\cdot,\alpha_m α1,α2,⋅⋅⋅,αm线性无关;
- V中任意向量
β
\beta
β均可由向量组
α
1
,
α
2
,
⋅
⋅
⋅
,
α
m
\alpha_1,\alpha_2,\cdot\cdot\cdot,\alpha_m
α1,α2,⋅⋅⋅,αm线性表出,即
x
1
α
1
+
x
2
α
2
+
…
+
x
m
α
m
=
β
x_1\alpha_1 + x_2\alpha_2 + …+x_m\alpha_m = \beta
x1α1+x2α2+…+xmαm=β则称
α
1
,
α
2
,
⋅
⋅
⋅
,
α
m
\alpha_1,\alpha_2,\cdot\cdot\cdot,\alpha_m
α1,α2,⋅⋅⋅,αm为向量空间V的一个
基底
(或称基
).基中所含向量的个数m称为向量空间V的维数.向量 β \beta β的表示系数 x 1 , x 2 , … , x m x_1,x_2,…,x_m x1,x2,…,xm称为向量 β \beta β在基底 α 1 , α 2 , ⋅ ⋅ ⋅ , α m \alpha_1,\alpha_2,\cdot\cdot\cdot,\alpha_m α1,α2,⋅⋅⋅,αm下的坐标。
- 规范正交基:基向量中每两个向量之间相互正交,如直角坐标系的坐标轴可以取出规范正交基。
- 如果一个基底能由另一个基底乘以一个矩阵表出,则称该矩阵为过渡矩阵
[ β 1 , β 2 , … , β n ] = [ α 1 , α 2 , … , α n ] C [\beta_1,\beta_2,…,\beta_n] = [\alpha_1,\alpha_2,…,\alpha_n]C [β1,β2,…,βn]=[α1,α2,…,αn]C
其中, C = [ c 11 c 12 … c 1 n c 21 c 22 … c 2 n ⋮ ⋮ ⋮ c n 1 c n 1 … c n n ] C = \begin{bmatrix} c_{11} & c_{12} & \dots & c_{1n}\\ c_{21} & c_{22} & \dots & c_{2n}\\ \vdots & \vdots & & \vdots \\ c_{n1} & c_{n1} & \dots &c_{nn}\end{bmatrix}_{} C= c11c21⋮cn1c12c22⋮cn1………c1nc2n⋮cnn 注:过渡矩阵是对基向量做相应的变换,因此一定是一组基向量
右乘
过渡矩阵,且过渡矩阵一定可逆。 - 如果向量
γ
\gamma
γ在基底
α
1
,
α
2
,
…
,
α
n
\alpha_1,\alpha_2,…,\alpha_n
α1,α2,…,αn的坐标为
x
1
,
x
2
,
…
,
x
n
x_1,x_2,\dots,x_n
x1,x2,…,xn,向量
γ
\gamma
γ在基底
β
!
,
β
2
,
…
,
β
n
\beta_!,\beta_2,\dots,\beta_n
β!,β2,…,βn的坐标为
y
1
,
y
2
,
…
,
y
n
y_1,y_2,\dots,y_n
y1,y2,…,yn,则坐标变换公式为
[ x 1 x 2 ⋮ x n ] = C [ y 1 y 2 ⋮ y n ] 或 [ y 1 y 2 ⋮ y n ] = C − 1 [ x 1 x 2 ⋮ x n ] 或 x = C y \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_n\end{bmatrix}=C \begin{bmatrix}y_1 \\ y_2 \\ \vdots \\ y_n\end{bmatrix} 或\begin{bmatrix}y_1 \\ y_2 \\ \vdots \\ y_n\end{bmatrix}=C^{-1} \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_n\end{bmatrix}或\mathbf{x} = C\mathbf{y} x1x2⋮xn =C y1y2⋮yn 或 y1y2⋮yn =C−1 x1x2⋮xn 或x=Cy注:向量(坐标)的变换一定是
左乘
矩阵,具体是乘过渡矩阵还是过渡矩阵的逆,需要看是哪个基向量到哪个基向量的过渡矩阵。
四、透视变换
1. 齐次坐标
[
x
y
1
]
=
[
a
11
a
12
a
13
a
21
a
22
a
23
0
0
1
]
[
x
c
y
c
1
]
\begin{bmatrix} x\\ y\\ 1\end{bmatrix}=\begin{bmatrix} a_{11} & a_{12} & a_{13}\\ a_{21}&a_{22} & a_{23} \\ 0 & 0 & 1\end{bmatrix}\begin{bmatrix} x_c\\ y_c\\ 1\end{bmatrix}
xy1
=
a11a210a12a220a13a231
xcyc1
仿射变换中,用来表示二维像素位置的坐标为:
[
x
y
1
]
\begin{bmatrix} x\\ y\\ 1\end{bmatrix}
xy1
从形式上来说,这就是用了三维坐标来表示二维坐标。这种用n
维坐标表达n-1
维坐标就称为齐次坐标
2. 透视
透视本是一种画法技巧,通过在平面上画出三维物体来创造视觉上的深度感和立体感。透视的基本原理是,当人们观看物体时,视点和视线两者之间的距离会影响视觉体验。离视点和视线较近的物体看起来会比离得远的物体更大,更接近观察者。
而透视变换能够让一个倾斜的物体,以一个新的视角展现。
对于像素位置(x,y)只能表示像素在平面上的位置关系,此时齐次坐标就能排上用场了。三维的齐次坐标虽然表示的是二维的平面,但是其本质还是一个三维空间的坐标值,这样就能将图片像素由二维空间扩展到三维空间进行处理,齐次坐标的w
分量也就有了新的含义:三维空间的深度。
在仿射变换中,像素的齐次坐标为
[
x
,
y
,
1
]
T
[x, y, 1]^T
[x,y,1]T,可以解释为图像位于三维空间的w=1平面上,即w=1的平面是我们在三维空间中的视线平面(三维空间中所有东西都被投影到w=1平面,然后我们才能看见,因为图片本身是个二维平面,每个像素点由行列坐标所定)。透视就规定了所有物体如何投影到视线平面上。数学描述就是根据像素三维空间中的坐标点
P
(
x
,
y
,
w
)
P(x, y, w)
P(x,y,w)得出像素在视线平面上的坐标
P
e
(
x
e
,
y
e
,
1
)
P_e(x_e, y_e, 1)
Pe(xe,ye,1), 两个关系如图所示。由于
P
与
P
e
P与P_e
P与Pe在同一条直线上根据直线公式:
x
2
−
x
1
l
=
y
2
−
y
1
m
=
z
1
−
z
2
n
=
λ
,其中,(
l
,
m
,
n
)为直线的方向向量
\frac{x_2-x_1}{l} = \frac{y_2-y_1}{m} = \frac{z_1-z_2}{n} = \lambda,其中,(l, m, n)为直线的方向向量
lx2−x1=my2−y1=nz1−z2=λ,其中,(l,m,n)为直线的方向向量可得:
{
x
−
0
l
=
y
−
0
m
=
w
−
0
n
=
λ
1
x
e
−
0
l
=
y
e
−
0
m
=
1
−
0
n
=
λ
2
\left\{\begin{aligned} &\frac{x-0}{l} = \frac{y-0}{m} = \frac{w-0}{n} = \lambda_1& \\&\frac{x_e-0}{l} = \frac{y_e-0}{m} = \frac{1-0}{n} =\lambda_2&\end{aligned}\right.
⎩
⎨
⎧lx−0=my−0=nw−0=λ1lxe−0=mye−0=n1−0=λ2
化简得:
{
x
e
=
x
w
y
e
=
y
w
1
=
w
w
\left\{ \begin{aligned} x_e = \frac{x}{w}\\ y_e = \frac{y}{w}\\ 1 = \frac{w}{w} \end{aligned} \right.
⎩
⎨
⎧xe=wxye=wy1=ww
上述公式就实现了三维空间像素坐标向实现平面的透视投影。
3. 透视变换
[
x
y
w
]
=
[
a
11
a
12
a
13
a
21
a
22
a
23
a
31
a
32
a
33
]
[
x
c
y
c
1
]
\begin{bmatrix} x\\ y\\ w\end{bmatrix}=\begin{bmatrix} a_{11} & a_{12} & a_{13}\\ a_{21}&a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33}\end{bmatrix}\begin{bmatrix} x_c\\ y_c\\ 1\end{bmatrix}
xyw
=
a11a21a31a12a22a32a13a23a33
xcyc1
根据仿射变换可知, 上述矩阵就能实现图片像素坐标
[
x
e
,
y
e
,
1
]
T
[x_e, y_e, 1]^T
[xe,ye,1]T在三维空间中的旋转,缩放、切变的变换操作(不能实现三维空间的平移操作,还记得仿射变换中平移操作至少是一个2乘3的矩阵吗,平移的变换矩阵比旋转、缩放这些多一个维度),得到像素位置变换后的三维坐标就为
[
x
,
y
,
w
]
T
[x, y, w]^T
[x,y,w]T。再将信的像素坐标进行透视处理,将坐标映射到w=1平面,得到的像素位置就是最终透视变换的结果。
因此透视变换的变换矩阵就能改写为:
[
x
′
y
′
1
]
=
1
w
[
a
11
a
12
a
13
a
21
a
22
a
23
a
31
a
32
a
33
]
[
x
c
y
c
1
]
\begin{bmatrix} {x}' \\ {y}'\\ 1\end{bmatrix}=\frac{1}{w}\begin{bmatrix} a_{11} & a_{12} & a_{13}\\ a_{21}&a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33}\end{bmatrix}\begin{bmatrix} x_c\\ y_c\\ 1\end{bmatrix}
x′y′1
=w1
a11a21a31a12a22a32a13a23a33
xcyc1
由于w
是一个常量,也可以放入变换矩阵:
[
x
′
y
′
1
]
=
[
b
11
b
12
b
13
b
21
b
22
b
23
b
31
b
32
b
33
]
[
x
c
y
c
1
]
\begin{bmatrix} {x}' \\ {y}'\\ 1\end{bmatrix}=\begin{bmatrix} b_{11} & b_{12} & b_{13}\\ b_{21}&b_{22} & b_{23} \\ b_{31} & b_{32} & b_{33}\end{bmatrix}\begin{bmatrix} x_c\\ y_c\\ 1\end{bmatrix}
x′y′1
=
b11b21b31b12b22b32b13b23b33
xcyc1
写乘方程组形式:
{
x
′
=
b
11
x
c
+
b
12
y
c
+
b
13
y
′
=
b
21
x
c
+
b
22
y
c
+
b
23
1
=
b
31
x
c
+
b
32
y
c
+
b
33
⇒
{
x
′
=
b
11
x
c
+
b
12
y
c
+
b
13
b
31
x
c
+
b
32
y
c
+
b
33
y
′
=
b
21
x
c
+
b
22
y
c
+
b
23
b
31
x
c
+
b
32
y
c
+
b
33
⇒
同乘一个非零常数
α
{
x
′
=
α
(
b
11
x
c
+
b
12
y
c
+
b
13
)
α
(
b
31
x
c
+
b
32
y
c
+
b
33
)
y
′
=
α
(
b
21
x
c
+
b
22
y
c
+
b
23
)
α
(
b
31
x
c
+
b
32
y
c
+
b
33
)
\left\{\begin{aligned}{x}' = b_{11}x_c + b_{12}y_c + b_{13} \\{y}' = b_{21}x_c + b_{22}y_c + b_{23} \\1 = b_{31}x_c + b_{32}y_c + b_{33} \\\end{aligned}\right.\Rightarrow\left\{\begin{aligned}{x}' = \frac{b_{11}x_c + b_{12}y_c + b_{13}}{b_{31}x_c + b_{32}y_c + b_{33}} \\{y}' = \frac{b_{21}x_c + b_{22}y_c + b_{23}}{b_{31}x_c + b_{32}y_c + b_{33}} \\\end{aligned}\right.\overset{\text{同乘一个非零常数}\alpha}{\Rightarrow}\left\{\begin{aligned}{x}' = \frac{\alpha(b_{11}x_c + b_{12}y_c + b_{13})}{\alpha(b_{31}x_c + b_{32}y_c + b_{33})} \\{y}' = \frac{\alpha(b_{21}x_c + b_{22}y_c + b_{23})}{\alpha(b_{31}x_c + b_{32}y_c + b_{33})} \\\end{aligned}\right.
⎩
⎨
⎧x′=b11xc+b12yc+b13y′=b21xc+b22yc+b231=b31xc+b32yc+b33⇒⎩
⎨
⎧x′=b31xc+b32yc+b33b11xc+b12yc+b13y′=b31xc+b32yc+b33b21xc+b22yc+b23⇒同乘一个非零常数α⎩
⎨
⎧x′=α(b31xc+b32yc+b33)α(b11xc+b12yc+b13)y′=α(b31xc+b32yc+b33)α(b21xc+b22yc+b23)
再写成矩阵形式:
[
x
′
y
′
1
]
=
α
[
b
11
b
12
b
13
b
21
b
22
b
23
b
31
b
32
b
33
]
[
x
c
y
c
1
]
\begin{bmatrix} {x}' \\ {y}'\\ 1\end{bmatrix}=\alpha\begin{bmatrix} b_{11} & b_{12} & b_{13}\\ b_{21}&b_{22} & b_{23} \\ b_{31} & b_{32} & b_{33}\end{bmatrix}\begin{bmatrix} x_c\\ y_c\\ 1\end{bmatrix}
x′y′1
=α
b11b21b31b12b22b32b13b23b33
xcyc1
可以看出变换矩阵乘以一个非零常数,对结果毫无影响,不妨令
α
=
1
b
33
\alpha = \frac{1}{b_{33}}
α=b331其结果就为:
[
x
′
y
′
1
]
=
[
b
11
b
12
b
13
b
21
b
22
b
23
b
31
b
32
1
]
[
x
c
y
c
1
]
\begin{bmatrix} {x}' \\ {y}'\\ 1\end{bmatrix}=\begin{bmatrix} b_{11} & b_{12} & b_{13}\\ b_{21}&b_{22} & b_{23} \\ b_{31} & b_{32} &1\end{bmatrix}\begin{bmatrix} x_c\\ y_c\\ 1\end{bmatrix}
x′y′1
=
b11b21b31b12b22b32b13b231
xcyc1
可以看出两张图片之间的透视变换,只涉及8
个自由度,因此至少需要3对变换前后的坐标点,才能求出透视变换矩阵。
从最后公式也可看出,仿射变换是透视变换的一种特例。
4. 透视变换的逆推
虽然上述矩阵中每一对像素能产生3个方程,但是它们之间并不是相互独立的,第三个方程可以带入前两个中,因此有效的只有两个(具体为啥我还不太清楚)。
{
x
′
=
b
11
x
c
+
b
12
y
c
+
b
13
b
31
x
c
+
b
32
y
c
+
b
33
y
′
=
b
21
x
c
+
b
22
y
c
+
b
23
b
31
x
c
+
b
32
y
c
+
b
33
\left\{\begin{aligned}{x}' = \frac{b_{11}x_c + b_{12}y_c + b_{13}}{b_{31}x_c + b_{32}y_c + b_{33}} \\{y}' = \frac{b_{21}x_c + b_{22}y_c + b_{23}}{b_{31}x_c + b_{32}y_c + b_{33}} \\\end{aligned}\right.
⎩
⎨
⎧x′=b31xc+b32yc+b33b11xc+b12yc+b13y′=b31xc+b32yc+b33b21xc+b22yc+b23
综上,逆向求解变换矩阵需要4
对像素坐标。
5. OpenCV代码
# 逆向计算透视变换矩阵
# srcPoints : 原像素点坐标
# dstPoints : 转换后像素点坐标
# solveMethod :是选择计算矩阵的方法
cv2.getPerspectiveTransform(srcPoints:np.ndarray, dstPoints:np.ndarray[, solveMethod]) -> retval
# 透视变换
# src :图片
# M :透视变换矩阵 3x3
# dsize : 要显示的图片大小
cv2.warpPerspective(src:image, M, dsize[, dst[, flags[, borderMode[, borderValue]]]]) -> dst:image
注意,需要保证这四个点满足以下条件:
- 任意三个点不共线(non-collinear);
- 包含四个点的直线不相交;
示例:
import cv2
import numpy as np
import math
img = cv2.imread("./image/book.jpg")
img = cv2.resize(img, (0, 0), fx=0.2, fy=0.2, interpolation=cv2.INTER_AREA)
# 这个我手动获取了书的4个角的坐标 (155,142) (321,143) (421, 320) (228,340)
# 顺序是 左上 右上 右下 左下
srcPoints = np.array([[155, 142], [321, 143], [421, 320], [228, 340]], dtype=np.float32)
w = max(
math.sqrt((321 - 155) ** 2 + (143 - 142) ** 2),
math.sqrt((228 - 421) ** 2 + (340 - 320) ** 2),
) # 取变换后的宽
h = max(
math.sqrt((155 - 228) ** 2 + (142 - 340) ** 2),
math.sqrt((321 - 421) ** 2 + (143 - 320) ** 2),
) # 取变换后的高
dstPoints = np.array([[0, 0], [w - 1, 0], [w - 1, h - 1], [0, h - 1]], dtype=np.float32)
M = cv2.getPerspectiveTransform(srcPoints, dstPoints)
res = cv2.warpPerspective(img, M, (int(w), int(h)))
cv2.imshow("img", img)
cv2.imshow("res", res)
cv2.waitKey(0)
五、图像边界填充
作用: 当图像需要变大,但是不想直接进行缩放时,则可以选择不同的方法将图像外围进行扩展,使得原图变大。
dst = cv2.copyMakeBorder(src, top, bottom, left, right, borderType[, dst[, value]])
其中:
src
是输入的原始图像,可以是灰度图像或彩色图像。top
、bottom
、left
、right
是指定要添加的边框的上、下、左、右边界的像素个数。borderType
是指定要添加的边框类型的标志。常用的有:cv2.BORDER_CONSTANT
:添加一个常数值的边框。cv2.BORDER_REPLICATE
:复制最边缘的像素值,直到边框大小达到指定的大小。cv2.BORDER_REFLECT
:对边缘像素进行反射,两边反射。dcba|abcd|dcba-
cv2.BORDER_REFLECT101
:对边缘像素进行反射,以边缘为轴。abc|cba
cv2.BORDER_WRAP
:外包装法,abcd|abcd|abcd
dst
(可选)是输出的图像,如果不为None
,则需要确保大小和类型正确,否则会抛出错误。value
(可选)是当borderType
为cv2.BORDER_CONSTANT
时给定的常数值。
总结
本章主要讲述了图像的基本运算、图像的翻转与旋转、仿射变换、透视变换以及边界扩充,不仅描述了其原理,也进行了形象的表达,加深了自己对图像的一些操作。