HBU_神经网络与深度学习 实验8 卷积神经网络:卷积和卷积神经网络的基础算子


写在前面的一些内容

  1. 本文为HBU_神经网络与深度学习实验(2022年秋)实验7的实验报告,此文的基本内容参照 [1]Github/卷积神经网络-上.ipynb卷积卷积神经网络的基础算子两小节,检索时请按对应序号进行检索。
  2. 本实验编程语言为Python 3.10,使用Pycharm进行编程。
  3. 本实验报告目录标题级别顺序:一、 1. (1)
  4. 水平有限,难免有误,如有错漏之处敬请指正。

一、卷积

考虑到使用全连接前馈网络来处理图像时,会出现如下问题:

  1. 模型参数过多,容易发生过拟合。 在全连接前馈网络中,隐藏层的每个神经元都要跟该层所有输入的神经元相连接。随着隐藏层神经元数量的增多,参数的规模也会急剧增加,导致整个神经网络的训练效率非常低,也很容易发生过拟合。

  2. 难以提取图像中的局部不变性特征。 自然图像中的物体都具有局部不变性特征,比如尺度缩放、平移、旋转等操作不影响其语义信息。而全连接前馈网络很难提取这些局部不变性特征。

卷积神经网络有三个结构上的特性:局部连接、权重共享和汇聚。这些特性使得卷积神经网络具有一定程度上的平移、缩放和旋转不变性。和前馈神经网络相比,卷积神经网络的参数也更少。因此,通常会使用卷积神经网络来处理图像信息。

卷积是分析数学中的一种重要运算,常用于信号处理或图像处理任务。本节以二维卷积为例来进行实践。


1. 二维卷积运算

在机器学习和图像处理领域,卷积的主要功能是在一个图像(或特征图)上滑动一个卷积核,通过卷积操作得到一组新的特征。在计算卷积的过程中,需要进行卷积核的翻转,而这也会带来一些不必要的操作和开销。因此,在具体实现上,一般会以数学中的互相关(Cross-Correlatio)运算来代替卷积。 在神经网络中,卷积运算的主要作用是抽取特征,卷积核是否进行翻转并不会影响其特征抽取的能力。特别是当卷积核是可学习的参数时,卷积和互相关在能力上是等价的。因此,很多时候,为方便起见,会直接用互相关来代替卷积。

对于一个输入矩阵 X ∈ R M × N \mathbf X\in\Bbb{R}^{M\times N} XRM×N和一个滤波器 W ∈ R U × V \mathbf W \in\Bbb{R}^{U\times V} WRU×V,它们的卷积为
y i , j = ∑ u = 0 U − 1 ∑ v = 0 V − 1 w u v x i + u , j + v \begin{align} y_{i,j}=\sum_{u=0}^{U-1} \sum_{v=0}^{V-1} w_{uv}x_{i+u,j+v} \end{align} yi,j=u=0U1v=0V1wuvxi+u,j+v
图2 给出了卷积计算的示例。

图2 卷积操作的计算过程

经过卷积运算后,最终输出矩阵大小则为
M ′ = M − U + 1 N ′ = N − V + 1 \begin{align} M' = M - U + 1 \\ N' = N - V + 1 \end{align} M=MU+1N=NV+1可以发现,使用卷积处理图像,会有以下两个特性:

  1. 在卷积层(假设是第 l l l层)中的每一个神经元都只和前一层(第 l − 1 l-1 l1层)中某个局部窗口内的神经元相连,构成一个局部连接网络,这也就是卷积神经网络的局部连接特性。
  2. 由于卷积的主要功能是在一个图像(或特征图)上滑动一个卷积核,所以作为参数的卷积核 W ∈ R U × V \mathbf W \in\Bbb{R}^{U\times V} WRU×V对于第 l l l层的所有的神经元都是相同的,这也就是卷积神经网络的权重共享特性。

2. 二维卷积算子

