图像就是矩阵
一张灰度图片本质上就是一个二维 NumPy 数组。对矩阵做转置、翻转、裁剪、卷积——图片就会跟着变。
学完本案例你将理解:图像处理的底层就是线性代数,矩阵就是数字表格。
生活引入
照片的像素方格
把你手机里的一张照片放大、再放大——直到你能看到一个个小方格。每个方格只有一个颜色值:0 代表纯黑,255 代表纯白,中间的数字是不同深浅的灰色。
整张照片就是这些小方格排成的矩形阵列——数学家称之为「矩阵」。
直观理解
矩阵不是什么高深的概念——它就是一张填满数字的表格。
比如下面这个 \(3 \times 3\) 的矩阵,每个数字代表一个像素的亮度(0 最暗,1 最亮):
3x3 灰度图
一个矩阵,9 个数字
有了这个认知,图像处理的各种操作就豁然开朗了:
转置
img.T
沿对角线翻转
水平镜像
img[:, ::-1]
左右翻转
垂直镜像
img[::-1, :]
上下翻转
裁剪
img[10:54, 10:54]
截取子区域
模糊
卷积核滑动均值
邻域平均
旋转
坐标变换
矩阵乘法
你在 Photoshop 里点一个按钮,背后都是 NumPy 在操作一个二维数组。
数学定义
一张 \(H \times W\) 的灰度图像可以表示为一个实数矩阵:
\[ M \in \mathbb{R}^{H \times W}, \quad M_{ij} \in [0, 1] \]其中 \(M_{ij}\) 代表第 \(i\) 行、第 \(j\) 列的像素亮度值。
矩阵操作的核心概念:
| 操作 | 数学表达 | NumPy 写法 | 图像效果 |
|---|---|---|---|
| 转置 | \(M^T_{ij} = M_{ji}\) | img.T | 行列互换,沿对角线翻转 |
| 水平翻转 | \(M'_{ij} = M_{i, W-1-j}\) | img[:, ::-1] | 列索引反转 |
| 垂直翻转 | \(M'_{ij} = M_{H-1-i, j}\) | img[::-1, :] | 行索引反转 |
| 均值模糊 | \(M'_{ij} = \frac{1}{k^2}\sum_{p,q} M_{i+p, j+q}\) | 滑动窗口求 mean() | 每个像素替换为邻域均值 |
均值模糊用到的「滑动窗口」操作,本质上就是卷积的雏形——卷积神经网络(CNN)的核心运算。
Python 动手实践
生成一张合成灰度图,用 NumPy 数组操作实现转置、翻转、裁剪和手写卷积模糊。
实例
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import os
# 创建输出目录
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs")
os.makedirs(OUT, exist_ok=True)
# 1. 生成一张合成灰度图:对角渐变 + 中间亮方块
size = 64
img = np.zeros((size, size))
for i in range(size):
for j in range(size):
img[i, j] = (i + j) / (2 * size) # 从暗到亮的对角渐变
img[20:44, 20:44] += 0.6 # 中间加一个亮方块
img = np.clip(img, 0, 1) # 限制在 [0, 1] 范围内
print("RUNOOB 图像矩阵形状 (shape):", img.shape)
print("图像矩阵前 3x3 部分:\n", np.round(img[:3, :3], 3))
# 2. 矩阵运算 = 图像变换
img_transpose = img.T # 转置:行列互换
img_flip_lr = img[:, ::-1] # 水平镜像:列索引反转
img_flip_ud = img[::-1, :] # 垂直镜像:行索引反转
img_crop = img[10:54, 10:54] # 裁剪:切片取子矩阵
# 3. 手写均值模糊(5x5 卷积核,不调 OpenCV)
def box_blur(mat, k=5):
"""用 k x k 滑动窗口对矩阵做均值模糊"""
pad = k // 2
# 边界用边缘值填充(edge padding)
padded = np.pad(mat, pad, mode="edge")
out = np.zeros_like(mat)
for i in range(mat.shape[0]):
for j in range(mat.shape[1]):
# 取 k x k 邻域,求均值
out[i, j] = padded[i:i+k, j:j+k].mean()
return out
img_blur = box_blur(img, k=5)
print("RUNOOB 模糊后矩阵前 3x3 部分:\n",
np.round(img_blur[:3, :3], 3))
# 4. 保存可视化对比图
fig, axes = plt.subplots(2, 3, figsize=(12, 8))
titles = ["原图 (矩阵)", "转置 img.T",
"水平镜像 img[:, ::-1]",
"垂直镜像 img[::-1, :]",
"裁剪 img[10:54, 10:54]",
"均值模糊 (5x5 卷积)"]
images = [img, img_transpose, img_flip_lr,
img_flip_ud, img_crop, img_blur]
for ax, title, im in zip(axes.flat, titles, images):
ax.imshow(im, cmap="gray", vmin=0, vmax=1)
ax.set_title(title, fontsize=11)
ax.axis("off")
plt.tight_layout()
plt.savefig(os.path.join(OUT, "01_image_as_matrix.png"), dpi=130)
print(f"\nRUNOOB 可视化结果已保存到 {OUT}/01_image_as_matrix.png")
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import os
# 创建输出目录
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "outputs")
os.makedirs(OUT, exist_ok=True)
# 1. 生成一张合成灰度图:对角渐变 + 中间亮方块
size = 64
img = np.zeros((size, size))
for i in range(size):
for j in range(size):
img[i, j] = (i + j) / (2 * size) # 从暗到亮的对角渐变
img[20:44, 20:44] += 0.6 # 中间加一个亮方块
img = np.clip(img, 0, 1) # 限制在 [0, 1] 范围内
print("RUNOOB 图像矩阵形状 (shape):", img.shape)
print("图像矩阵前 3x3 部分:\n", np.round(img[:3, :3], 3))
# 2. 矩阵运算 = 图像变换
img_transpose = img.T # 转置:行列互换
img_flip_lr = img[:, ::-1] # 水平镜像:列索引反转
img_flip_ud = img[::-1, :] # 垂直镜像:行索引反转
img_crop = img[10:54, 10:54] # 裁剪:切片取子矩阵
# 3. 手写均值模糊(5x5 卷积核,不调 OpenCV)
def box_blur(mat, k=5):
"""用 k x k 滑动窗口对矩阵做均值模糊"""
pad = k // 2
# 边界用边缘值填充(edge padding)
padded = np.pad(mat, pad, mode="edge")
out = np.zeros_like(mat)
for i in range(mat.shape[0]):
for j in range(mat.shape[1]):
# 取 k x k 邻域,求均值
out[i, j] = padded[i:i+k, j:j+k].mean()
return out
img_blur = box_blur(img, k=5)
print("RUNOOB 模糊后矩阵前 3x3 部分:\n",
np.round(img_blur[:3, :3], 3))
# 4. 保存可视化对比图
fig, axes = plt.subplots(2, 3, figsize=(12, 8))
titles = ["原图 (矩阵)", "转置 img.T",
"水平镜像 img[:, ::-1]",
"垂直镜像 img[::-1, :]",
"裁剪 img[10:54, 10:54]",
"均值模糊 (5x5 卷积)"]
images = [img, img_transpose, img_flip_lr,
img_flip_ud, img_crop, img_blur]
for ax, title, im in zip(axes.flat, titles, images):
ax.imshow(im, cmap="gray", vmin=0, vmax=1)
ax.set_title(title, fontsize=11)
ax.axis("off")
plt.tight_layout()
plt.savefig(os.path.join(OUT, "01_image_as_matrix.png"), dpi=130)
print(f"\nRUNOOB 可视化结果已保存到 {OUT}/01_image_as_matrix.png")
RUNOOB 图像矩阵形状 (shape): (64, 64) 图像矩阵前 3x3 部分: [[0. 0.008 0.016] [0.008 0.016 0.023] [0.016 0.023 0.031]] RUNOOB 模糊后矩阵前 3x3 部分: [[0.009 0.011 0.013] [0.01 0.013 0.016] [0.011 0.016 0.021]] RUNOOB 可视化结果已保存到 .../outputs/01_image_as_matrix.png
可以看到,模糊后相邻像素之间的数值差异变小了——这正是「模糊」的数学本质:用邻域均值替代原像素值。
AI 中的应用场景
| AI 场景 | 如何使用矩阵 |
|---|---|
| 卷积神经网络 (CNN) | 输入是一批图片组成的 4 阶张量 (batch, H, W, C),每一层用卷积核(小矩阵)在输入上滑动 |
| 数据增强 (Augmentation) | 训练时随机翻转、旋转、裁剪图片——全部是矩阵操作,用于扩充训练集 |
| 图像预处理 | 送入模型前统一裁剪到固定尺寸(如 224x224)——就是矩阵切片和缩放 |
| 风格迁移 | 将图片表示为特征矩阵,通过矩阵运算改变「风格」特征 |
NumPy 中的 ndarray 和 PyTorch 中的 Tensor,本质上都是同一个东西:多维数字表格。理解了矩阵,就理解了深度学习框架里最基础的数据结构。
