跳到主要内容

CNN 例子之图像中目标的边缘检测

边缘检测是图像处理中的一个常见任务,它旨在识别图像中的边缘或突变。卷积操作是实现边缘检测的一种方法,通过使用特定的卷积核(或滤波器)来强调图像中的边缘。

这里我们通过介绍传统的卷积子边缘检测再过渡到如何使用 CNN 进行边缘检测来学习卷积操作。

传统的卷积子边缘检测

边缘是什么?

在图像处理中,边缘是图像亮度的显著变化。这些变化通常对应于物体的边界、物体的纹理或其他图像中的重要特征。边缘检测的目的是识别图像中的这些显著变化。

如何检测边缘?

边缘检测的基本原理是寻找图像中的亮度梯度。梯度是一个向量,其方向指向最大的亮度变化,其大小表示亮度变化的速度。当梯度的大小超过某个阈值时,我们可以认为在该位置存在一个边缘。

Sobel滤波器是一种常用的边缘检测方法。它通过计算图像的水平和垂直梯度来检测边缘。

Sobel 滤波器

Sobel 滤波器是用于边缘检测的常见滤波器之一。它包括两个3x3的卷积核,一个用于检测水平边缘,另一个用于检测垂直边缘。

水平Sobel滤波器:

Sx=[101202101]S_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \\ \end{bmatrix}

垂直Sobel滤波器:

Sy=[121000121]S_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \\ \end{bmatrix}

边缘检测过程

  1. 将水平和垂直的Sobel滤波器应用于图像。
  2. 对于每个像素位置,计算两个滤波器的输出的平方和的平方根,得到边缘强度。
  3. (可选)应用阈值来确定边缘。

在 PyTorch 中使用卷积层

以下是使用 PyTorch 和一个简单的张量进行边缘检测的代码:

import torch
import torch.nn.functional as F

# 定义模拟图像
image_tensor = torch.tensor([
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.]
]).unsqueeze(0).unsqueeze(0)

# 定义 Sobel 滤波器
sobel_horizontal = torch.Tensor([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]).unsqueeze(0).unsqueeze(0)
sobel_vertical = torch.Tensor([[-1, -2, -1], [0, 0, 0], [1, 2, 1]]).unsqueeze(0).unsqueeze(0)

# 应用滤波器
edge_horizontal = F.conv2d(image_tensor, sobel_horizontal, padding=1)
edge_vertical = F.conv2d(image_tensor, sobel_vertical, padding=1)

# 计算边缘强度
combined_edges = torch.sqrt(edge_horizontal**2 + edge_vertical**2)

print("Original Image Tensor:")
print(image_tensor[0][0])
print("\nHorizontal Edges Tensor:")
print(edge_horizontal[0][0])
print("\nVertical Edges Tensor:")
print(edge_vertical[0][0])
print("\nCombined Edges Tensor:")
print(combined_edges[0][0])

这段代码将使用一个简单的张量模拟图像,应用 Sobel 滤波器,并显示原始图像和检测到的边缘。您可以在自己的环境中运行此代码以查看结果。

输出结果

Original Image Tensor:
tensor([[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.]])

Horizontal Edges Tensor:
tensor([[ 3., -3., -3., 0., 0., 3., 3., -3.],
[ 4., -4., -4., 0., 0., 4., 4., -4.],
[ 4., -4., -4., 0., 0., 4., 4., -4.],
[ 4., -4., -4., 0., 0., 4., 4., -4.],
[ 4., -4., -4., 0., 0., 4., 4., -4.],
[ 3., -3., -3., 0., 0., 3., 3., -3.]])

Vertical Edges Tensor:
tensor([[ 3., 3., 1., 0., 0., 1., 3., 3.],
[ 0., 0., 0., 0., 0., 0., 0., 0.],
[ 0., 0., 0., 0., 0., 0., 0., 0.],
[ 0., 0., 0., 0., 0., 0., 0., 0.],
[ 0., 0., 0., 0., 0., 0., 0., 0.],
[-3., -3., -1., 0., 0., -1., -3., -3.]])