根据公式(1),我们首先实现一个简单的二维卷积算子,代码实现如下:

import torch
import torch.nn as nn


class Conv2D(nn.Module):
    def __init__(self, kernel_size, weight_attr=torch.tensor([[0., 1.], [2., 3.]])):
        super(Conv2D, self).__init__()
        # 使用'torch.nn.Parameter'创建卷积核
        self.weight = nn.Parameter(weight_attr)

    def forward(self, X):
        """
        输入:
            - X:输入矩阵,shape=[B, M, N],B为样本数量
        输出:
            - output:输出矩阵
        """
        u, v = self.weight.shape
        output = torch.zeros([X.shape[0], X.shape[1] - u + 1, X.shape[2] - v + 1])
        for i in range(output.shape[1]):
            for j in range(output.shape[2]):
                output[:, i, j] = torch.sum(X[:, i:i + u, j:j + v] * self.weight, dim=[1, 2])
        return output

# 随机构造一个二维输入矩阵
torch.random.manual_seed(100)
inputs = torch.tensor([[[1., 2., 3.], [4., 5., 6.], [7., 8., 9.]]])

conv2d = Conv2D(kernel_size=2)
outputs = conv2d(inputs)
print("input: {}, \noutput: {}".format(inputs, outputs))

代码执行结果:

input: tensor([[[1., 2., 3.],
         [4., 5., 6.],
         [7., 8., 9.]]]), 
output: tensor([[[25., 31.],
         [43., 49.]]], grad_fn=<CopySlices>)

3. 二维卷积的参数量和计算量

参数量

由于二维卷积的运算方式为在一个图像(或特征图)上滑动一个卷积核,通过卷积操作得到一组新的特征。所以参数量仅仅与卷积核的尺寸有关,对于一个输入矩阵 X ∈ R M × N \mathbf X\in\Bbb{R}^{M\times N} XRM×N和一个滤波器 W ∈ R U × V \mathbf W \in\Bbb{R}^{U\times V} WRU×V,卷积核的参数量为 U × V U\times V U×V

假设有一幅大小为 32 × 32 32\times 32 32×32的图像,如果使用全连接前馈网络进行处理,即便第一个隐藏层神经元个数为1,此时该层的参数量也高达 1025 1025 1025个,此时该层的计算过程如 图3 所示。

图3 使用全连接前馈网络处理图像数据的计算过程

可以想像,随着隐藏层神经元数量的变多以及层数的加深,使用全连接前馈网络处理图像数据时,参数量会急剧增加。

如果使用卷积进行图像处理,当卷积核为 3 × 3 3\times 3 3×3时,参数量仅为 9 9 9,相较于全连接前馈网络,参数量少了非常多。

计算量

在卷积神经网络中运算时,通常会统计网络总的乘加运算次数作为计算量(FLOPs,floating point of operations),来衡量整个网络的运算速度。对于单个二维卷积,计算量的统计方式为:
F L O P s = M ′ × N ′ × U × V \begin{align} FLOPs=M'\times N'\times U\times V \end{align} FLOPs=M×N×U×V其中 M ′ × N ′ M'\times N' M×N表示输出特征图的尺寸,即输出特征图上每个点都要与卷积核 W ∈ R U × V \mathbf W \in\Bbb{R}^{U\times V} WRU×V进行 U × V U\times V U×V次乘加运算。对于一幅大小为 32 × 32 32\times 32 32×32的图像,使用 3 × 3 3\times 3 3×3的卷积核进行运算可以得到以下的输出特征图尺寸:
M ′ = M − U + 1 = 30 N ′ = N − V + 1 = 30 M' = M - U + 1 = 30 \\ N' = N - V + 1 = 30 M=MU+1=30N=NV+1=30此时,计算量为:
F L O P s = M ′ × N ′ × U × V = 30 × 30 × 3 × 3 = 8100 FLOPs=M'\times N'\times U\times V=30\times 30\times 3\times 3=8100 FLOPs=M×N×U×V=30×30×3×3=8100


