目录
5.1卷积
5.1.1二维卷积运算
5.1.2二维卷积算子
5.1.3二维卷积的参数量和计算量
5.1.4感受野
5.1.5 卷积的变种
5.1.5.1 步长(Stride)
5.1.5.2 零填充(Zero Padding)
5.1.6带步长和零填充的二维卷积算子
5.1.7使用卷积运算完成图像边缘检测任务
选做题
边缘检测系列1:传统边缘检测算子 - 飞桨AI Studio
1.1构建通用的边缘检测算子
1.2图像边缘检测测试函数
1.3 Roberts 算子
1.4 Prewitt 算子
1.5 Sobel 算子
1.6 Scharr 算子
1.7 Krisch 算子
1.8 Robinson算子
1.9 Laplacian 算子
边缘检测系列2:简易的 Canny 边缘检测器
边缘检测系列3:【HED】 Holistically-Nested 边缘检测
边缘检测系列4:【RCF】基于更丰富的卷积特征的边缘检测
边缘检测系列5:【CED】添加了反向细化路径的 HED 模型
总结
参考
卷积神经网络(Convolutional Neural Network,CNN)
考虑到使用全连接前馈网络来处理图像时,会出现如下问题:
模型参数过多,容易发生过拟合。 在全连接前馈网络中,隐藏层的每个神经元都要跟该层所有输入的神经元相连接。随着隐藏层神经元数量的增多,参数的规模也会急剧增加,导致整个神经网络的训练效率非常低,也很容易发生过拟合。
难以提取图像中的局部不变性特征。 自然图像中的物体都具有局部不变性特征,比如尺度缩放、平移、旋转等操作不影响其语义信息。而全连接前馈网络很难提取这些局部不变性特征。
卷积神经网络有三个结构上的特性:局部连接、权重共享和汇聚。这些特性使得卷积神经网络具有一定程度上的平移、缩放和旋转不变性。和前馈神经网络相比,卷积神经网络的参数也更少。因此,通常会使用卷积神经网络来处理图像信息。
卷积是分析数学中的一种重要运算,常用于信号处理或图像处理任务。
在机器学习和图像处理领域,卷积的主要功能是在一个图像(或特征图)上滑动一个卷积核,通过卷积操作得到一组新的特征。在计算卷积的过程中,需要进行卷积核的翻转,而这也会带来一些不必要的操作和开销。因此,在具体实现上,一般会以数学中的互相关(Cross-Correlatio)运算来代替卷积。
在神经网络中,卷积运算的主要作用是抽取特征,卷积核是否进行翻转并不会影响其特征抽取的能力。特别是当卷积核是可学习的参数时,卷积和互相关在能力上是等价的。因此,很多时候,为方便起见,会直接用互相关来代替卷积。
对于一个输入矩阵和一个滤波器,它们的卷积为
矩阵的下标从0开始
经过卷积运算后,最终输出矩阵大小则为
可以发现,使用卷积处理图像,会有以下两个特性:
根据公式(5.1),我们首先实现一个简单的二维卷积算子,代码实现如下:
import torch
import torch.nn as nn
import torch.nn
import numpy as np
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super(Conv2D, self).__init__()
w = torch.tensor(np.array([[0., 1.], [2., 3.]], dtype='float32').reshape([kernel_size, kernel_size]))
self.weight = torch.nn.Parameter(w, requires_grad=True)
def forward(self, X):
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, axis=[1, 2])
return output
# 随机构造一个二维输入矩阵
inputs = torch.as_tensor([[[1., 2., 3.], [4., 5., 6.], [7., 8., 9.]]])
conv2d = Conv2D(kernel_size=2)
outputs = conv2d(inputs)
print("input: {}, \noutput: {}".format(inputs, outputs))
运行结果:
参数量
由于二维卷积的运算方式为在一个图像(或特征图)上滑动一个卷积核,通过卷积操作得到一组新的特征。所以参数量仅仅与卷积核的尺寸有关,对于一个输入矩阵和一个滤波器,卷积核的参数量为U*V。
假设有一幅大小为32×3232×32的图像,如果使用全连接前馈网络进行处理,即便第一个隐藏层神经元个数为1,此时该层的参数量也高达1025个,此时该层的计算过程如 图5.3 所示。
可以想像,随着隐藏层神经元数量的变多以及层数的加深,使用全连接前馈网络处理图像数据时,参数量会急剧增加。
如果使用卷积进行图像处理,当卷积核为3×3时,参数量仅为9,相较于全连接前馈网络,参数量少了非常多。
计算量
在卷积神经网络中运算时,通常会统计网络总的乘加运算次数作为计算量(FLOPs,floating point of operations),来衡量整个网络的运算速度。对于单个二维卷积,计算量的统计方式为:
其中M′×N′表示输出特征图的尺寸,即输出特征图上每个点都要与卷积核进行U×V次乘加运算。对于一幅大小为32×32的图像,使用3×3的卷积核进行运算可以得到以下的输出特征图尺寸:
此时,计算量为:
输出特征图上每个点的数值,是由输入图片上大小为U×V的区域的元素与卷积核每个元素相乘再相加得到的,所以输入图像上U×V区域内每个元素数值的改变,都会影响输出点的像素值。我们将这个区域叫做输出特征图上对应点的感受野。感受野内每个元素数值的变动,都会影响输出点的数值变化。比如3×33×3卷积对应的感受野大小就是3×3,如 图5.4 所示。
而当通过两层3×3的卷积之后,感受野的大小将会增加到5×5,如 图5.5 所示。
因此,当增加卷积网络深度的同时,感受野将会增大,输出特征图中的一个像素点将会包含更多的图像语义信息。
在卷积的标准定义基础上,还可以引入卷积核的滑动步长和零填充来增加卷积的多样性,从而更灵活地进行特征抽取。
在卷积运算的过程中,有时会希望跳过一些位置来降低计算的开销,也可以把这一过程看作是对标准卷积运算输出的下采样。
在计算卷积时,可以在所有维度上每间隔SS个元素计算一次,SS称为卷积运算的步长(Stride),也就是卷积核在滑动时的间隔。
此时,对于一个输入矩阵和一个滤波器,它们的卷积为
在二维卷积运算中,当步长S=2时,计算过程如 图5.6 所示。
在卷积运算中,还可以对输入用零进行填充使得其尺寸变大。根据卷积的定义,如果不进行填充,当卷积核尺寸大于1时,输出特征会缩减。对输入进行零填充则可以对卷积核的宽度和输出的大小进行独立的控制。
在二维卷积运算中,零填充(Zero Padding)是指在输入矩阵周围对称地补上P个0。图5.7 为使用零填充的示例。
对于一个输入矩阵和一个滤波器,步长为SS,对输入矩阵进行零填充,那么最终输出矩阵大小则为
引入步长和零填充后的卷积,参数量和计算量的统计方式与之前一致,参数量与卷积核的尺寸有关,为:U×V,计算量与输出特征图和卷积核的尺寸有关,为:
一般常用的卷积有以下三类:
窄卷积:步长S=1,两端不补零P=0,卷积后输出尺寸为:
宽卷积:步长S=1S=1,两端补零P=U−1=V−1P=U−1=V−1,卷积后输出尺寸为:
等宽卷积:步长S=1S=1,两端补零P=(U−1)2=(V−1)2P=(U−1)2=(V−1)2,卷积后输出尺寸为:
通常情况下,在层数较深的卷积神经网络,比如:VGG、ResNet中,会使用等宽卷积保证输出特征图的大小不会随着层数的变深而快速缩减。例如:当卷积核的大小为3×3时,会将步长设置为S=1,两端补零P=1,此时,卷积后的输出尺寸就可以保持不变。在本章后续的案例中,会使用ResNet进行实验。
代码如下:
import torch
import torch.nn as nn
import torch.nn
import numpy as np
class Conv2D(nn.Module):
def __init__(self, kernel_size,stride=1, padding=0):
super(Conv2D, self).__init__()
w = torch.tensor(np.array([[0., 1., 2.], [3., 4. ,5.],[6.,7.,8.]], dtype='float32').reshape([kernel_size, kernel_size]))
self.weight = torch.nn.Parameter(w, requires_grad=True)
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,
axis=[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))
运行结果:
从输出结果看出,使用3×3大小卷积,padding为1,当stride=1时,模型的输出特征图可以与输入特征图保持一致;当stride=2时,输出特征图的宽和高都缩小一倍。
在图像处理任务中,常用拉普拉斯算子对物体边缘进行提取,拉普拉斯算子为一个大小为3×3的卷积核,中心元素值是8,其余元素值是−1。
考虑到边缘其实就是图像上像素值变化很大的点的集合,因此可以通过计算二阶微分得到,当二阶微分为0时,像素值的变化最大。此时,对x方向和y方向分别求取二阶导数:
下面我们利用上面定义的Conv2D算子,构造一个简单的拉普拉斯算子,并对一张输入的灰度图片进行边缘检测,提取出目标的外形轮廓。
代码如下:
Pytorch实现1、2;阅读3、4、5写体会。
边缘检测系列1:传统边缘检测算子 - 飞桨AI Studio
实现一些传统边缘检测算子,如:Roberts、Prewitt、Sobel、Scharr、Kirsch、Robinson、Laplacian
import numpy as np
import torch
import torch.nn as nn
import os
import cv2
from PIL import Image
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',
bias=False)
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).values
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)
为了方便测试就构建了如下的测试函数,测试同一张图片不同算子/不同边缘强度计算方法的边缘检测效果
import os
import cv2
from PIL import Image
def test_edge_det(kernel, img_path='img-3.jpg'):
img = cv2.imread(img_path, 0)
print(img)
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算法优点:局部差分算子寻找边缘,边缘定位精度较高,对陡峭边缘且含有噪声少的图像效果较好。
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_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_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_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()
总结:边缘检测,实质上还是通过模板来求目标灰度值。只是模板的区别,以及通过求多个模板对应的灰度值后的处理方式不同而已。
基于 OpenCV 实现快速的 Canny 边缘检测
import cv2
import numpy as np
from PIL import Image
lower = 30 # 最小阈值
upper = 70 # 最大阈值
img_path = 'img-3.jpg' # 指定测试图像路径
gray = cv2.imread(img_path, 0) # 读取灰度图像
edge = cv2.Canny(gray, lower, upper) # Canny 图像边缘检测
contrast = np.concatenate([edge, gray], 1) # 图像拼接
Image.fromarray(contrast).show()# 显示图像
边缘检测系列3:【HED】 Holistically-Nested 边缘检测 - 飞桨AI Studio
复现论文 Holistically-Nested Edge Detection,发表于 CVPR 2015
一个基于深度学习的端到端边缘检测模型。
模型结构
使用 VGG Block 提取层级特征图
使用层级特征图计算层级输出
层级输出上采样
最后融合五个层级输出作为模型的最终输出:
通道维度拼接五个层级的输出
1x1 卷积对层级输出进行融合
Holistically表示该算法试图训练一个image-to-image的网络;Nested则强调在生成的输出过程中通过不断的集成和学习得到更精确的边缘预测图的过程。它解决了两个重要的问题,1)整体图片的训练和预测,2)多尺度多层级的特征学习。HED能实现端到端的训练,输入一个图片,输出对应的边缘图片。
从上图中HED和传统Canny算法进行边缘检测的效果对比图我们可以看到HED的效果要明显优于Canny算子的。
边缘检测系列4:【RCF】基于更丰富的卷积特征的边缘检测 - 飞桨AI Studio
复现论文 Richer Convolutional Features for Edge Detection,CVPR 2017 发表
一个基于更丰富的卷积特征的边缘检测模型 【RCF】。
模型架构(RCF 与 HED 比较)
RCF将所有卷积特性封装成更具判别性的表示形式,这很好地利用了丰富的特性层次结构,并且可以通过反向传播进行训练。RCF充分利用了目标的多尺度、多层次信息,全面地进行图像对图像的预测。使用VGG16网络,可以在几个可用的数据集上实现了最先进的性能。
边缘检测系列5:【CED】添加了反向细化路径的 HED 模型 - 飞桨AI Studio
Crisp Edge Detection(CED)模型是前面介绍过的 HED 模型的另一种改进模型
模型架构
CED 模型利用自上而下的反向细化路径,并逐渐增加特征图的分辨率以生成清晰的边缘。
CED提出了细化的方案,即1)反向细化路径模块,以及2)采用亚像素卷积替代线性插值的全卷积,对于边缘定位效果更好。
本次实验的内容充实,主要围绕卷积网络方面的知识,在本次实验中动手实践了各种边缘检测的模型,接触到了很多边缘检测的卷积核,在学习中体会它们的不同并进行比较。对于这部分内容,要多花时间来理解,继续加油!
Deep Crisp Boundaries(CED)论文学习笔记_小风_的博客-CSDN博客
NNDL 实验六 卷积神经网络(1)卷积_HBU_David的博客-CSDN博客
NNDL 实验5(上) - HBU_DAVID - 博客园