Combined Edges Tensor:
...
[4.0000, 4.0000, 4.0000, 0.0000, 0.0000, 4.0000, 4.0000, 4.0000],
[4.0000, 4.0000, 4.0000, 0.0000, 0.0000, 4.0000, 4.0000, 4.0000],
[4.0000, 4.0000, 4.0000, 0.0000, 0.0000, 4.0000, 4.0000, 4.0000],
[4.2426, 4.2426, 3.1623, 0.0000, 0.0000, 3.1623, 4.2426, 4.2426]])
提示

这里的输出的结果代表什么含义?

  1. 水平边缘:当我们应用水平Sobel滤波器时,我们计算的是图像的水平梯度。输出的结果表示图像在水平方向上的亮度变化。正值表示从暗到亮的变化,负值表示从亮到暗的变化。

  2. 垂直边缘:当我们应用垂直Sobel滤波器时,我们计算的是图像的垂直梯度。输出的结果表示图像在垂直方向上的亮度变化。

  3. 组合边缘:为了得到图像中所有边缘的完整表示,我们可以组合水平和垂直梯度。这通常是通过计算两个梯度的平方和的平方根来完成的。结果是一个表示边缘强度的图像,其中较高的值表示边缘的存在。

总的来说,边缘检测的输出提供了图像中亮度变化的映射。这些变化可能对应于物体的边界、物体的纹理或其他重要特征。边缘检测是许多图像处理和计算机视觉任务的基础,如物体检测、图像分割和特征提取。

使用 CNN 进行边缘检测

如果我们只需寻找黑白边缘,那么以上 Sobel 滤波器足以。然而,当有了更复杂数值的卷积核,或者连续的卷积层时,我们不可能手动设计滤波器。那么我们是否可以学习由 X 生成 Y 的卷积核呢?

学习由 XX 生成 YY 的卷积核是深度学习中卷积神经网络(CNN)的基础任务。这通常通过反向传播和梯度下降来完成。以下是一个简化的流程:

  1. 初始化卷积核:首先,我们随机初始化一个卷积核(或多个卷积核,取决于我们想要学习的特征数量)。

  2. 前向传播:使用初始化的卷积核对输入 XX 进行卷积操作,得到输出 ZZ

  3. 计算损失:比较 ZZ 和目标输出 YY 之间的差异,通常使用均方误差或其他损失函数。

  4. 反向传播:使用梯度下降算法来更新卷积核,以减少 ZZYY 之间的差异。

  5. 迭代:重复上述步骤多次,直到损失收敛到一个较小的值。

一个简单的PyTorch例子

假设我们有一个简单的输入 XX 和目标输出 YY,我们想要学习一个 3×33 \times 3 的卷积核。

import torch
import torch.nn as nn
import torch.optim as optim

# 定义输入 X 和目标输出 Y
X = torch.tensor([
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.],
[1., 1., 0., 0., 0., 0., 1., 1.]
]).unsqueeze(0).unsqueeze(0)

Y = torch.tensor([
[0., 1., 0., 0., 0., 0., -1., 0.],
[0., 1., 0., 0., 0., 0., -1., 0.],
[0., 1., 0., 0., 0., 0., -1., 0.],
[0., 1., 0., 0., 0., 0., -1., 0.],
[0., 1., 0., 0., 0., 0., -1., 0.],
[0., 1., 0., 0., 0., 0., -1., 0.]
]).unsqueeze(0).unsqueeze(0)

# 定义一个单一的卷积层
conv = nn.Conv2d(1, 1, 3, padding=1)

# 定义损失函数和优化器
criterion = nn.MSELoss()
optimizer = optim.SGD(conv.parameters(), lr=0.01)

# 训练模型
for epoch in range(1000):
# 前向传播
outputs = conv(X)
loss = criterion(outputs, Y)

# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()

# 打印损失
if epoch % 100 == 0:
print(f"Epoch {epoch}, Loss: {loss.item()}")