4. 感受野

输出特征图上每个点的数值,是由输入图片上大小为 U × V U\times V U×V的区域的元素与卷积核每个元素相乘再相加得到的,所以输入图像上 U × V U\times V U×V区域内每个元素数值的改变,都会影响输出点的像素值。我们将这个区域叫做输出特征图上对应点的感受野。感受野内每个元素数值的变动,都会影响输出点的数值变化。比如 3 × 3 3\times3 3×3卷积对应的感受野大小就是 3 × 3 3\times3 3×3,如 图4 所示。

图4 感受野为3×3的卷积

而当通过两层 3 × 3 3\times3 3×3的卷积之后,感受野的大小将会增加到 5 × 5 5\times5 5×5,如 图5 所示。

图5 感受野为5×5的卷积

因此,当增加卷积网络深度的同时,感受野将会增大,输出特征图中的一个像素点将会包含更多的图像语义信息。


5. 卷积的变种

在卷积的标准定义基础上,还可以引入卷积核的滑动步长和零填充来增加卷积的多样性,从而更灵活地进行特征抽取。

(1)步长(Stride)

在卷积运算的过程中,有时会希望跳过一些位置来降低计算的开销,也可以把这一过程看作是对标准卷积运算输出的下采样

在计算卷积时,可以在所有维度上每间隔 S S S个元素计算一次, S S S称为卷积运算的步长(Stride),也就是卷积核在滑动时的间隔。

此时,对于一个输入矩阵 X ∈ R M × N \mathbf X\in\Bbb{R}^{M\times N} XRM×N和一个滤波器 W ∈ R U × V \mathbf W \in\Bbb{R}^{U\times V} WRU×V,它们的卷积为
y i , j = ∑ u = 0 U − 1 ∑ v = 0 V − 1 w u v x i × S + u , j × S + v \begin{align} y_{i,j}=\sum_{u=0}^{U-1} \sum_{v=0}^{V-1} w_{uv}x_{i\times S+u,j\times S+v} \end{align} yi,j=u=0U1v=0V1wuvxi×S+u,j×S+v在二维卷积运算中,当步长 S = 2 S=2 S=2时,计算过程如 图6 所示。

图6 步长为2的二维卷积计算过程

(2)零填充(Zero Padding)

在卷积运算中,还可以对输入用零进行填充使得其尺寸变大。根据卷积的定义,如果不进行填充,当卷积核尺寸大于1时,输出特征会缩减。对输入进行零填充则可以对卷积核的宽度和输出的大小进行独立的控制。

在二维卷积运算中,零填充(Zero Padding)是指在输入矩阵周围对称地补上 P P P 0 0 0图7 为使用零填充的示例。

图7 padding=1的零填充

