程序设计与计算思维¶

Computer Programming and Computational Thinking

第 4 讲:图像变换 II¶

2025—2026学年度春季学期

清华大学 韩文弢

内容回顾¶

  • 图像变换
    • 采样(降采样、升采样)
    • 线性组合
    • 卷积(恒等、边界探测、锐化、模糊)

锐化与模糊的数学原理¶

  • 锐化:求差(离散域)、差分或求导(连续域) $$ \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 元组¶

元组由多个用逗号隔开的值组成的不可变序列。例如:

In [1]:
t = 12345, 54321, 'hello!'
t[0]
Out[1]:
12345
In [2]:
t
Out[2]:
(12345, 54321, 'hello!')
In [3]:
# 元组可以嵌套:
u = t, (1, 2, 3, 4, 5)
u
Out[3]:
((12345, 54321, 'hello!'), (1, 2, 3, 4, 5))
In [4]:
# 元组是不可变对象:
#!!! t[0] = 88888
# 但它们可以包含可变对象:
v = ([1, 2, 3], [3, 2, 1])
v
Out[4]:
([1, 2, 3], [3, 2, 1])
  • 输出时,元组都由圆括号标注,这样才能正确地解释嵌套元组。
  • 输入时,圆括号可有可无,不过经常是必须的(比如元组是更大的表达式的一部分)。

可变类型与不可变类型¶

  • 元组是不可变的(immutable),一般可包含异质元素序列,通过解包或索引访问
  • 列表是可变的(mutable),列表元素一般为同质类型,可迭代访问

特殊元组¶

构造 0 个或 1 个元素的元组比较特殊:

  • 空元组:用一对空圆括号
  • 只有一个元素的元组:通过在这个元素后添加逗号来构建(圆括号里只有一个值不行,为什么?)

例如:

In [5]:
empty = ()
len(empty)
Out[5]:
0
In [6]:
singleton = 'hello',    # <-- 注意末尾的逗号
len(singleton)
Out[6]:
1
In [7]:
singleton
Out[7]:
('hello',)

打包与解包¶

可以用元组将一组元素打包成一个对象,例如:

In [8]:
t = 12345, 54321, 'hello!'
t
Out[8]:
(12345, 54321, 'hello!')

它的逆操作是序列解包。序列解包时,左侧变量与右侧元组的元素数量应相等。

In [9]:
x, y, z = t
y
Out[9]:
54321

注意,多重赋值其实只是元组打包和序列解包的组合。

Python 函数定义详解¶

为了使用方便,Python 中的函数定义支持可变数量的参数。

默认值参数¶

为参数指定默认值是非常有用的方式。调用函数时,可以使用比定义时更少的参数,例如:

In [10]:
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!')

默认值在定义时求值,例如:

In [11]:
i = 5

def f(arg=i):
    print(arg)

i = 6
f()
5

上例输出的是 5,在函数 f 定义时已经确定。

注意: 默认值只计算一次。默认值为列表等可变对象时,可能会产生意料外的结果。

例如,下面的函数会累积后续调用时传递的参数:

In [12]:
def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))
[1]
[1, 2]
[1, 2, 3]

不想在后续调用之间共享默认值时,应以如下方式编写函数:

In [13]:
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]
In [ ]:
 

关键字参数¶

函数还可以使用形如 kwarg=value 的关键字参数。之前的普通参数被称为位置参数。例如:

In [14]:
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)。该函数可用下列方式调用:

In [15]:
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 !

以下调用函数的方式都无效:

In [16]:
#parrot()                     # 缺失必需的参数
#parrot(voltage=5.0, 'dead')  # 关键字参数后存在非关键字参数
#parrot(110, voltage=220)     # 同一个参数重复的值
#parrot(actor='John Cleese')  # 未知的关键字参数

解包参数列表¶

调用函数时,如果多个参数在序列中,可以用 * 操作符将其解包,例如:

In [17]:
args = [3, 6]
list(range(*args))            # 使用从一个列表解包的参数的函数调用
Out[17]:
[3, 4, 5]

Lambda 表达式¶

  • lambda 关键字用于创建匿名函数。
    • 例如,lambda a, b: a+b 定义一个函数,它会返回两个参数的和。
  • Lambda 函数可用于任何需要函数对象的地方。
  • 在语法上,匿名函数只能是单个表达式。
  • 在语义上,它只是常规函数定义的语法糖。
  • Lambda 函数可以引用包含作用域中的变量,例如:
In [18]:
def make_incrementor(n):
    return lambda x: x + n

f = make_incrementor(42)
f(1)
Out[18]:
43

这里 lambda 表达式返回了一个函数对象。

另一种常见用法是传入一个小函数作为参数。例如,list.sort() 接受排序键函数 key,它可以是一个 lambda 函数:

In [19]:
pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
pairs.sort(key=lambda pair: pair[1])
pairs
Out[19]:
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

函数对象¶

普通函数、lambda 表达式都会创建函数对象。像其他对象一样可以绑定到名字,可以通过函数参数进行传递。

In [20]:
# 定义函数:创建函数对象,并绑定到指定的名字
def fib(n):
    if n <= 1:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)

fib
Out[20]:
<function __main__.fib(n)>
In [21]:
# 通过赋值绑定到其他名字
f = fib
f(10)
Out[21]:
89
In [22]:
# 函数调用时作为参数传递
def call(f, n):
    print(f(n))
call(fib, 20)
10946

线性变换示例¶

以下是一些线性变换的例子:

In [23]:
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)

使用举例:

In [24]:
scale(0.5)(2, 4)
Out[24]:
(1.0, 2.0)

线性变换可以写成更一般的形式:

In [25]:
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} $$

非线性变换示例¶

以下是一些非线性变换的例子:

In [26]:
# 平移,是仿射变换,但不是线性变换
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))

使用举例:

In [27]:
warp(0.5)(2, 4)
Out[27]:
(1.9124507732745224, -4.0425897689230945)

线性变换与矩阵¶

回顾线性代数的内容:矩阵表示线性映射。它不只是一堆杂乱的数字,能够表示线性变换。

线性变换的定义¶

直观定义:

变换图像中的矩形(网格线)总是变成全等平行四边形的网格。

操作定义:

如果一个变换由 $v \mapsto Av$(矩阵乘以向量)定义,其中 $A$ 是某个固定矩阵,则该变换是线性的。

缩放和加法定义:

  1. 如果先缩放再变换,或者先变换再缩放,结果总是相同的:

$T(cv)=c \, T(v)$($v$ 是任意向量,$c$ 是任意数。)

  1. 如果先相加再变换,或反之,结果是相同的:

$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\ 列)}$$

而这就是矩阵乘法的定义。

矩阵乘法的含义¶

思考为什么矩阵乘法要定义成这种复杂的乘法和加法过程。

In [28]:
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)
Out[28]:
((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]$ 是相同的。

In [29]:
P = np.random.randn(2, 2)
Q = np.random.randn(2, 2)
lin(P@Q)(1, 0), lin(P)(*lin(Q)(1, 0))
Out[29]:
((np.float64(0.5750792593745916), np.float64(-0.16480644749396492)),
 (np.float64(0.5750792593745916), np.float64(-0.16480644749396492)))

本讲小结¶

  • Python 元组、函数(参数、对象)
  • 线性变换与非线性变换
  • 线性变换与矩阵

目标:

image.png