# 打印学习到的卷积核
print(conv.weight)

这段代码首先定义了输入 XX 和目标输出 YY。然后,它使用一个 3×33 \times 3 的卷积核进行训练,目标是最小化 XX 经过卷积后的输出与 YY 之间的均方误差。经过多次迭代后,我们得到了一个学习到的卷积核,该卷积核可以将 XX 转换为 YY

Epoch 0, Loss: 0.9060706496238708
Epoch 100, Loss: 0.17186753451824188
Epoch 200, Loss: 0.16850632429122925
Epoch 300, Loss: 0.16808538138866425
Epoch 400, Loss: 0.1678571254014969
Epoch 500, Loss: 0.16769878566265106
Epoch 600, Loss: 0.1675812155008316
Epoch 700, Loss: 0.16748976707458496
Epoch 800, Loss: 0.16741575300693512
Epoch 900, Loss: 0.16735388338565826
Parameter containing:
tensor([[[[ 0.0927, -0.0744, -0.0056],
[ 0.2065, 0.0897, -0.2812],
[ 0.0476, -0.0238, -0.0592]]]], requires_grad=True)
提示

在 PyTorch 中,unsqueeze() 是一个非常有用的方法,用于增加张量的维度。

具体来说,unsqueeze(dim) 会在指定的维度 dim 处为张量增加一个维度,其大小为 1。

例如,假设我们有一个形状为 (A, B) 的二维张量。使用 unsqueeze(0) 会在最前面增加一个维度,使其变为 (1, A, B)。而使用 unsqueeze(1) 会在第二个维度位置增加一个维度,使其变为 (A, 1, B)

在上面的代码中,unsqueeze(0).unsqueeze(0) 被用于将一个二维张量 (height, width) 转换为一个四维张量 (1, 1, height, width)。这是因为 PyTorch 中的卷积操作通常期望输入是一个四维张量,其形状为 (batch_size, channels, height, width)

简单地说:

  • 第一次使用 unsqueeze(0) 是为了添加一个批次维度。
  • 第二次使用 unsqueeze(0) 是为了添加一个通道维度。

这样,我们得到了一个形状为 (1, 1, height, width) 的张量,其中 1 表示批次大小为 1,1 表示通道数为 1。

为什么定义为 3 * 3 的卷积?

在卷积神经网络(CNN)中,3×33 \times 3 的卷积核是一个常见的选择,有以下几个原因:

  1. 感受野大小3×33 \times 3 的卷积核提供了一个小而合适的感受野,可以捕获图像的局部特征。与较大的卷积核相比,它可以更有效地捕获细节。

  2. 参数效率:与较大的卷积核(如 5×55 \times 57×77 \times 7)相比,3×33 \times 3 的卷积核具有更少的参数。例如,一个 3×33 \times 3 的卷积核有 9 个参数,而一个 5×55 \times 5 的卷积核有 25 个参数。使用较小的卷积核可以减少模型的参数数量,从而减少过拟合的风险并提高计算效率。

  3. 组合能力:多个连续的 3×33 \times 3 卷积层可以模拟较大的感受野。例如,两个连续的 3×33 \times 3 卷积层的感受野与一个 5×55 \times 5 的卷积层相同,但使用了更少的参数。

  4. 非线性:使用多个较小的卷积层(每个层后都有一个激活函数)可以增加网络的非线性,使其能够学习更复杂的特征。

在代码 conv = nn.Conv2d(1, 1, 3, padding=1) 中:

  • 第一个参数 1 表示输入通道数。
  • 第二个参数 1 表示输出通道数。
  • 第三个参数 3 表示卷积核的大小,即 3×33 \times 3
  • padding=1 确保输出的空间尺寸与输入相同。

尽管 3×33 \times 3 的卷积核在许多应用中都很受欢迎,但选择卷积核的大小应根据具体任务和数据来确定。在某些情况下,可能会选择其他大小的卷积核,或者使用多种大小的卷积核组合。