对于一个输入矩阵 X ∈ R M × N \mathbf X\in\Bbb{R}^{M\times N} XRM×N和一个滤波器 W ∈ R U × V \mathbf W \in\Bbb{R}^{U\times V} WRU×V,,步长为 S S S,对输入矩阵进行零填充,那么最终输出矩阵大小则为
M ′ = M + 2 P − U S + 1 N ′ = N + 2 P − V S + 1 \begin{align} M' = \frac{M + 2P - U}{S} + 1 \\ N' = \frac{N + 2P - V}{S} + 1 \end{align} M=SM+2PU+1N=SN+2PV+1引入步长和零填充后的卷积,参数量和计算量的统计方式与之前一致,参数量与卷积核的尺寸有关,为: U × V U\times V U×V,计算量与输出特征图和卷积核的尺寸有关,为:
F L O P s = M ′ × N ′ × U × V = ( M + 2 P − U S + 1 ) × ( N + 2 P − V S + 1 ) × U × V \begin{align} FLOPs=M'\times N'\times U\times V=(\frac{M + 2P - U}{S} + 1)\times (\frac{N + 2P - V}{S} + 1)\times U\times V \end{align} FLOPs=M×N×U×V=(SM+2PU+1)×(SN+2PV+1)×U×V一般常用的卷积有以下三类:

  1. 窄卷积:步长 S = 1 S=1 S=1,两端不补零 P = 0 P=0 P=0,卷积后输出尺寸为:
    M ′ = M − U + 1 N ′ = N − V + 1 \begin{align} M' = M - U + 1 \\ N' = N - V + 1 \end{align} M=MU+1N=NV+1
  2. 宽卷积:步长 S = 1 S=1 S=1,两端补零 P = U − 1 = V − 1 P=U-1=V-1 P=U1=V1,卷积后输出尺寸为:
    M ′ = M + U − 1 N ′ = N + V − 1 \begin{align} M' = M + U - 1 \\ N' = N + V - 1 \end{align} M=M+U1N=N+V1
  3. 等宽卷积:步长 S = 1 S=1 S=1,两端补零 P = ( U − 1 ) 2 = ( V − 1 ) 2 P=\frac{(U-1)}{2}=\frac{(V-1)}{2} P=2(U1)=2(V1),卷积后输出尺寸为:
    M ′ = M N ′ = N \begin{align} M' = M \\ N' = N \end{align} M=MN=N

通常情况下,在层数较深的卷积神经网络,比如:VGG、ResNet中,会使用等宽卷积保证输出特征图的大小不会随着层数的变深而快速缩减。例如:当卷积核的大小为 3 × 3 3\times 3 3×3时,会将步长设置为 S = 1 S=1 S=1,两端补零 P = 1 P=1 P=1,此时,卷积后的输出尺寸就可以保持不变。在本章后续的案例中,会使用ResNet进行实验。


6. 带步长和零填充的二维卷积算子

引入步长和零填充后,二维卷积算子代码实现如下:

class Conv2D(nn.Module):
    def __init__(self, kernel_size, stride=1, padding=0, weight_attr=1.0):
        super(Conv2D, self).__init__()
        weight_attr = torch.ones(size=(kernel_size, kernel_size))
        self.weight = nn.Parameter(weight_attr)
        # 步长
        self.stride = stride
        # 零填充
        self.padding = padding

    def forward(self, X):
        # 零填充
        new_X = torch.zeros([X.shape[0], X.shape[1] + 2 * self.padding, X.shape[2] + 2 * self.padding])
        new_X[:, self.padding:X.shape[1] + self.padding, self.padding:X.shape[2] + self.padding] = X
        u, v = self.weight.shape
        output_w = (new_X.shape[1] - u) // self.stride + 1
        output_h = (new_X.shape[2] - v) // self.stride + 1
        output = torch.zeros([X.shape[0], output_w, output_h])
        for i in range(0, output.shape[1]):
            for j in range(0, output.shape[2]):
                output[:, i, j] = torch.sum(
                    new_X[:, self.stride * i:self.stride * i + u, self.stride * j:self.stride * j + v] * self.weight,
                    dim=[1, 2])
        return output

inputs = torch.randn(size=[2, 8, 8])
conv2d_padding = Conv2D(kernel_size=3, padding=1)
outputs = conv2d_padding(inputs)
print(
    "When kernel_size=3, padding=1 stride=1, input's shape: {}, output's shape: {}".format(inputs.shape, outputs.shape))
conv2d_stride = Conv2D(kernel_size=3, stride=2, padding=1)
outputs = conv2d_stride(inputs)
print(
    "When kernel_size=3, padding=1 stride=2, input's shape: {}, output's shape: {}".format(inputs.shape, outputs.shape))

代码执行结果:

When kernel_size=3, padding=1 stride=1, input's shape: torch.Size([2, 8, 8]), output's shape: torch.Size([2, 8, 8])
When kernel_size=3, padding=1 stride=2, input's shape: torch.Size([2, 8, 8]), output's shape: torch.Size([2, 4, 4])

