内容回顾¶
- 图像变换
- 采样(降采样、升采样)
- 线性组合
- 卷积(恒等、边界探测、锐化、模糊)
锐化与模糊的数学原理¶
- 锐化:求差(离散域)、差分或求导(连续域) $$ \begin{bmatrix} 0 & -1 & 0 \\ -1 & \ \ 5 & -1 \\ 0 & -1 & 0 \\ \end{bmatrix} $$
- 模糊:求和(离散域)、积分(连续域) $$ \frac{1}{9} \begin{bmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \\ \end{bmatrix} $$
Python 元组¶
元组由多个用逗号隔开的值组成的不可变序列。例如:
t = 12345, 54321, 'hello!'
t[0]
12345
t
(12345, 54321, 'hello!')
# 元组可以嵌套:
u = t, (1, 2, 3, 4, 5)
u
((12345, 54321, 'hello!'), (1, 2, 3, 4, 5))
# 元组是不可变对象:
#!!! t[0] = 88888
# 但它们可以包含可变对象:
v = ([1, 2, 3], [3, 2, 1])
v
([1, 2, 3], [3, 2, 1])
- 输出时,元组都由圆括号标注,这样才能正确地解释嵌套元组。
- 输入时,圆括号可有可无,不过经常是必须的(比如元组是更大的表达式的一部分)。
可变类型与不可变类型¶
- 元组是不可变的(immutable),一般可包含异质元素序列,通过解包或索引访问
- 列表是可变的(mutable),列表元素一般为同质类型,可迭代访问
empty = ()
len(empty)
0
singleton = 'hello', # <-- 注意末尾的逗号
len(singleton)
1
singleton
('hello',)
打包与解包¶
可以用元组将一组元素打包成一个对象,例如:
t = 12345, 54321, 'hello!'
t
(12345, 54321, 'hello!')
它的逆操作是序列解包。序列解包时,左侧变量与右侧元组的元素数量应相等。
x, y, z = t
y
54321
注意,多重赋值其实只是元组打包和序列解包的组合。
Python 函数定义详解¶
为了使用方便,Python 中的函数定义支持可变数量的参数。
默认值参数¶
为参数指定默认值是非常有用的方式。调用函数时,可以使用比定义时更少的参数,例如:
def ask_ok(prompt, retries=4, reminder='Please try again!'):
while True:
reply = input(prompt)
if reply in ['y', 'ye', 'yes']:
return True
if reply in ['n', 'no', 'nop', 'nope']:
return False
retries = retries - 1
if retries < 0:
raise ValueError('invalid user response')
print(reminder)
该函数可以用以下方式调用:
- 只给出必选实参:
ask_ok('Do you really want to quit?') - 给出一个可选实参:
ask_ok('OK to overwrite the file?', 2) - 给出所有实参:
ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')
默认值在定义时求值,例如:
i = 5
def f(arg=i):
print(arg)
i = 6
f()
5
上例输出的是 5,在函数 f 定义时已经确定。
例如,下面的函数会累积后续调用时传递的参数:
def f(a, L=[]):
L.append(a)
return L
print(f(1))
print(f(2))
print(f(3))
[1] [1, 2] [1, 2, 3]
不想在后续调用之间共享默认值时,应以如下方式编写函数:
def f(a, L=None):
if L is None:
L = []
L.append(a)
return L
print(f(1))
print(f(2))
print(f(3))
[1] [2] [3]
关键字参数¶
函数还可以使用形如 kwarg=value 的关键字参数。之前的普通参数被称为位置参数。例如:
def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
print("-- This parrot wouldn't", action, end=' ')
print("if you put", voltage, "volts through it.")
print("-- Lovely plumage, the", type)
print("-- It's", state, "!")
该函数接受一个必选参数(voltage)和三个可选参数(state, action 和 type)。该函数可用下列方式调用:
parrot(1000) # 1 个位置参数
parrot(voltage=1000) # 1 个关键字参数
parrot(voltage=1000000, action='VOOOOOM') # 2 个关键字参数
parrot(action='VOOOOOM', voltage=1000000) # 2 个关键字参数
parrot('a million', 'bereft of life', 'jump') # 3 个位置参数
parrot('a thousand', state='pushing up the daisies') # 1 个位置参数,1 个关键字参数
-- This parrot wouldn't voom if you put 1000 volts through it. -- Lovely plumage, the Norwegian Blue -- It's a stiff ! -- This parrot wouldn't voom if you put 1000 volts through it. -- Lovely plumage, the Norwegian Blue -- It's a stiff ! -- This parrot wouldn't VOOOOOM if you put 1000000 volts through it. -- Lovely plumage, the Norwegian Blue -- It's a stiff ! -- This parrot wouldn't VOOOOOM if you put 1000000 volts through it. -- Lovely plumage, the Norwegian Blue -- It's a stiff ! -- This parrot wouldn't jump if you put a million volts through it. -- Lovely plumage, the Norwegian Blue -- It's bereft of life ! -- This parrot wouldn't voom if you put a thousand volts through it. -- Lovely plumage, the Norwegian Blue -- It's pushing up the daisies !
以下调用函数的方式都无效:
#parrot() # 缺失必需的参数
#parrot(voltage=5.0, 'dead') # 关键字参数后存在非关键字参数
#parrot(110, voltage=220) # 同一个参数重复的值
#parrot(actor='John Cleese') # 未知的关键字参数
解包参数列表¶
调用函数时,如果多个参数在序列中,可以用 * 操作符将其解包,例如:
args = [3, 6]
list(range(*args)) # 使用从一个列表解包的参数的函数调用
[3, 4, 5]
Lambda 表达式¶
lambda关键字用于创建匿名函数。- 例如,
lambda a, b: a+b定义一个函数,它会返回两个参数的和。
- 例如,
- Lambda 函数可用于任何需要函数对象的地方。
- 在语法上,匿名函数只能是单个表达式。
- 在语义上,它只是常规函数定义的语法糖。
- Lambda 函数可以引用包含作用域中的变量,例如:
def make_incrementor(n):
return lambda x: x + n
f = make_incrementor(42)
f(1)
43
这里 lambda 表达式返回了一个函数对象。
另一种常见用法是传入一个小函数作为参数。例如,list.sort() 接受排序键函数 key,它可以是一个 lambda 函数:
pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
pairs.sort(key=lambda pair: pair[1])
pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]
函数对象¶
普通函数、lambda 表达式都会创建函数对象。像其他对象一样可以绑定到名字,可以通过函数参数进行传递。
# 定义函数:创建函数对象,并绑定到指定的名字
def fib(n):
if n <= 1:
return 1
else:
return fib(n - 1) + fib(n - 2)
fib
<function __main__.fib(n)>
# 通过赋值绑定到其他名字
f = fib
f(10)
89
# 函数调用时作为参数传递
def call(f, n):
print(f(n))
call(fib, 20)
10946
线性变换示例¶
以下是一些线性变换的例子:
from math import sin, cos, hypot, atan2
import numpy as np
import matplotlib.pyplot as plt
# 恒等
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)
使用举例:
scale(0.5)(2, 4)
(1.0, 2.0)
线性变换可以写成更一般的形式:
lin = lambda a, b, c, d: lambda x, y: (a*x + b*y, c*x + d*y)
# 用矩阵表示
def lin(A):
a = A[0, 0]
b = A[0, 1]
c = A[1, 0]
d = A[1, 1]
return lambda x, y: (a*x + b*y, c*x + d*y)
其中, $$ A = \begin{bmatrix} a & b \\ c & d \\ \end{bmatrix} $$
非线性变换示例¶
以下是一些非线性变换的例子:
# 平移,是仿射变换,但不是线性变换
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))
使用举例:
warp(0.5)(2, 4)
(1.9124507732745224, -4.0425897689230945)
线性变换与矩阵¶
回顾线性代数的内容:矩阵表示线性映射。它不只是一堆杂乱的数字,能够表示线性变换。
线性变换的定义¶
直观定义:
变换图像中的矩形(网格线)总是变成全等平行四边形的网格。
操作定义:
如果一个变换由 $v \mapsto Av$(矩阵乘以向量)定义,其中 $A$ 是某个固定矩阵,则该变换是线性的。
缩放和加法定义:
- 如果先缩放再变换,或者先变换再缩放,结果总是相同的:
$T(cv)=c \, T(v)$($v$ 是任意向量,$c$ 是任意数。)
- 如果先相加再变换,或反之,结果是相同的:
$T(v_1+v_2) = T(v_1) + T(v_2).$($v_1,v_2$ 是任意向量。)
数学上的严格定义:
如果对于所有数 $c_1,c_2$ 和向量 $v_1,v_2$,都有
$T(c_1 v_1 + c_2 v_2) = c_1 T(v_1) + c_2 T(v_2)$,则 $T$ 是线性的。
变换矩阵¶
令线性变换 $T$ 对应的矩阵为 $A$,另有列向量 $v = [x, y]$。则 $A$ 的第一列是 $T([1, 0])$,第二列是 $T([0,1])$。于是,
$$T([x,y]) = x \, T([1,0]) + y \, T([0,1]) = x \, \mathrm{(第\ 1\ 列)} + y \, \mathrm{(第\ 2\ 列)}$$
而这就是矩阵乘法的定义。
矩阵乘法的含义¶
思考为什么矩阵乘法要定义成这种复杂的乘法和加法过程。
A = np.random.randn(2, 2)
B = np.random.randn(2, 2)
v = np.random.rand(2)
# NumPy 中的矩阵乘法用 @ 运算符,* 运算符是按元素乘法!
lin(A)(*lin(B)(*v)), lin(A@B)(*v)
((np.float64(-4.196363742234947), np.float64(0.16379692340200336)), (np.float64(-4.196363742234947), np.float64(0.16379692340200336)))
重要:线性变换的复合 $A(Bv)$ 就是相乘矩阵的线性变换 $(AB)v$。只有一种矩阵乘法(matmul)的定义符合要求。
具体来说,$AB$ 的第一列应该是计算两个矩阵乘以向量的结果 $u=B[1,0]$ 然后 $w=Au$,第二列对于 $[0,1]$ 是相同的。
P = np.random.randn(2, 2)
Q = np.random.randn(2, 2)
lin(P@Q)(1, 0), lin(P)(*lin(Q)(1, 0))
((np.float64(0.5750792593745916), np.float64(-0.16480644749396492)), (np.float64(0.5750792593745916), np.float64(-0.16480644749396492)))
本讲小结¶
- Python 元组、函数(参数、对象)
- 线性变换与非线性变换
- 线性变换与矩阵
目标: