图像基础¶
图像基础解释了数字图像在传入任何模型之前是如何表示、形成和预处理的。本文件涵盖像素、色彩空间(RGB、HSV、YCbCr、LAB)、小孔相机模型、卷积、边缘检测(Sobel、Canny)、直方图以及特征描述符(SIFT、ORB),是底层视觉的工具包。
-
数字图像是一个由数字组成的二维网格。网格中的每个单元格称为一个像素(picture element),其数值表示强度或颜色。灰度图像是一个单独的二维矩阵,每个像素存储一个亮度值,对于8位图像,通常范围从0(黑色)到255(白色)。
-
彩色图像则扩展为三个通道。在 RGB 色彩空间中,每个像素存储三个数值:红色、绿色和蓝色的强度。
-
彩色图像是一个形状为(高度,宽度,3)的三维张量(矩阵)。将这三个通道以不同强度混合,就能产生可见的全光谱颜色。
-
位深度决定了每个通道可以表示的离散强度级数量。
-
8位图像每个通道有 \(2^8 = 256\) 个级别,因此总共可以表示 \(256^3 \approx 1670\) 万种颜色。16位图像每个通道有65,536个级别,用于医学成像和高动态范围摄影等需要精细强度区分的场景。
-
RGB 便于显示,但其他色彩空间更适合不同的任务。
-
HSV(色相、饱和度、明度)将颜色信息与亮度分离。色相是纯颜色(在色环上取值0-360度),饱和度表示颜色的鲜艳程度(0 = 灰色,1 = 纯色),明度则是亮度。HSV 在基于颜色的分割中非常有用,因为可以仅根据色相进行阈值处理,而不受光照条件影响。在 HSV 中检测“红色物体”比在 RGB 中容易得多。
-
YCbCr 将亮度(Y,感知亮度)与色度(Cb、Cr,色差信号)分离。这是 JPEG 压缩和视频编解码器使用的色彩空间。人眼对亮度比颜色更敏感,因此色度可以用较低分辨率存储(色度子采样)而几乎不影响视觉感受。
-
LAB(CIELAB)的设计使得两个颜色之间的数值距离与人类感知的差异相对应。在 LAB 空间中,等步长的变化看起来与人类观察者的等步长感受一致。L 通道是亮度,A 通道从绿色到红色,B 通道从蓝色到黄色。当需要感知均匀的颜色比较时,可以使用 LAB。
-
图像形成描述了三维场景如何变成二维图像。最简单的模型是小孔相机:场景的光线通过一个小孔,投射到后面的传感器平面上。世界坐标中的一点 \((X, Y, Z)\) 投影到像素坐标 \((u, v)\):
- 这个 3x3 矩阵就是内部参数矩阵 \(K\)。它编码了相机的内部属性:焦距 \(f_x, f_y\)(透镜会聚光线的强度)和主点 \((c_x, c_y)\)(光轴与传感器相交的点,通常靠近图像中心)。对于给定的相机和镜头组合,这些参数是固定的。
- 外部参数描述了相机在世界中的位置:一个旋转矩阵 \(R\)(3x3,来自第02章)和一个平移向量 \(t\)(3x1)。它们共同将世界坐标转换为相机坐标。完整的投影公式为:
-
其中 \(\mathbf{P} = [X, Y, Z, 1]^T\) 是齐次坐标下的三维点,\(\mathbf{p} = [u, v, 1]^T\) 是投影后的像素。矩阵 \([R \mid t]\) 是 3x4 的,将旋转和平移并排放置。这完全是第02章的线性代数内容。
-
实际镜头会引入畸变。
- 径向畸变使直线弯曲成曲线(桶形畸变使图像向外凸出;枕形畸变使图像向内凹陷)。
- 切向畸变 发生在镜头不与传感器完美平行时。
-
相机标定通过拍摄已知图案(如棋盘格)的图像来估计内部参数和畸变系数,然后可对图像进行畸变校正(去畸变)。
-
空间滤波是经典图像处理的基础。滤波器(或称为核)是一个小矩阵(通常是 3x3 或 5x5),在图像上滑动。在每个位置,滤波器的数值与重叠的图像块逐元素相乘并求和,产生一个输出像素。这就是二维卷积,与驱动 CNN(文件02)的运算相同,但这里的滤波器权重是手工设计的,而不是学习得来的。
-
这是第06章中一维卷积的二维扩展。滤波器决定了运算检测到什么:不同的滤波器检测不同的特征。
-
模糊通过对邻近像素取平均来平滑图像。盒式滤波器对所有邻域像素赋予相同的权重。
-
高斯滤波器使用二维高斯分布(第05章)对邻域像素进行加权,距离越近的像素权重越大,距离越远的权重越小。高斯模糊是最常见的平滑操作,由参数 \(\sigma\) 控制:\(\sigma\) 越大,平滑程度越高。
-
中值滤波将每个像素替换为其邻域的中值,而不是加权平均值。它对去除椒盐噪声(随机出现的黑白像素)特别有效,同时能保留边缘,因为中值对离群值具有鲁棒性(参见第04章)。
-
边缘检测用于识别像素强度发生急剧变化的边界。边缘承载了图像中的大部分结构信息;仅凭边缘就能识别物体。
-
Sobel算子使用两个 3x3 滤波器来估计水平方向和垂直方向的梯度:
-
将图像与 \(G_x\) 卷积得到水平梯度(在垂直边缘处响应强烈),与 \(G_y\) 卷积得到垂直梯度(在水平边缘处响应强烈)。
-
梯度幅值 \(\sqrt{G_x^2 + G_y^2}\) 和方向 \(\arctan(G_y / G_x)\) 一起描述了每个像素处的边缘强度和方向。这是第03章中梯度概念在图像域的对偶。
-
Canny边缘检测器是边缘检测的黄金标准。它包含四个步骤:
- 用高斯滤波器平滑图像以减少噪声
- 计算梯度幅值和方向(使用 Sobel 算子)
- 非极大值抑制:只保留沿梯度方向上是局部最大值的像素,从而细化边缘
- 双阈值滞后处理:使用两个阈值(高阈值和低阈值)。高于高阈值的像素确定为边缘。介于两个阈值之间的像素仅当与确定的边缘相连时才被视为边缘。低于低阈值的像素被丢弃。
-
Canny 中的双阈值使其比单一阈值更鲁棒:强边缘始终被保留,而弱边缘只有属于连续边缘结构时才被保留。
-
频域分析可以揭示在空间域中难以看到的模式。二维傅里叶变换(延续第03章的一维版本)将图像分解为不同频率和方向上的二维正弦模式之和:
-
低频对应平滑、变化缓慢的区域(如天空、墙壁)。高频对应尖锐的过渡(边缘、纹理、噪声)。幅值谱显示了每个频率上存在多少能量,而相位谱则编码了空间排列信息。
-
低通滤波去除高频,从而平滑图像(等效于空间域的高斯模糊)。高通滤波去除低频,从而增强边缘和精细细节。带通滤波只保留某一频段,可用于纹理分析。
-
实际应用中,对于大尺寸滤波器,在频域进行滤波可能比空间域卷积更快,因为空间域中的卷积等价于频域中的逐元素乘法(卷积定理)。这直接联系到第03章的傅里叶变换性质。
-
直方图总结了像素强度的分布。直方图统计每个强度值(对于8位图像为0-255)有多少个像素。这是第04章中的频率分布概念应用于像素值的结果。
-
暗图像的直方图集中在左侧(低值)。亮图像的直方图集中在右侧。低对比度图像的直方图狭窄。高对比度图像的直方图宽而分散。
-
直方图均衡化将直方图拉伸到整个强度范围,从而提高对比度。其思想是找到一个映射,使得像素强度的累积分布函数(CDF)近似线性。这是第04章中 CDF 概念的直接应用。
-
大津法自动找到最佳阈值,将图像分割为前景和背景。它会尝试每一个可能的阈值,并选择使类内方差最小(等价于使类间方差最大)的那个阈值。这与第04章中的方差概念相同,应用于像素强度群体。
-
特征提取是在图像中识别出独特的点或区域,可用于匹配、识别和三维重建。好的特征应该具有可重复性(在不同视角下能被再次找到)、独特性(能与其他特征区分开)以及计算效率高。
-
角点检测用于寻找图像强度在多个方向上都有显著变化的点。平滑区域在任何方向变化都很小。边缘在一个方向上有变化。角点至少在两个方向上有变化,因此具有局部唯一性,成为可靠的标志点。
-
Harris角点检测器在每个像素处分析结构张量(也称为二阶矩矩阵):
-
其中 \(I_x\) 和 \(I_y\) 是图像梯度(用 Sobel 计算),\(W\) 是局部窗口,\(w\) 是高斯加权函数。矩阵 \(M\) 的特征值(来自第02章)显示了特征类型:
- 两个特征值都很小:平坦区域(无特征)
- 一个大,一个小:边缘
- 两个都大:角点
-
Harris 不直接计算特征值,而是使用角点响应函数:\(R = \det(M) - k \cdot (\text{trace}(M))^2\),其中 \(\det(M) = \lambda_1 \lambda_2\),\(\text{trace}(M) = \lambda_1 + \lambda_2\)(均来自第02章)。大的正 \(R\) 表示角点。常数 \(k\) 通常取 0.04-0.06。
-
Shi-Tomasi 检测器将其简化为 \(R = \min(\lambda_1, \lambda_2)\),直接检查较小的特征值是否足够大。实际应用中这一方法略为稳定。
-
斑点检测寻找与其周围环境不同的区域。与角点(点特征)不同,斑点具有特征尺寸。
-
SIFT(尺度不变特征变换,Lowe, 2004)在多尺度上检测斑点,并构建一个对旋转、尺度具有不变性,且对光照变化部分不变的描述子。其步骤包括:
- 使用逐渐增大 \(\sigma\) 的高斯模糊构建尺度空间(见下文)
- 在尺度空间的高斯差分(DoG)中寻找极值点
- 精炼关键点位置,去除低对比度点和边缘响应点
- 基于局部梯度方向分配主方向
- 在关键点周围的 16x16 区域内,从梯度直方图构建 128 维描述子
-
SURF(加速稳健特征)使用盒式滤波器和积分图像近似 SIFT,计算速度更快。ORB(Oriented FAST and Rotated BRIEF)是一种快速、开源的替代方案,结合了 FAST 角点检测器和 BRIEF 二进制描述子,并增加了旋转不变性。
-
HOG(方向梯度直方图)描述子将图像划分为小的单元格(cell),在每个单元格内计算梯度方向的直方图,然后在由多个单元格组成的块(block)上进行归一化。HOG 捕获了边缘方向的分布,对物体形状信息非常有效。在深度学习出现之前,HOG + SVM(第06章)是行人检测和物体识别的主流方法。
-
图像金字塔以多种分辨率表示图像。
- 高斯金字塔通过反复模糊和下采样(分辨率减半)构建。每一层都是原始图像的更粗糙版本。
- 拉普拉斯金字塔存储连续高斯层之间的差异,捕获了每次下采样过程中丢失的细节。拉普拉斯金字塔是可逆的:可以从它重建原始图像。
- 尺度空间形式化了物体存在于不同尺度的概念。一棵树是一个大的斑点;树上的叶子则是小的斑点。为了同时检测两者,需要在尺度空间中进行搜索。图像的尺度空间是通过将图像与不同 \(\sigma\) 的高斯核卷积得到的一系列图像:
- 其中 \(G\) 是标准差为 \(\sigma\) 的二维高斯核。在多个尺度上持续存在的特征更有可能是有意义的结构而非噪声。尺度空间是 SIFT 以及现代计算机视觉中多尺度处理(包括物体检测中的特征金字塔网络,文件03)的理论基础。
编程任务(使用 CoLab 或 notebook)¶
-
加载一张图像,将其转换为不同的色彩空间(RGB、HSV、LAB),并可视化各个通道。观察颜色信息在不同空间中的分布差异。
import jax.numpy as jnp import matplotlib.pyplot as plt from PIL import Image import numpy as np # 创建一个具有明显颜色的合成测试图像 H, W = 128, 256 img = np.zeros((H, W, 3), dtype=np.uint8) img[:, :64] = [255, 50, 50] # 红色 img[:, 64:128] = [50, 255, 50] # 绿色 img[:, 128:192] = [50, 50, 255] # 蓝色 img[:, 192:] = [255, 255, 50] # 黄色 # 添加亮度渐变 for y in range(H): scale = 0.3 + 0.7 * y / H img[y] = (img[y] * scale).astype(np.uint8) img_jnp = jnp.array(img, dtype=jnp.float32) / 255.0 # 手动实现 RGB 到 HSV 的转换 def rgb_to_hsv(rgb): r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2] maxc = jnp.max(rgb, axis=-1) minc = jnp.min(rgb, axis=-1) diff = maxc - minc + 1e-7 # 色相 h = jnp.where(maxc == minc, 0.0, jnp.where(maxc == r, 60 * ((g - b) / diff % 6), jnp.where(maxc == g, 60 * ((b - r) / diff + 2), 60 * ((r - g) / diff + 4)))) s = jnp.where(maxc < 1e-7, 0.0, diff / maxc) v = maxc return jnp.stack([h / 360, s, v], axis=-1) hsv = rgb_to_hsv(img_jnp) fig, axes = plt.subplots(2, 3, figsize=(14, 8)) for i, (ch, name) in enumerate(zip([img_jnp[...,0], img_jnp[...,1], img_jnp[...,2]], ['红色', '绿色', '蓝色'])): axes[0, i].imshow(ch, cmap='gray', vmin=0, vmax=1) axes[0, i].set_title(f'RGB: {name}'); axes[0, i].axis('off') for i, (ch, name) in enumerate(zip([hsv[...,0], hsv[...,1], hsv[...,2]], ['色相', '饱和度', '明度'])): axes[1, i].imshow(ch, cmap='gray', vmin=0, vmax=1) axes[1, i].set_title(f'HSV: {name}'); axes[1, i].axis('off') plt.suptitle('RGB 通道 vs HSV 通道') plt.tight_layout(); plt.show() -
使用二维卷积从头实现 Sobel 边缘检测和高斯模糊。将它们应用于一张图像并比较结果。
import jax import jax.numpy as jnp import matplotlib.pyplot as plt def conv2d(image, kernel): """从零实现的二维卷积(valid 模式)。""" H, W = image.shape kH, kW = kernel.shape out_h, out_w = H - kH + 1, W - kW + 1 output = jnp.zeros((out_h, out_w)) for i in range(out_h): for j in range(out_w): patch = image[i:i+kH, j:j+kW] output = output.at[i, j].set(jnp.sum(patch * kernel)) return output # 创建一个测试图像:深色背景上的白色矩形 img = jnp.zeros((64, 64)) img = img.at[15:50, 20:45].set(1.0) # 添加一些噪声 key = jax.random.PRNGKey(42) img = img + jax.random.normal(key, img.shape) * 0.05 # Sobel 滤波器 sobel_x = jnp.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=jnp.float32) sobel_y = jnp.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=jnp.float32) # 高斯模糊核(5x5, sigma=1) ax = jnp.arange(-2, 3, dtype=jnp.float32) xx, yy = jnp.meshgrid(ax, ax) gaussian = jnp.exp(-(xx**2 + yy**2) / (2 * 1.0**2)) gaussian = gaussian / gaussian.sum() # 应用滤波器 gx = conv2d(img, sobel_x) gy = conv2d(img, sobel_y) edges = jnp.sqrt(gx**2 + gy**2) blurred = conv2d(img, gaussian) fig, axes = plt.subplots(1, 4, figsize=(16, 4)) for ax, data, title in zip(axes, [img, edges, blurred, gx], ['原始图像', '边缘幅值', '高斯模糊', '水平梯度']): ax.imshow(data, cmap='gray') ax.set_title(title); ax.axis('off') plt.tight_layout(); plt.show() -
从头实现直方图均衡化,并将其应用于一张低对比度的灰度图像。比较均衡化前后的直方图。
import jax.numpy as jnp import matplotlib.pyplot as plt # 创建一个低对比度图像(像素值聚集在一个狭窄的范围) key = __import__('jax').random.PRNGKey(42) img = __import__('jax').random.uniform(key, (128, 128)) * 0.3 + 0.3 # 值在 [0.3, 0.6] def histogram_equalise(img, n_bins=256): """灰度图像的直方图均衡化。""" # 量化到 bins bins = jnp.linspace(0, 1, n_bins + 1) hist = jnp.histogram(img, bins=bins)[0] # 计算 CDF cdf = jnp.cumsum(hist) cdf_normalised = (cdf - cdf.min()) / (cdf.max() - cdf.min()) # 通过 CDF 映射每个像素 indices = jnp.clip((img * n_bins).astype(jnp.int32), 0, n_bins - 1) equalised = cdf_normalised[indices] return equalised eq_img = histogram_equalise(img) fig, axes = plt.subplots(2, 2, figsize=(12, 10)) axes[0, 0].imshow(img, cmap='gray', vmin=0, vmax=1) axes[0, 0].set_title('原始图像(低对比度)'); axes[0, 0].axis('off') axes[0, 1].imshow(eq_img, cmap='gray', vmin=0, vmax=1) axes[0, 1].set_title('直方图均衡化后'); axes[0, 1].axis('off') axes[1, 0].hist(img.ravel(), bins=64, color='#3498db', alpha=0.8) axes[1, 0].set_title('均衡化前的直方图'); axes[1, 0].set_xlim(0, 1) axes[1, 1].hist(eq_img.ravel(), bins=64, color='#e74c3c', alpha=0.8) axes[1, 1].set_title('均衡化后的直方图'); axes[1, 1].set_xlim(0, 1) plt.tight_layout(); plt.show() -
从头实现 Harris 角点检测器。在一张简单的图像中检测角点并可视化。
import jax import jax.numpy as jnp import matplotlib.pyplot as plt def harris_corners(img, k=0.05, threshold=0.01): """从零实现的 Harris 角点检测。""" # 使用 Sobel 计算梯度 sobel_x = jnp.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=jnp.float32) sobel_y = jnp.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=jnp.float32) # 对图像进行填充,以便进行 valid 卷积并保持输出尺寸 img_pad = jnp.pad(img, 1, mode='edge') H, W = img.shape Ix = jnp.zeros_like(img) Iy = jnp.zeros_like(img) for i in range(H): for j in range(W): patch = img_pad[i:i+3, j:j+3] Ix = Ix.at[i, j].set(jnp.sum(patch * sobel_x)) Iy = Iy.at[i, j].set(jnp.sum(patch * sobel_y)) # 结构张量分量 Ixx = Ix * Ix Iyy = Iy * Iy Ixy = Ix * Iy # 对结构张量进行高斯平滑(这里用窗口求和近似) w = 3 # 窗口半宽 R = jnp.zeros_like(img) pad_xx = jnp.pad(Ixx, w, mode='constant') pad_yy = jnp.pad(Iyy, w, mode='constant') pad_xy = jnp.pad(Ixy, w, mode='constant') for i in range(H): for j in range(W): sxx = jnp.sum(pad_xx[i:i+2*w+1, j:j+2*w+1]) syy = jnp.sum(pad_yy[i:i+2*w+1, j:j+2*w+1]) sxy = jnp.sum(pad_xy[i:i+2*w+1, j:j+2*w+1]) det = sxx * syy - sxy * sxy trace = sxx + syy R = R.at[i, j].set(det - k * trace * trace) # 阈值处理 corners = R > threshold * R.max() return R, corners # 测试图像:棋盘格图案(含有大量角点) block = 16 n = 4 checker = jnp.zeros((block * n, block * n)) for i in range(n): for j in range(n): if (i + j) % 2 == 0: checker = checker.at[i*block:(i+1)*block, j*block:(j+1)*block].set(1.0) R, corners = harris_corners(checker) cy, cx = jnp.where(corners) fig, axes = plt.subplots(1, 3, figsize=(14, 4)) axes[0].imshow(checker, cmap='gray') axes[0].set_title('棋盘格'); axes[0].axis('off') axes[1].imshow(R, cmap='hot') axes[1].set_title('Harris 响应'); axes[1].axis('off') axes[2].imshow(checker, cmap='gray') axes[2].scatter(cx, cy, c='#e74c3c', s=15, marker='x') axes[2].set_title(f'检测到的角点 ({len(cx)})'); axes[2].axis('off') plt.tight_layout(); plt.show()