从输出结果看出,使用 3 × 3 3\times3 3×3大小卷积,padding为1,当stride=1时,模型的输出特征图可以与输入特征图保持一致;当stride=2时,输出特征图的宽和高都缩小一倍。


7. 使用卷积运算完成图像边缘检测任务

在图像处理任务中,常用拉普拉斯算子对物体边缘进行提取,拉普拉斯算子为一个大小为 3 × 3 3 \times 3 3×3的卷积核,中心元素值是 8 8 8,其余元素值是 − 1 -1 1

考虑到边缘其实就是图像上像素值变化很大的点的集合,因此可以通过计算二阶微分得到,当二阶微分为0时,像素值的变化最大。此时,对 x x x方向和 y y y方向分别求取二阶导数:
δ 2 I δ x 2 = I ( i , j + 1 ) − 2 I ( i , j ) + I ( i , j − 1 ) δ 2 I δ y 2 = I ( i + 1 , j ) − 2 I ( i , j ) + I ( i − 1 , j ) \begin{align} \frac{\delta^2 I}{\delta x^2} = I(i, j+1) - 2I(i,j) + I(i,j-1) \\ \frac{\delta^2 I}{\delta y^2} = I(i+1, j) - 2I(i,j) + I(i-1,j) \end{align} δx2δ2I=I(i,j+1)2I(i,j)+I(i,j1)δy2δ2I=I(i+1,j)2I(i,j)+I(i1,j)完整的二阶微分公式为:
∇ 2 I = δ 2 I δ x 2 + δ 2 I δ y 2 = − 4 I ( i , j ) + I ( i , j − 1 ) + I ( i , j + 1 ) + I ( i + 1 , j ) + I ( i − 1 , j ) \begin{align} \nabla^2I = \frac{\delta^2 I}{\delta x^2} + \frac{\delta^2 I}{\delta y^2} = - 4I(i,j) + I(i,j-1) + I(i, j+1) + I(i+1, j) + I(i-1,j) \end{align} 2I=δx2δ2I+δy2δ2I=4I(i,j)+I(i,j1)+I(i,j+1)+I(i+1,j)+I(i1,j)上述公式也被称为拉普拉斯算子,对应的二阶微分卷积核为:
[ 0 1 0 1 − 4 1 0 1 0 ] \begin{bmatrix} 0 &1 & 0 \\ 1 &-4 & 1 \\ 0 & 1 & 0 \\ \end{bmatrix} 010141010 对上述算子全部求反也可以起到相同的作用,此时,该算子可以表示为:
[ 0 − 1 0 − 1 4 − 1 0 − 1 0 ] \begin{bmatrix} 0 &-1 & 0 \\ -1 &4 & -1 \\ 0 & -1 & 0 \\ \end{bmatrix} 010141010 也就是一个点的四邻域拉普拉斯的算子计算结果是自己像素值的四倍减去上下左右的像素的和,将这个算子旋转 45 ° 45° 45°后与原算子相加,就变成八邻域的拉普拉斯算子,也就是一个像素自己值的八倍减去周围一圈八个像素值的和,做为拉普拉斯计算结果,此时,该算子可以表示为:
[ − 1 − 1 − 1 − 1 8 − 1 − 1 − 1 − 1 ] \begin{bmatrix} -1 &-1 & -1 \\ -1 &8 & -1 \\ -1 & -1 & -1 \\ \end{bmatrix} 111181111

下面我们利用上面定义的Conv2D算子,构造一个简单的拉普拉斯算子,并对一张输入的灰度图片进行边缘检测,提取出目标的外形轮廓。

输入的灰度图片如下:

import matplotlib.pyplot as plt
from PIL import Image
import numpy as np

# 读取图片
img = Image.open('./number.bmp').resize((256,256))

