内容回顾¶
- Python 语言
- 对象、类型、值
- 内置类型
- 简单类型:整数、浮点数、布尔型、空值
- 序列类型:字符串、列表
- 第三方库的类型:
np.ndarray - 赋值
- 分支:
if语句 - 循环:
while语句、for语句 - 函数:定义和调用
- 图像
- 用矩阵(或张量)来表示
- 通过运算进行变换
准备工作¶
import numpy as np
import matplotlib.pyplot as plt
from urllib.request import urlretrieve
philip_file, _ = urlretrieve("https://pacman.cs.tsinghua.edu.cn/~hanwentao/cpct/01/philip.png")
philip = plt.imread(philip_file)
plt.imshow(philip)
<matplotlib.image.AxesImage at 0x10e621160>
r = 20
downsample_philip = philip[::r, ::r]
plt.imshow(downsample_philip)
<matplotlib.image.AxesImage at 0x10e6bf390>
降采样之后要让图像恢复原来的大小,需要进行升采样(upsampling),把每个像素扩展成一个正方形。
升采样可以用克罗内克积(Kronecker product)实现。
upsample_philip = np.kron(downsample_philip, np.full((r, r, 1), 1))
plt.imshow(upsample_philip)
<matplotlib.image.AxesImage at 0x10e74ad50>
克罗内克积¶
数学上,克罗内克积是两个任意大小的矩阵间的运算,表示为 $\otimes$。简单地说,就是将前一个矩阵的每个元素乘上后一个完整的矩阵。
定义:如果 $A$ 是一个 $m \times n$ 的矩阵,而 $B$ 是一个 $p \times q$ 的矩阵,克罗内克积 $A \otimes B$ 是一个 $mp \times nq$ 的分块矩阵。
$$ A \otimes B = \begin{bmatrix} a_{11} B & \cdots & a_{1n}B \\ \vdots & \ddots & \vdots \\ a_{m1} B & \cdots & a_{mn} B \end{bmatrix} $$
进一步展开就是:
$$ A \otimes B = \begin{bmatrix} a_{11} b_{11} & a_{11} b_{12} & \cdots & a_{11} b_{1q} & \cdots & \cdots & a_{1n} b_{11} & a_{1n} b_{12} & \cdots & a_{1n} b_{1q} \\ a_{11} b_{21} & a_{11} b_{22} & \cdots & a_{11} b_{2q} & \cdots & \cdots & a_{1n} b_{21} & a_{1n} b_{22} & \cdots & a_{1n} b_{2q} \\ \vdots & \vdots & \ddots & \vdots & & & \vdots & \vdots & \ddots & \vdots \\ a_{11} b_{p1} & a_{11} b_{p2} & \cdots & a_{11} b_{pq} & \cdots & \cdots & a_{1n} b_{p1} & a_{1n} b_{p2} & \cdots & a_{1n} b_{pq} \\ \vdots & \vdots & & \vdots & \ddots & & \vdots & \vdots & & \vdots \\ \vdots & \vdots & & \vdots & & \ddots & \vdots & \vdots & & \vdots \\ a_{m1} b_{11} & a_{m1} b_{12} & \cdots & a_{m1} b_{1q} & \cdots & \cdots & a_{mn} b_{11} & a_{mn} b_{12} & \cdots & a_{mn} b_{1q} \\ a_{m1} b_{21} & a_{m1} b_{22} & \cdots & a_{m1} b_{2q} & \cdots & \cdots & a_{mn} b_{21} & a_{mn} b_{22} & \cdots & a_{mn} b_{2q} \\ \vdots & \vdots & \ddots & \vdots & & & \vdots & \vdots & \ddots & \vdots \\ a_{m1} b_{p1} & a_{m1} b_{p2} & \cdots & a_{m1} b_{pq} & \cdots & \cdots & a_{mn} b_{p1} & a_{mn} b_{p2} & \cdots & a_{mn} b_{pq} \end{bmatrix} $$
例如:
$$ \begin{bmatrix} 1 & 2 \\ 3 & 1 \\ \end{bmatrix} \otimes \begin{bmatrix} 0 & 3 \\ 2 & 1 \\ \end{bmatrix} = \begin{bmatrix} 1\cdot 0 & 1\cdot 3 & 2\cdot 0 & 2\cdot 3 \\ 1\cdot 2 & 1\cdot 1 & 2\cdot 2 & 2\cdot 1 \\ 3\cdot 0 & 3\cdot 3 & 1\cdot 0 & 1\cdot 3 \\ 3\cdot 2 & 3\cdot 1 & 1\cdot 2 & 1\cdot 1 \\ \end{bmatrix} = \begin{bmatrix} 0 & 3 & 0 & 6 \\ 2 & 1 & 4 & 2 \\ 0 & 9 & 0 & 3 \\ 6 & 3 & 2 & 1 \end{bmatrix} $$
图像合成(线性组合)¶
数学中的一个重要概念是线性组合。这个概念结合了
- 缩放一个对象
- 组合多个对象
通过线性组合,可以实现将多个对象的缩放版本组合在一起。
下面尝试对图像进行线性组合操作。首先载入新的图像。
corgis_file, _ = urlretrieve("https://pacman.cs.tsinghua.edu.cn/~hanwentao/cpct/03/corgis.png")
corgis = plt.imread(corgis_file)[:, :, :3] # 去掉 alpha 通道(透明度)
plt.imshow(corgis)
<matplotlib.image.AxesImage at 0x10e7f1450>
下面对图像中每个像素的颜色进行缩放。注意过渡缩放(颜色分量大于 1)会导致饱和。
c = 0.5
plt.imshow(c * corgis)
<matplotlib.image.AxesImage at 0x10e85b390>
交互式结果¶
可以使用 ipywidgets 来创建可以交互的结果。
from ipywidgets import interact
def scale_color(c):
plt.imshow((c * corgis).clip(0, 1))
# 进行交互式操作,最后的分号用于忽略本行代码的输出
interact(scale_color, c=(0.0, 3.0, 0.01));
interactive(children=(FloatSlider(value=1.5, description='c', max=3.0, step=0.01), Output()), _dom_classes=('w…
下面尝试进行多幅图像的组合,需要另外一幅图像,例如可以通过上下颠倒来得到。
upsidedown_corgis = corgis[::-1, :]
plt.imshow(upsidedown_corgis)
<matplotlib.image.AxesImage at 0x10eb05090>
对两幅图像进行组合,看看有什么效果。
plt.imshow(0.5 * upsidedown_corgis + 0.5 * corgis)
<matplotlib.image.AxesImage at 0x10eb63250>
def mix_image(alpha):
plt.imshow(alpha * upsidedown_corgis + (1 - alpha) * corgis)
interact(mix_image, alpha=(0.0, 1.0, 0.01));
interactive(children=(FloatSlider(value=0.5, description='alpha', max=1.0, step=0.01), Output()), _dom_classes…
Photoshop 中的操作¶
在使用 Photoshop 处理图像时,会发现滤镜(Photoshop 滤镜参考)很有用。例如:
- 模糊
- 锐化
- 风格化 → 查找边缘
- 像素化
- 扭曲
其中一些变换(如模糊、锐化、查找边缘)是通过计算卷积(convolution)来实现的。卷积非常高效,在当前的人工智能(机器学习)中经常出现,特别是在图像识别场景中。
图像滤镜(卷积)¶
卷积的概念¶
在做图像变换或检测等操作时,为了考虑像素点周围的情况,需要一种操作能够去以每个像素为中心,对其周围的像素进行某些运算,并汇总成一个数值。卷积是可以实现这一目标的操作,用来描述具体操作的矩阵被称为卷积核(kernel)。
卷积的实现¶
卷积可以通过多重循环来实现。
def my_convolve(image, kernel):
# 计算卷积后的图像大小(不考虑边界情况),创建结果数组
ih, iw, ic = image.shape
kh, kw = kernel.shape
oh, ow, oc = ih - kh + 1, iw - kw + 1, ic
out = np.zeros((oh, ow, oc))
# 卷积计算
for y in range(oh): # 图像的每行
for x in range(ow): # 图像的每列
for c in range(ic): # 图像的每个通道
for i in range(kh): # 卷积核的每行
for j in range(kw): # 卷积核的每列
out[y, x, c] += image[y + i, x + j, c] * kernel[i, j]
return out
硬件加速器¶
一些重要的计算可以通过使用专用硬件来加速,最常见加速器是 GPU。
图形处理单元(GPU,Graphical Processing Units):
- 最初是作为图像渲染器设计的
- 在其他非常规律的计算中也相当快速
- 卷积由于其规律的结构,是一种非常适合 GPU 的操作
identity = np.array([
0, 0, 0,
0, 1, 0,
0, 0, 0
]).reshape(3, 3)
corgis0 = my_convolve(corgis, identity)
plt.imshow(corgis0)
<matplotlib.image.AxesImage at 0x10ec6fb10>
结果正确,但是速度有点慢。原因是 Python 语言动态特性导致效率的损失,不适用于大量计算的场景。
在 Jupyter 中,可以用 %timeit 命令来测量运行时间:
%timeit -n1 -r1 my_convolve(corgis, identity)
5.31 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
使用 SciPy 中的卷积实现¶
可以使用 SciPy 软件包中的卷积实现来获得更快的运行速度。
from scipy.ndimage import convolve
corgis0 = convolve(corgis, identity, axes=(0, 1)) # 需要明确指定做卷积的维度
plt.imshow(corgis0)
<matplotlib.image.AxesImage at 0x10f593890>
结果一致,速度也很快。SciPy 中的卷积是用 C 语言实现的,然后给出了 Python 调用的接口。
%timeit convolve(corgis, identity, axes=(0, 1))
1.67 ms ± 8.89 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
边界探测¶
边界探测需要考虑四周的情况,卷积核是
$$ \begin{bmatrix} 0 & -1 & 0 \\ -1 & \ \ 4 & -1 \\ 0 & -1 & 0 \end{bmatrix} $$
edge_detect = np.array([
0, -1, 0,
-1, 4, -1,
0, -1, 0
]).reshape(3, 3)
#corgis1 = convolve(corgis, edge_detect, axes=(0, 1))
#plt.imshow(corgis1)
corgis1 = np.abs(convolve(corgis, edge_detect, axes=(0, 1))).sum(axis=2)
plt.imshow(corgis1, cmap='gray')
<matplotlib.image.AxesImage at 0x10f9b9f90>
锐化¶
在原始图像的基础上增强边界可以实现锐化效果,卷积核是
$$ \begin{bmatrix} 0 & -1 & 0 \\ -1 & \ \ 5 & -1 \\ 0 & -1 & 0 \\ \end{bmatrix} $$
sharpen = identity + edge_detect
corgis2 = convolve(corgis, sharpen, axes=(0, 1)).clip(0, 1)
plt.imshow(corgis2)
<matplotlib.image.AxesImage at 0x10fa13ed0>
模糊¶
跟周围做平均可以达到模糊的效果,卷积核是
$$ \frac{1}{9} \begin{bmatrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \\ \end{bmatrix} $$
box_blur = np.full((3, 3), 1) / 9
corgis3 = convolve(corgis, box_blur, axes=(0, 1))
plt.imshow(corgis3)
<matplotlib.image.AxesImage at 0x10fac9e50>
思考:上述操作为什么不需要 clip?
水平方向变化探测¶
对左右两边的像素求差值可以探测水平方向上的变化,卷积核是
$$ \begin{bmatrix} -1 & 0 & 1 \\ -1 & 0 & 1 \\ -1 & 0 & 1 \\ \end{bmatrix} $$
dx = np.array([
-1, 0, 1,
-1, 0, 1,
-1, 0, 1
]).reshape(3, 3) / 2
corgis4 = np.abs(convolve(corgis, dx, axes=(0, 1))).sum(axis=2)
plt.imshow(corgis4, cmap='gray')
<matplotlib.image.AxesImage at 0x10fb7c050>
竖直方向变化探测¶
对上下两边的像素求差值可以探测竖直方向上的变化,卷积核是
$$ \begin{bmatrix} -1 & -1 & -1 \\ 0 & 0 & 0 \\ 1 & 1 & 1 \\ \end{bmatrix} $$
dy = dx.T
corgis5 = np.abs(convolve(corgis, dy, axes=(0, 1))).sum(axis=2)
plt.imshow(corgis5, cmap='gray')
<matplotlib.image.AxesImage at 0x10fbda210>
卷积的应用:卷积神经网络¶
在图像识别等任务中,先要识别各个局部,然后再看更全局的信息。这时就可以用卷积来提取局部信息,称为卷积神经网络(Convolutional Neural Networks,CNN)。