内容回顾¶
- Python 语言
- 元组
- 函数(参数默认值、关键字参数、Lambda 表达式、函数对象)
- 变换
- 线性变换与非线性变换
- 线性变换与矩阵
准备工作¶
from urllib.request import urlretrieve
import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
# 设置作图时的中文字体,避免乱码
plt.rcParams['font.sans-serif'] = ['Noto Sans CJK SC']
img_path, _ = urlretrieve('https://pacman.cs.tsinghua.edu.cn/~hanwentao/cpct/05/longcorgi.png')
img = plt.imread(img_path)[:, :, :3]
plt.imshow(img)
<matplotlib.image.AxesImage at 0x10aa99be0>
最终效果¶
black = np.array([0, 0, 0])
white = np.array([1, 1, 1])
green = np.array([0, 1, 0])
red = np.array([1, 0, 0])
from math import sin, cos, hypot, atan2
# 恒等
identity = lambda x, y: (x, y)
# 伸缩
scalex = lambda alpha: lambda x, y: (alpha*x, y)
scaley = lambda alpha: lambda x, y: (x, alpha*y)
scale = lambda alpha: lambda x, y: (alpha*x, alpha*y)
# 翻转
swap = lambda x, y: (y, x)
flipy = lambda x, y: (x, -y)
# 旋转
rotate = lambda theta: lambda x, y: (cos(theta)*x + sin(theta)*y, -sin(theta)*x + cos(theta)*y)
# 剪切
shear = lambda alpha: lambda x, y: (x + alpha*y, y)
# 一般线性变换
lin = lambda a, b, c, d: lambda x, y: (a*x + b*y, c*x + d*y)
# 用矩阵表示
def lin_mat(A):
a = A[0, 0]
b = A[0, 1]
c = A[1, 0]
d = A[1, 1]
return lambda x, y: lin(a, b, c, d)
# 平移,是仿射变换,但不是线性变换
translate = lambda alpha, beta: lambda x, y: (x + alpha, y + beta)
# 非线性剪切
nonlin_shear = lambda alpha: lambda x, y: (x, y + alpha*x**2)
# 扭曲
warp = lambda alpha: lambda x, y: rotate(alpha*hypot(x, y))(x, y)
# 极坐标转直角坐标
xy = lambda rho, theta: (rho*cos(theta), rho*sin(theta))
# 直角坐标转极坐标
rho_theta = lambda x, y: (hypot(x, y), atan2(y, x))
def transform_image(img, transform_func, pixels=400):
"""
对图像应用变换
参数:
- img: 输入图像 (NumPy 数组)
- transform_func: 变换函数 (x, y) -> (new_x, new_y)
- pixels: 输出图像的像素数
"""
rows, cols = img.shape[:2]
m = max(cols, rows)
# 创建输出图像
output = np.zeros((pixels, pixels, img.shape[2]), dtype=img.dtype)
for i in range(pixels):
for j in range(pixels):
# 将像素坐标转换为 [-1, 1] 范围
x, y = scale(2/pixels)(*flipy(*swap(*translate(-pixels/2, -pixels/2)(i, j))))
# 应用变换的逆(因为我们是从输出像素找输入像素)
new_x, new_y = transform_func(x, y)
# 将坐标转换回像素索引
src_i, src_j = translate(rows/2, cols/2)(*swap(*flipy(*scale(m/2)(new_x, new_y))))
src_i = int(src_i)
src_j = int(src_j)
if 0 <= src_i < rows and 0 <= src_j < cols:
output[i, j] = img[src_i, src_j]
else:
output[i, j] = black # 黑色背景
return output
def interactive_transform(a=1.0, b=0.0, c=0.0, d=1.0):
"""
交互式变换演示
"""
transformed = transform_image(img, lin(a, b, c, d), pixels=400)
plt.figure(figsize=(8, 8))
plt.imshow(transformed)
plt.title(f'变换矩阵: a={a:.1f}, b={b:.1f}, c={c:.1f}, d={d:.1f}')
plt.axis('off')
plt.show()
# 创建交互式控件
interact = widgets.interactive(
interactive_transform,
a=widgets.FloatSlider(value=1.0, min=-1.5, max=1.5, step=0.1, description='a:'),
b=widgets.FloatSlider(value=0.0, min=-1.5, max=1.5, step=0.1, description='b:'),
c=widgets.FloatSlider(value=0.0, min=-1.5, max=1.5, step=0.1, description='c:'),
d=widgets.FloatSlider(value=1.0, min=-1.5, max=1.5, step=0.1, description='d:'),
)
display(interact)
interactive(children=(FloatSlider(value=1.0, description='a:', max=1.5, min=-1.5), FloatSlider(value=0.0, desc…
对象变换与坐标变换¶
- 对象变换是指通过改变对象的坐标来实现变换,如点 $(x, y) \to (2x, 2y)$ 是进行了 2 倍的放大
- 坐标变换是指通过改变坐标轴来实现变换,例如 $x$ 和 $y$ 坐标轴都缩小一半,点的坐标也会发生上述变换
这两种变换的效果可能相同(点的坐标发生同样的变化),但是含义不同。
为了做想要的变换时有一致的效果,我们将图像的坐标系变换到 $[-1, 1]^2$ 的区域里再进行变换。
逆变换¶
如果 $f$ 是从 2 维向量到 2 维向量的函数,我们定义 $f$ 的逆,记为 $f^{-1}$,具有“撤销” $f$ 效果的性质,即
$$f(f^{-1}(v)) = v$$
和
$$f^{-1}(f(v)) = v$$
这个等式可能对所有 $v$ 成立,也可能对某些 $v$ 成立。
示例:放大和缩小¶
# 验证缩放变换的逆
v = np.random.rand(2)
# scale(2) 的逆是 scale(0.5)
T = scale(0.5)
T_inv = scale(2)
# 应用 T 然后应用 T_inv
result = T_inv(*T(*v))
print(f'原始向量: {v}')
print(f'变换后: {result}')
print(f'相等? {np.allclose(v, result)}')
原始向量: [0.82284975 0.77955379] 变换后: (np.float64(0.822849746691098), np.float64(0.7795537925491557)) 相等? True
# 验证旋转变换的逆
v = np.random.rand(2)
# rotate(θ) 的逆是 rotate(-θ)
theta = np.radians(30)
T = rotate(theta)
T_inv = rotate(-theta)
result = T_inv(*T(*v))
print(f'原始向量: {v}')
print(f'变换后: {result}')
print(f'相等? {np.allclose(v, result)}')
原始向量: [0.11806597 0.27107506] 变换后: (np.float64(0.1180659687326121), np.float64(0.2710750580133623)) 相等? True
求解逆变换¶
逆变换到底是在做什么?
再次考虑缩放。假设将输入向量 $v$ 缩放 2 得到输出向量 $u$:
$$u = 2 v$$
现在假设想进行反向操作。如果给定 $u$,如何找到 $v$?
在这个特定情况下,有 $v = \frac{1}{2} u$。
考虑更加一般的情况,如果有一个线性变换,可以写成
$$u = A \, v$$
其中 $A$ 是矩阵。
如果给定 $u$ 并想反向找到 $v$,需要求解线性方程组。
通常可以(什么时候不可以?)解这个方程组找到一个新矩阵 $B$ 使得
$$v = B \, u$$
即 $B$ 撤销了 $A$ 的效果。那么,
$$v = (B \, A) v$$
因此 $B A$ 必须是单位矩阵,称 $B$ 为 $A$ 的矩阵逆,并写成
$$B = A^{-1}$$
对于 $2 \times 2$ 的矩阵,可以写出矩阵逆的公式。对于更大规模的矩阵,通常需要计算机运行算法来找到逆。
# 验证矩阵逆
A = np.random.randn(2, 2)
v = np.random.rand(2)
# 使用矩阵逆
A_inv = np.linalg.inv(A)
# 应用 A 然后应用 A_inv
result = A_inv @ (A @ v)
print(f'原始向量: {v}')
print(f'变换后: {result}')
print(f'相等? {np.allclose(v, result)}')
原始向量: [0.83213606 0.74802034] 变换后: [0.83213606 0.74802034] 相等? True
# 验证 (A*B)^(-1) = B^(-1) * A^(-1)
A = np.random.randn(2, 2)
B = np.random.randn(2, 2)
left = np.linalg.inv(A @ B)
right = np.linalg.inv(B) @ np.linalg.inv(A)
print(f'(A*B)^(-1):\n{left}')
print(f'B^(-1) * A^(-1):\n{right}')
print(f'相等? {np.allclose(left, right)}')
(A*B)^(-1): [[-0.81035038 -2.08689412] [ 0.52604683 0.31146011]] B^(-1) * A^(-1): [[-0.81035038 -2.08689412] [ 0.52604683 0.31146011]] 相等? True
2x2 矩阵逆的公式¶
如果 $A = \begin{pmatrix} a & b \\ c & d \end{pmatrix}$,则
$$A^{-1} = \frac{1}{ad-bc} \begin{pmatrix} d & -b \\ -c & a \end{pmatrix}$$
# 手动计算 2x2 矩阵逆
def inv_2x2_manual(A):
a, b = A[0, 0], A[0, 1]
c, d = A[1, 0], A[1, 1]
det = a * d - b * c
if abs(det) < 1e-10:
raise ValueError("矩阵不可逆")
return np.array([[d, -b], [-c, a]]) / det
A = np.random.randn(2, 2)
print(f'NumPy 逆:\n{np.linalg.inv(A)}')
print(f'手动计算逆:\n{inv_2x2_manual(A)}')
print(f'相等? {np.allclose(np.linalg.inv(A), inv_2x2_manual(A))}')
NumPy 逆: [[ 0.72936789 -1.0798709 ] [-0.11181915 2.1705302 ]] 手动计算逆: [[ 0.72936789 -1.0798709 ] [-0.11181915 2.1705302 ]] 相等? True
非线性变换的逆变换¶
如果有一个非线性变换 $T$,能求逆吗?换句话说,如果 $u = T(v)$,能否解这个方程找到用 $v$ 关于 $u$ 的函数表示?
通常这是一个困难的问题,有时可以解析地做到,但通常不能。
尽管如此,有一些数值方法有时可以解这些方程,例如牛顿法。在 scipy 中可以使用 fsolve 函数来进行数值求解。
from scipy.optimize import fsolve
# 使用数值方法求非线性变换的逆
def inverse_numerical(f, y, x0=(0.0, 0.0)):
"""
数值求解非线性变换的逆
f: 变换函数 (x, y) -> (u, v)
y: 目标输出 (u, v)
x0: 初始猜测
"""
def equations(x):
u, v = f(x[0], x[1])
return [u - y[0], v - y[1]]
solution = fsolve(equations, x0)
return solution[0], solution[1]
# 测试:非线性剪切的逆
alpha = 0.5
original_point = (0.5, 0.5)
transformed = nonlin_shear(alpha)(*original_point)
# 创建逆函数
inverse_nonlin_shear = lambda alpha: lambda x, y: inverse_numerical(nonlin_shear(alpha), (x, y))
recovered = inverse_nonlin_shear(alpha)(*transformed)
# 等价于 recovered = inverse_numerical(nonlin_shear(alpha), transformed)
print(f'原始点: {original_point}')
print(f'变换后: {transformed}')
print(f'恢复的点: {recovered}')
原始点: (0.5, 0.5) 变换后: (0.5, 0.625) 恢复的点: (np.float64(0.5), np.float64(0.5))
图像变换的完整流程¶
下面实现完整的图像变换(反向生成):
def transform_image(img, transform_func, pixels=400):
"""
对图像应用变换
参数:
- img: 输入图像 (NumPy 数组)
- transform_func: 变换函数 (x, y) -> (new_x, new_y)
- pixels: 输出图像的像素数
"""
rows, cols = img.shape[:2]
m = max(cols, rows)
# 创建输出图像
output = np.zeros((pixels, pixels, img.shape[2]), dtype=img.dtype)
for i in range(pixels):
for j in range(pixels):
# 将像素坐标转换为 [-1, 1] 范围
x, y = scale(2/pixels)(*flipy(*swap(*translate(-pixels/2, -pixels/2)(i, j))))
# 应用变换的逆(因为我们是从输出像素找输入像素)
new_x, new_y = transform_func(x, y)
# 将坐标转换回像素索引
src_i, src_j = translate(rows/2, cols/2)(*swap(*flipy(*scale(m/2)(new_x, new_y))))
src_i = int(src_i)
src_j = int(src_j)
if 0 <= src_i < rows and 0 <= src_j < cols:
output[i, j] = img[src_i, src_j]
else:
output[i, j] = black # 黑色背景
return output
# 示例:应用旋转变换
theta = np.radians(30)
rotated_img = transform_image(img, rotate(-theta))
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
axes[0].imshow(img)
axes[0].set_title('原始图像')
axes[0].axis('off')
axes[1].imshow(rotated_img)
axes[1].set_title(f'旋转 {np.degrees(theta):.1f}°')
axes[1].axis('off')
(np.float64(-0.5), np.float64(399.5), np.float64(399.5), np.float64(-0.5))
# 示例:应用剪切变换
alpha = 0.5
sheared_img = transform_image(img, shear(-alpha))
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
axes[0].imshow(img)
axes[0].set_title('原始图像')
axes[0].axis('off')
axes[1].imshow(sheared_img)
axes[1].set_title(f'剪切 (α={alpha})')
axes[1].axis('off')
(np.float64(-0.5), np.float64(399.5), np.float64(399.5), np.float64(-0.5))