# 设置卷积核参数
w = np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], dtype='float32')
# 创建卷积算子,卷积核大小为3x3,并使用上面的设置好的数值作为卷积核权重的初始化参数
conv = Conv2D(kernel_size=3, stride=1, padding=0,
              weight_attr=torch.tensor(w))

# 将读入的图片转化为float32类型的numpy.ndarray
inputs = np.array(img).astype('float32')
print("bf to_tensor, inputs:", inputs)
# 将图片转为Tensor
inputs = torch.tensor(inputs)
print("bf unsqueeze, inputs:", inputs)
inputs = torch.unsqueeze(inputs, dim=0)
print("af unsqueeze, inputs:", inputs)
outputs = conv(inputs)

# 可视化结果
plt.figure(figsize=(8, 4))
f = plt.subplot(121)
f.set_title('input image', fontsize=15)
plt.imshow(img)
f = plt.subplot(122)
f.set_title('output feature map', fontsize=15)
plt.imshow(outputs.detach().numpy().squeeze(), cmap='gray')
plt.savefig('conv-vis.pdf')
plt.show()

代码执行结果:

bf to_tensor, inputs: [[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. 0. ... 0. 0. 0.]]
bf unsqueeze, inputs: tensor([[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., 0.,  ..., 0., 0., 0.]])
af unsqueeze, inputs: tensor([[[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., 0.,  ..., 0., 0., 0.]]])

执行代码后得到下图:

换一个resize大小:(112,112)

从输出结果看,使用拉普拉斯算子,目标的边缘可以成功被检测出来。


二、卷积神经网络的基础算子

卷积神经网络的基础算子


三、实验Q&A

该部分的内容参考了以下文章:
边缘检测系列1:传统边缘检测算子


部分传统边缘检测算子的实现

实现一些传统边缘检测算子,如:Roberts、Prewitt、Sobel、Scharr、Kirsch、Robinson、Laplacian。

引入

  • 图像的边缘指的是灰度值发生急剧变化的位置。
  • 在图像形成过程中,由于亮度、纹理、颜色、阴影等物理因素的不同而导致图像灰度值发生突变,从而形成边缘。
  • 边缘是通过检查每个像素的邻域并对其灰度变化进行量化的,这种灰度变化的量化相当于微积分里连续函数中方向导数或者离散数列的差分。

算法原理

  • 传统的边缘检测大多数是通过基于方向导数掩码(梯度方向导数)求卷积的方法。
  • 计算灰度变化的卷积算子包含Roberts算子、Prewitt算子、Sobel算子、Scharr算子、Kirsch算子、Robinson算子、Laplacian算子。
  • 大多数边缘检测算子是基于方向差分卷积核求卷积的方法,在使用由两个或者多个卷积核组成的边缘检测算子时假设有 n 个卷积核,记 C o n v 1 , C o n v 2 , . . . , C o n v n Conv_1,Conv_2,...,Conv_n Conv1,Conv2,...,Convn​,为图像分别与个卷积核做卷积的结果,通常有四种方式来衡量最后输出的边缘强度。
  1. 取对应位置绝对值的和: ∑ i = 1 n ∣ c o n v i ∣ \sum_{i=1}^{n} |\mathbf{conv}_i| i=1nconvi
  2. 取对应位置平方和的开方: ∑ i = 1 n c o n v i 2 \sqrt{\sum_{i=1}^{n} \mathbf{conv}_i^2} i=1nconvi2
  3. 取对应位置绝对值的最大值: max ⁡ { ∣ c o n v 1 ∣ , ∣ c o n v 2 ∣ , . . . , ∣ c o n v i ∣ } \max{\{|\mathbf{conv}_1|, |\mathbf{conv}_2|, ..., |\mathbf{conv}_i|\}} max{conv1,conv2,...,convi}
  4. 插值法: ∑ i = 1 n a i ∣ c o n v i ∣ \sum_{i=1}^n a_i |\mathbf{conv}_i| i=1naiconvi,其中 a i > = 0 a_i >= 0 ai>=0,且 ∑ i = 1 n a i = 1 \sum_{i=1}^n a_i = 1 i=1nai=1

代码实现

构建通用的边缘检测算子

因为上述的这些算子在本质上都是通过卷积计算实现的,只是所使用到的卷积核参数有所不同,所以可以构建一个通用的计算算子,只需要传入对应的卷积核参数即可实现不同的边缘检测,并且在后处理时集成了上述的四种计算最终边缘强度的方式。

import torch
import torch.nn as nn

class EdgeOP(nn.Module):
    def __init__(self, kernel):
        '''
        kernel: shape(out_channels, in_channels, h, w)
        '''
        super(EdgeOP, self).__init__()
        out_channels, in_channels, h, w = kernel.shape
        self.filter = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=(h, w), padding='same')
        self.filter.weight.data = torch.tensor(kernel, dtype=torch.float32)

    @staticmethod
    def postprocess(outputs, mode=0, weight=None):
        '''
        Input: NCHW
        Output: NHW(mode==1-3) or NCHW(mode==4)

        Params:
            mode: switch output mode(0-4)
            weight: weight when mode==3
        '''
        if mode == 0:
            results = torch.sum(torch.abs(outputs), dim=1)
        elif mode == 1:
            results = torch.sqrt(torch.sum(torch.pow(outputs, 2), dim=1))
        elif mode == 2:
            results = torch.max(torch.abs(outputs), dim=1)[0]
        elif mode == 3:
            if weight is None:
                C = outputs.shape[1]
                weight = torch.tensor([1 / C] * C, dtype=torch.float32)
            else:
                weight = torch.tensor(weight, dtype=torch.float32)
            results = torch.einsum('nchw, c -> nhw', torch.abs(outputs), weight)
        elif mode == 4:
            results = torch.abs(outputs)
        return torch.clip(results, 0, 255).to(torch.uint8)

    @torch.no_grad()
    def forward(self, images, mode=0, weight=None):
        outputs = self.filter(images)
        return self.postprocess(outputs, mode, weight)

图像边缘检测测试函数

为了方便测试就构建了如下的测试函数,测试同一张图片不同算子/不同边缘强度计算方法的边缘检测效果。

注1:cv2包由于导入问题无法使用,此处使用skimage包。
注2:此处使用图像和卷积一节中使用的图像相同。

import numpy as np
import skimage
  
def test_edge_det(kernel, img_path='7_262.bmp'):
    img = skimage.io.imread(img_path)
    img_tensor = torch.tensor(img, dtype=torch.float32)[None, None, ...]
    op = EdgeOP(kernel)
    all_results = []
    for mode in range(4):
        results = op(img_tensor, mode=mode)
        all_results.append(results.numpy()[0])

    results = op(img_tensor, mode=4)
    for result in results.numpy()[0]:
        all_results.append(result)
    return all_results, np.concatenate(all_results, 1)

Roberts算子

from PIL import Image

# Roberts 算子
roberts_kernel = np.array([
    [[
        [1, 0],
        [0, -1]
    ]],
    [[
        [0, -1],
        [1, 0]
    ]]
])

_, concat_res = test_edge_det(roberts_kernel)
Image.fromarray(concat_res).show()

代码执行结果如下图所示:

Prewitt算子

# Prewitt 算子
prewitt_kernel = np.array([
    [[
        [-1, -1, -1],
        [0, 0, 0],
        [1, 1, 1]
    ]],
    [[
        [-1, 0, 1],
        [-1, 0, 1],
        [-1, 0, 1]
    ]],
    [[
        [0, 1, 1],
        [-1, 0, 1],
        [-1, -1, 0]
    ]],
    [[
        [-1, -1, 0],
        [-1, 0, 1],
        [0, 1, 1]
    ]]
])

_, concat_res = test_edge_det(prewitt_kernel)
Image.fromarray(concat_res).show()

代码执行结果如下图所示:

Sobel算子

# Sobel 算子
sobel_kernel = np.array([
    [[
        [-1, -2, -1],
        [0, 0, 0],
        [1, 2, 1]
    ]],
    [[
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1]
    ]],
    [[
        [0, 1, 2],
        [-1, 0, 1],
        [-2, -1, 0]
    ]],
    [[
        [-2, -1, 0],
        [-1, 0, 1],
        [0, 1, 2]
    ]]
])

_, concat_res = test_edge_det(sobel_kernel)
Image.fromarray(concat_res).show()

代码执行结果如下图所示:

Scharr算子

# Scharr 算子
scharr_kernel = np.array([
    [[
        [-3, -10, -3],
        [0, 0, 0],
        [3, 10, 3]
    ]],
    [[
        [-3, 0, 3],
        [-10, 0, 10],
        [-3, 0, 3]
    ]],
    [[
        [0, 3, 10],
        [-3, 0, 3],
        [-10, -3, 0]
    ]],
    [[
        [-10, -3, 0],
        [-3, 0, 3],
        [0, 3, 10]
    ]]
])

_, concat_res = test_edge_det(scharr_kernel)
Image.fromarray(concat_res).show()

代码执行结果如下图所示:

Krisch算子

# Krisch 算子
Krisch_kernel = np.array([
    [[
        [5, 5, 5],
        [-3, 0, -3],
        [-3, -3, -3]
    ]],
    [[
        [-3, 5, 5],
        [-3, 0, 5],
        [-3, -3, -3]
    ]],
    [[
        [-3, -3, 5],
        [-3, 0, 5],
        [-3, -3, 5]
    ]],
    [[
        [-3, -3, -3],
        [-3, 0, 5],
        [-3, 5, 5]
    ]],
    [[
        [-3, -3, -3],
        [-3, 0, -3],
        [5, 5, 5]
    ]],
    [[
        [-3, -3, -3],
        [5, 0, -3],
        [5, 5, -3]
    ]],
    [[
        [5, -3, -3],
        [5, 0, -3],
        [5, -3, -3]
    ]],
    [[
        [5, 5, -3],
        [5, 0, -3],
        [-3, -3, -3]
    ]],
])

_, concat_res = test_edge_det(Krisch_kernel)
Image.fromarray(concat_res).show()

代码执行结果如下图所示:

Robinson算子

# Robinson算子
robinson_kernel = np.array([
    [[
        [1, 2, 1],
        [0, 0, 0],
        [-1, -2, -1]
    ]],
    [[
        [0, 1, 2],
        [-1, 0, 1],
        [-2, -1, 0]
    ]],
    [[
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1]
    ]],
    [[
        [-2, -1, 0],
        [-1, 0, 1],
        [0, 1, 2]
    ]],
    [[
        [-1, -2, -1],
        [0, 0, 0],
        [1, 2, 1]
    ]],
    [[
        [0, -1, -2],
        [1, 0, -1],
        [2, 1, 0]
    ]],
    [[
        [1, 0, -1],
        [2, 0, -2],
        [1, 0, -1]
    ]],
    [[
        [2, 1, 0],
        [1, 0, -1],
        [0, -1, -2]
    ]],
])

_, concat_res = test_edge_det(robinson_kernel)
Image.fromarray(concat_res).show()

代码执行结果如下图所示:

Laplacian算子

# Laplacian 算子
laplacian_kernel = np.array([
    [[
        [1, 1, 1],
        [1, -8, 1],
        [1, 1, 1]
    ]],
    [[
        [0, 1, 0],
        [1, -4, 1],
        [0, 1, 0]
    ]]
])

_, concat_res = test_edge_det(laplacian_kernel)
Image.fromarray(concat_res).show()

代码执行结果如下图所示:


三、实验总结

本次实验内容是做特征提取和边缘检测。对于提出的栈溢出问题,将output结果放在plt.imshow中处理似乎可以解决问题(来自同学的建议)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值