文章目录
实现一些传统边缘检测算子,如:Roberts、Prewitt、Sobel、Scharr、Kirsch、Robinson、Laplacian
详细说一下canny(这个确实比较火,之前做项目用过sobel算子,但是后来发现还是canny较多)
复现论文 Holistically-Nested Edge Detection,发表于 CVPR 2015
复现论文 Richer Convolutional Features for Edge Detection,CVPR 2017 发表
Crisp Edge Detection(CED)模型是前面介绍过的 HED 模型的另一种改进模型
前言
这次,我还是写的很细,并且这两天事这真的多,这次,是喝了点酒,然后之前写了一点,本着今日事今日毕的原则,挣扎着写完了,越写越精神(哈哈哈)。
写的不好,希望老师和各位大佬多教教我。
第5章 卷积神经网络
卷积神经网络(Convolutional Neural Network,CNN)是受生物学上感受野机制的启发而提出的。目前的卷积神经网络一般是由卷积层、汇聚层和全连接层交叉堆叠而成的前馈神经网络,有三个结构上的特性:局部连接、权重共享以及汇聚。这些特性使得卷积神经网络具有一定程度上的平移、缩放和旋转不变性。和前馈神经网络相比,卷积神经网络的参数更少。卷积神经网络主要应用在图像和视频分析的任务上,其准确率一般也远远超出了其他的神经网络模型。近年来卷积神经网络也广泛地应用到自然语言处理、推荐系统等领域。
在学习本章内容前,建议您先阅读《神经网络与深度学习》第5章:卷积神经网络的相关内容,关键知识点如 图5.1 所示,以便更好的理解和掌握书中的理论知识在实践中的应用方法。
一、5.1 卷积
考虑到使用全连接前馈网络来处理图像时,会出现如下问题:
-
模型参数过多,容易发生过拟合。 在全连接前馈网络中,隐藏层的每个神经元都要跟该层所有输入的神经元相连接。随着隐藏层神经元数量的增多,参数的规模也会急剧增加,导致整个神经网络的训练效率非常低,也很容易发生过拟合。
-
难以提取图像中的局部不变性特征。 自然图像中的物体都具有局部不变性特征,比如尺度缩放、平移、旋转等操作不影响其语义信息。而全连接前馈网络很难提取这些局部不变性特征。
卷积神经网络有三个结构上的特性:局部连接、权重共享和汇聚。这些特性使得卷积神经网络具有一定程度上的平移、缩放和旋转不变性。和前馈神经网络相比,卷积神经网络的参数也更少。因此,通常会使用卷积神经网络来处理图像信息。
卷积是分析数学中的一种重要运算,常用于信号处理或图像处理任务。本节以二维卷积为例来进行实践。
5.1.1 二维卷积运算
在机器学习和图像处理领域,卷积的主要功能是在一个图像(或特征图)上滑动一个卷积核,通过卷积操作得到一组新的特征。在计算卷积的过程中,需要进行卷积核的翻转,而这也会带来一些不必要的操作和开销。因此,在具体实现上,一般会以数学中的互相关(Cross-Correlatio)运算来代替卷积。
在神经网络中,卷积运算的主要作用是抽取特征,卷积核是否进行翻转并不会影响其特征抽取的能力。特别是当卷积核是可学习的参数时,卷积和互相关在能力上是等价的。因此,很多时候,为方便起见,会直接用互相关来代替卷积。
下边是卷积的运算方式,后边是卷积后大小的计算方式(我把老师写的和老师上课讲的综合了一下)。
经过卷积运算后,最终输出矩阵大小则为
可以发现,使用卷积处理图像,会有以下两个特性:
- 在卷积层(假设是第ll层)中的每一个神经元都只和前一层(第l−1l−1层)中某个局部窗口内的神经元相连,构成一个局部连接网络,这也就是卷积神经网络的局部连接特性。
- 由于卷积的主要功能是在一个图像(或特征图)上滑动一个卷积核,所以作为参数的卷积核W∈RU×VW∈RU×V对于第ll层的所有的神经元都是相同的,这也就是卷积神经网络的权重共享特性。
5.1.2 二维卷积算子
在本书后面的实现中,算子都继承torch.nn.Moudle,并使用支持反向传播的飞桨API进行实现,这样我们就可以不用手工写backword()
的代码实现。
根据公式,,我们首先实现一个简单的二算子。
# coding=gbk
import torch
import torch.nn as nn
from torch.nn.init import constant_, normal_, uniform_
import matplotlib.pyplot as plt
from PIL import Image
import numpy as np
首先,这个是用到的库。这个必须要说一下的就是上边解码方式,就算是你强制定义了,我感觉大部分时候是没啥错的,但是这次再调open-cv库的时候还是报错了,用中文定义名字,open-cv识别不了,这个下边再说。
class Conv2D(nn.Module):
def __init__(self, kernel_size,
weight_attr=torch.tensor([[0., 1.],[2., 3.]])):
super(Conv2D, self).__init__()
# 使用'paddle.create_parameter'创建卷积核
# 使用'paddle.ParamAttr'进行参数初始化
self.weight = torch.nn.parameter.Parameter(weight_attr,requires_grad=True)
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.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>)
代码的大致思想:
这个就是自己写了一个卷积层函数,它的核心是互相关运算,互相关的思想,在我上一篇博客写的很清楚,其实就是按着公式遍历一遍,但是这个与单纯的互相关不一样的是,这个还有反向传播的东西。
需要注意的函数:
尤其需要注意paddle和torch的参数化之间的区别,真的很难弄,如果不理解是不容易改出来的。尤其是parameter与ParamAttr之间的区别。
5.1.3 二维卷积的参数量和计算量
这个是最直接的优势,就是因为它解决了参数爆炸的问题,才体现出了优越性,所以我要多说一点。
参数量
由于二维卷积的运算方式为在一个图像(或特征图)上滑动一个卷积核,通过卷积操作得到一组新的特征。所以参数量仅仅与卷积核的尺寸有关,对于一个输入矩阵X∈RM×NX和一个滤波器W∈RU×V,卷积核的参数量为U×V。
假设有一幅大小为32×32的图像,如果使用全连接前馈网络进行处理,即便第一个隐藏层神经元个数为1,此时该层的参数量也高达1025个,此时该层的计算过程如 图5.3 所示。
计算量
在卷积神经网络中运算时,通常会统计网络总的乘加运算次数作为计算量(FLOPs,floating point of operations),来衡量整个网络的运算速度。对于单个二维卷积,计算量的统计方式为:
其中M′ ×N′表示输出特征图的尺寸,即输出特征图上每个点都要与卷积核进行U×V次乘加运算。对于一幅大小为32×32的图像,使用3×3的卷积核进行运算可以得到以下的输出特征图尺寸:
此时,计算量为:
5.1.4 感受野
输出特征图上每个点的数值,是由输入图片上大小为U×V的区域的元素与卷积核每个元素相乘再相加得到的,所以输入图像上U×V区域内每个元素数值的改变,都会影响输出点的像素值。我们将这个区域叫做输出特征图上对应点的感受野。感受野内每个元素数值的变动,都会影响输出点的数值变化。比如3×3卷积对应的感受野大小就是3×3,而当通过两层3×33×3的卷积之后,感受野的大小将会增加到5×5,如 图5.5 所示。
5.1.5 卷积的变种
在卷积的标准定义基础上,还可以引入卷积核的滑动步长和零填充来增加卷积的多样性,从而更灵活地进行特征抽取。
5.1.5.1 步长(Stride)
在卷积运算的过程中,有时会希望跳过一些位置来降低计算的开销,也可以把这一过程看作是对标准卷积运算输出的下采样。
在计算卷积时,可以在所有维度上每间隔SS个元素计算一次,SS称为卷积运算的步长(Stride),也就是卷积核在滑动时的间隔。
5.1.5.2 零填充(Zero Padding)
在卷积运算中,还可以对输入用零进行填充使得其尺寸变大。根据卷积的定义,如果不进行填充,当卷积核尺寸大于1时,输出特征会缩减。对输入进行零填充则可以对卷积核的宽度和输出的大小进行独立的控制。
在二维卷积运算中,零填充(Zero Padding)是指在输入矩阵周围对称地补上PP个00。图5.7 为使用零填充的示例。
5.1.6 带步长和零填充的二维卷积算子
引入步长和零填充后,二维卷积算子代码实现如下:
class Conv2D(nn.Module):
def __init__(self, kernel_size, stride=1, padding=0):
weight_attr = torch.tensor([[-1,-1,-1], [-1,8,-1], [-1,-1,-1]],dtype=torch.float32)
super(Conv2D, self).__init__()
self.weight = torch.nn.parameter.Parameter(weight_attr,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,
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大小卷积,padding
为1,当stride
=1时,模型的输出特征图可以与输入特征图保持一致;当stride
=2时,输出特征图的宽和高都缩小一倍。
5.1.7 使用卷积运算完成图像边缘检测任务
在图像处理任务中,常用拉普拉斯算子对物体边缘进行提取,拉普拉斯算子为一个大小为3×3的卷积核,中心元素值是8,其余元素值是−1。
下面我们利用上面定义的Conv2D
算子,构造一个简单的拉普拉斯算子,并对一张输入的灰度图片进行边缘检测,提取出目标的外形轮廓。
# 读取图片
img = Image.open('number.jpg').convert('L').resize((100,100))
# 设置卷积核参数
w = np.array([[-1,-1,-1], [-1,8,-1], [-1,-1,-1]], dtype='float32')
# 创建卷积算子,卷积核大小为3x3,并使用上面的设置好的数值作为卷积核权重的初始化参数
conv = Conv2D(kernel_size=3, stride=1, padding=0)
# 将读入的图片转化为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)
print(inputs.shape)
outputs = conv(inputs)
outputs = outputs.detach().numpy()
# 可视化结果
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.squeeze(), cmap='gray')
plt.savefig('conv-vis.pdf')
plt.show()
运行结果为:
bf to_tensor, inputs: [[ 96. 95. 97. ... 81. 80. 82.]
[144. 132. 115. ... 80. 80. 81.]
[163. 198. 122. ... 81. 80. 81.]
...
[ 32. 36. 34. ... 104. 104. 101.]
[ 51. 52. 60. ... 79. 114. 117.]
[ 92. 105. 112. ... 127. 138. 166.]]
bf unsqueeze, inputs: tensor([[ 96., 95., 97., ..., 81., 80., 82.],
[144., 132., 115., ..., 80., 80., 81.],
[163., 198., 122., ..., 81., 80., 81.],
...,
[ 32., 36., 34., ..., 104., 104., 101.],
[ 51., 52., 60., ..., 79., 114., 117.],
[ 92., 105., 112., ..., 127., 138., 166.]])
af unsqueeze, inputs: tensor([[[ 96., 95., 97., ..., 81., 80., 82.],
[144., 132., 115., ..., 80., 80., 81.],
[163., 198., 122., ..., 81., 80., 81.],
...,
[ 32., 36., 34., ..., 104., 104., 101.],
[ 51., 52., 60., ..., 79., 114., 117.],
[ 92., 105., 112., ..., 127., 138., 166.]]])
torch.Size([1, 100, 100])
二、选做题
Pytorch实现1、2;阅读3、4、5写体会。
1.选做1
边缘检测系列1:传统边缘检测算子 - 飞桨AI Studio
实现一些传统边缘检测算子,如:Roberts、Prewitt、Sobel、Scharr、Kirsch、Robinson、Laplacian
构建通用的边缘检测算子
- 因为上述的这些算子在本质上都是通过卷积计算实现的,只是所使用到的卷积核参数有所不同
- 所以可以构建一个通用的计算算子,只需要传入对应的卷积核参数即可实现不同的边缘检测
- 并且在后处理时集成了上述的四种计算最终边缘强度的方式
# coding=gbk
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='cat.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_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_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_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_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_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_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_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()
运行结果为:
关于这个几个的对比,由于之前做项目用到过sobel算子,所以比较熟悉,但是当时由于没有查到canny,这个里发一下,我们当时的查到的对比的链接吧。(我们当时做的猫咪识别,所以这里也就用猫咪了)
这里说一下我们当时的统计结果(我把canny也加进去了)。
Sobel算子检测方法对灰度渐变和噪声较多的图像处理效果较好,sobel算子对边缘定位不是很准确,图像的边缘不止一个像素;当对精度要求不是很高时,是一种较为常用的边缘检测方法。
Canny方法不容易受噪声干扰,能够检测到真正的弱边缘。优点在于,使用两种不同的阈值分别检测强边缘和弱边缘,并且当弱边缘和强边缘相连时,才将弱边缘包含在输出图像中。
Laplacian算子法对噪声比较敏感,所以很少用该算子检测边缘,而是用来判断边缘像素视为与图像的明区还是暗区。拉普拉斯高斯算子是一种二阶导数算子,将在边缘处产生一个陡峭的零交叉, Laplacian算子是各向同性的,能对任何走向的界线和线条进行锐化,无方向性。这是拉普拉斯算子区别于其他算法的最大优点。
下边是一些具体的比较,我感觉大家都在发一样的东西,所以我就发一下当时的研究的结果吧,具体的比较看链接就行。
常见边缘检测对比(Roberts算子、Prewitt算子、Sobel算子、Laplacian算子、Canny算子)_闭关修炼——暂退的博客-CSDN博客_比deta算子更好的算子
2.选做2
边缘检测系列2:简易的 Canny 边缘检测器 - 飞桨AI Studio
实现的简易的 Canny 边缘检测算法
基于 OpenCV 实现快速的 Canny 边缘检测
- 在 OpenCV 中只需要使用 cv2.Canny 函数即可实现 Canny 边缘检测
# coding=gbk
import cv2
import numpy as np
from PIL import Image
lower = 30 # 最小阈值
upper = 70 # 最大阈值
img_path = 'cat.jpg' # 指定测试图像路径
gray = cv2.imread(img_path, 0) # 读取灰度图像
edge = cv2.Canny(gray, lower, upper) # Canny 图像边缘检测
contrast = np.concatenate([edge, gray], 1) # 图像拼接
Image.fromarray(contrast).show()# 显示图像
运行结果为:
基于 Numpy 模块实现简单的 Canny 检测器
1、导入必要的包
import cv2
import math
import numpy as np
2、高斯模糊
def smooth(img_gray, kernel_size=5):
# 生成高斯滤波器
"""
要生成一个 (2k+1)x(2k+1) 的高斯滤波器,滤波器的各个元素计算公式如下:
H[i, j] = (1/(2*pi*sigma**2))*exp(-1/2*sigma**2((i-k-1)**2 + (j-k-1)**2))
"""
sigma1 = sigma2 = 1.4
gau_sum = 0
gaussian = np.zeros([kernel_size, kernel_size])
for i in range(kernel_size):
for j in range(kernel_size):
gaussian[i, j] = math.exp(
(-1 / (2 * sigma1 * sigma2)) *
(np.square(i - 3) + np.square(j-3))
) / (2 * math.pi * sigma1 * sigma2)
gau_sum = gau_sum + gaussian[i, j]
# 归一化处理
gaussian = gaussian / gau_sum
# 高斯滤波
img_gray = np.pad(img_gray, ((kernel_size//2, kernel_size//2), (kernel_size//2, kernel_size//2)), mode='constant')
W, H = img_gray.shape
new_gray = np.zeros([W - kernel_size, H - kernel_size])
for i in range(W-kernel_size):
for j in range(H-kernel_size):
new_gray[i, j] = np.sum(
img_gray[i: i + kernel_size, j: j + kernel_size] * gaussian
)
return new_gray
3、计算图像的梯度幅值
def gradients(new_gray):
"""
:type: image which after smooth
:rtype:
dx: gradient in the x direction
dy: gradient in the y direction
M: gradient magnitude
theta: gradient direction
"""
W, H = new_gray.shape
dx = np.zeros([W-1, H-1])
dy = np.zeros([W-1, H-1])
M = np.zeros([W-1, H-1])
theta = np.zeros([W-1, H-1])
for i in range(W-1):
for j in range(H-1):
dx[i, j] = new_gray[i+1, j] - new_gray[i, j]
dy[i, j] = new_gray[i, j+1] - new_gray[i, j]
# 图像梯度幅值作为图像强度值
M[i, j] = np.sqrt(np.square(dx[i, j]) + np.square(dy[i, j]))
# 计算 θ - artan(dx/dy)
theta[i, j] = math.atan(dx[i, j] / (dy[i, j] + 0.000000001))
return dx, dy, M, theta
4、非极大值抑制
def NMS(M, dx, dy):
d = np.copy(M)
W, H = M.shape
NMS = np.copy(d)
NMS[0, :] = NMS[W-1, :] = NMS[:, 0] = NMS[:, H-1] = 0
for i in range(1, W-1):
for j in range(1, H-1):
# 如果当前梯度为0,该点就不是边缘点
if M[i, j] == 0:
NMS[i, j] = 0
else:
gradX = dx[i, j] # 当前点 x 方向导数
gradY = dy[i, j] # 当前点 y 方向导数
gradTemp = d[i, j] # 当前梯度点
# 如果 y 方向梯度值比较大,说明导数方向趋向于 y 分量
if np.abs(gradY) > np.abs(gradX):
weight = np.abs(gradX) / np.abs(gradY) # 权重
grad2 = d[i-1, j]
grad4 = d[i+1, j]
# 如果 x, y 方向导数符号一致
# 像素点位置关系
# g1 g2
# c
# g4 g3
if gradX * gradY > 0:
grad1 = d[i-1, j-1]
grad3 = d[i+1, j+1]
# 如果 x,y 方向导数符号相反
# 像素点位置关系
# g2 g1
# c
# g3 g4
else:
grad1 = d[i-1, j+1]
grad3 = d[i+1, j-1]
# 如果 x 方向梯度值比较大
else:
weight = np.abs(gradY) / np.abs(gradX)
grad2 = d[i, j-1]
grad4 = d[i, j+1]
# 如果 x, y 方向导数符号一致
# 像素点位置关系
# g3
# g2 c g4
# g1
if gradX * gradY > 0:
grad1 = d[i+1, j-1]
grad3 = d[i-1, j+1]
# 如果 x,y 方向导数符号相反
# 像素点位置关系
# g1
# g2 c g4
# g3
else:
grad1 = d[i-1, j-1]
grad3 = d[i+1, j+1]
# 利用 grad1-grad4 对梯度进行插值
gradTemp1 = weight * grad1 + (1 - weight) * grad2
gradTemp2 = weight * grad3 + (1 - weight) * grad4
# 当前像素的梯度是局部的最大值,可能是边缘点
if gradTemp >= gradTemp1 and gradTemp >= gradTemp2:
NMS[i, j] = gradTemp
else:
# 不可能是边缘点
NMS[i, j] = 0
return NMS
5、图像二值化和边缘连接
def double_threshold(NMS, threshold1, threshold2):
NMS = np.pad(NMS, ((1, 1), (1, 1)), mode='constant')
W, H = NMS.shape
DT = np.zeros([W, H])
# 定义高低阈值
TL = threshold1 * np.max(NMS)
TH = threshold2 * np.max(NMS)
for i in range(1, W-1):
for j in range(1, H-1):
# 双阈值选取
if (NMS[i, j] < TL):
DT[i, j] = 0
elif (NMS[i, j] > TH):
DT[i, j] = 1
# 连接
elif ((NMS[i-1, j-1:j+1] < TH).any() or
(NMS[i+1, j-1:j+1].any() or
(NMS[i, [j-1, j+1]] < TH).any())):
DT[i, j] = 1
return DT
6、Canny 边缘检测
def canny(gray, threshold1, threshold2, kernel_size=5):
norm_gray = gray
gray_smooth = smooth(norm_gray, kernel_size)
dx, dy, M, theta = gradients(gray_smooth)
nms = NMS(M, dx, dy)
DT = double_threshold(nms, threshold1, threshold2)
return DT
7、代码测试
import cv2
import numpy as np
from PIL import Image
lower = 0.1 # 最小阈值
upper = 0.3 # 最大阈值
img_path = 'cat.jpg' # 指定测试图像路径
gray = cv2.imread(img_path, 0) # 读取灰度图像
edge = canny(gray, lower, upper) # Canny 图像边缘检测
edge = (edge * 255).astype(np.uint8) # 反归一化
contrast = np.concatenate([edge, gray], 1) # 图像拼接
Image.fromarray(contrast).show() # 显示图像
运行结果为:
基于Pytorch 实现的 Canny 边缘检测器
1、导入必要的模块
# coding=gbk
import torch
import torch.nn as nn
import math
import cv2
import numpy as np
from scipy.signal import gaussian
2、卷积参数设置
def get_state_dict(filter_size=5, std=1.0, map_func=lambda x:x):
generated_filters = gaussian(filter_size, std=std).reshape([1, filter_size
]).astype(np.float32)
gaussian_filter_horizontal = generated_filters[None, None, ...]
gaussian_filter_vertical = generated_filters.T[None, None, ...]
sobel_filter_horizontal = np.array([[[
[1., 0., -1.],
[2., 0., -2.],
[1., 0., -1.]]]],
dtype='float32'
)
sobel_filter_vertical = np.array([[[
[1., 2., 1.],
[0., 0., 0.],
[-1., -2., -1.]]]],
dtype='float32'
)
directional_filter = np.array(
[[[[ 0., 0., 0.],
[ 0., 1., -1.],
[ 0., 0., 0.]]],
[[[ 0., 0., 0.],
[ 0., 1., 0.],
[ 0., 0., -1.]]],
[[[ 0., 0., 0.],
[ 0., 1., 0.],
[ 0., -1., 0.]]],
[[[ 0., 0., 0.],
[ 0., 1., 0.],
[-1., 0., 0.]]],
[[[ 0., 0., 0.],
[-1., 1., 0.],
[ 0., 0., 0.]]],
[[[-1., 0., 0.],
[ 0., 1., 0.],
[ 0., 0., 0.]]],
[[[ 0., -1., 0.],
[ 0., 1., 0.],
[ 0., 0., 0.]]],
[[[ 0., 0., -1.],
[ 0., 1., 0.],
[ 0., 0., 0.]]]],
dtype=np.float32
)
connect_filter = np.array([[[
[1., 1., 1.],
[1., 0., 1.],
[1., 1., 1.]]]],
dtype=np.float32
)
return {
'gaussian_filter_horizontal.weight': map_func(gaussian_filter_horizontal),
'gaussian_filter_vertical.weight': map_func(gaussian_filter_vertical),
'sobel_filter_horizontal.weight': map_func(sobel_filter_horizontal),
'sobel_filter_vertical.weight': map_func(sobel_filter_vertical),
'directional_filter.weight': map_func(directional_filter),
'connect_filter.weight': map_func(connect_filter)
}
3、Canny 检测器
class CannyDetector(nn.Module):
def __init__(self, filter_size=5, std=1.0, device='cpu'):
super(CannyDetector, self).__init__()
# 配置运行设备
self.device = device
# 高斯滤波器
self.gaussian_filter_horizontal = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(1,filter_size), padding=(0,filter_size//2), bias=False)
self.gaussian_filter_vertical = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(filter_size,1), padding=(filter_size//2,0), bias=False)
# Sobel 滤波器
self.sobel_filter_horizontal = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1, bias=False)
self.sobel_filter_vertical = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1, bias=False)
# 定向滤波器
self.directional_filter = nn.Conv2d(in_channels=1, out_channels=8, kernel_size=3, padding=1, bias=False)
# 连通滤波器
self.connect_filter = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, padding=1, bias=False)
# 初始化参数
params = get_state_dict(filter_size=filter_size, std=std, map_func=lambda x:torch.from_numpy(x).to(self.device))
self.load_state_dict(params)
@torch.no_grad()
def forward(self, img, threshold1=10.0, threshold2=100.0):
# 拆分图像通道
img_r = img[:,0:1] # red channel
img_g = img[:,1:2] # green channel
img_b = img[:,2:3] # blue channel
# Step1: 应用高斯滤波进行模糊降噪
blur_horizontal = self.gaussian_filter_horizontal(img_r)
blurred_img_r = self.gaussian_filter_vertical(blur_horizontal)
blur_horizontal = self.gaussian_filter_horizontal(img_g)
blurred_img_g = self.gaussian_filter_vertical(blur_horizontal)
blur_horizontal = self.gaussian_filter_horizontal(img_b)
blurred_img_b = self.gaussian_filter_vertical(blur_horizontal)
# Step2: 用 Sobel 算子求图像的强度梯度
grad_x_r = self.sobel_filter_horizontal(blurred_img_r)
grad_y_r = self.sobel_filter_vertical(blurred_img_r)
grad_x_g = self.sobel_filter_horizontal(blurred_img_g)
grad_y_g = self.sobel_filter_vertical(blurred_img_g)
grad_x_b = self.sobel_filter_horizontal(blurred_img_b)
grad_y_b = self.sobel_filter_vertical(blurred_img_b)
# Step2: 确定边缘梯度和方向
grad_mag = torch.sqrt(grad_x_r**2 + grad_y_r**2)
grad_mag += torch.sqrt(grad_x_g**2 + grad_y_g**2)
grad_mag += torch.sqrt(grad_x_b**2 + grad_y_b**2)
grad_orientation = (torch.atan2(grad_y_r+grad_y_g+grad_y_b, grad_x_r+grad_x_g+grad_x_b) * (180.0/math.pi))
grad_orientation += 180.0
grad_orientation = torch.round(grad_orientation / 45.0) * 45.0
# Step3: 非最大抑制,边缘细化
all_filtered = self.directional_filter(grad_mag)
inidices_positive = (grad_orientation / 45) % 8
inidices_negative = ((grad_orientation / 45) + 4) % 8
batch, _, height, width = inidices_positive.shape
pixel_count = height * width * batch
pixel_range = torch.Tensor([range(pixel_count)]).to(self.device)
indices = (inidices_positive.reshape((-1, )) * pixel_count + pixel_range).squeeze()
channel_select_filtered_positive = all_filtered.reshape((-1, ))[indices.long()].reshape((batch, 1, height, width))
indices = (inidices_negative.reshape((-1, )) * pixel_count + pixel_range).squeeze()
channel_select_filtered_negative = all_filtered.reshape((-1, ))[indices.long()].reshape((batch, 1, height, width))
channel_select_filtered = torch.stack([channel_select_filtered_positive, channel_select_filtered_negative])
is_max = channel_select_filtered.min(dim=0)[0] > 0.0
thin_edges = grad_mag.clone()
thin_edges[is_max==0] = 0.0
# Step4: 双阈值
low_threshold = min(threshold1, threshold2)
high_threshold = max(threshold1, threshold2)
thresholded = thin_edges.clone()
lower = thin_edges<low_threshold
thresholded[lower] = 0.0
higher = thin_edges>high_threshold
thresholded[higher] = 1.0
connect_map = self.connect_filter(higher.float())
middle = torch.logical_and(thin_edges>=low_threshold, thin_edges<=high_threshold)
thresholded[middle] = 0.0
connect_map[torch.logical_not(middle)] = 0
thresholded[connect_map>0] = 1.0
thresholded[..., 0, :] = 0.0
thresholded[..., -1, :] = 0.0
thresholded[..., :, 0] = 0.0
thresholded[..., :, -1] = 0.0
thresholded = (thresholded>0.0).float()
return thresholded
4、代码测试
if __name__ == '__main__':
img_path = 'cat.jpg'
res_path = "cat._torch_our.jpg"
img = cv2.imread(img_path)/255.0 # height, width, channel
img = np.transpose(img, [2, 1, 0]) # channel width height
canny_operator = CannyDetector()
result = canny_operator(torch.from_numpy(np.expand_dims(img, axis=0)).float(),threshold1=2.5, threshold2=5 ) # batch channel width height
res = np.squeeze(result.numpy())
res = np.transpose(res, [1, 0])
res = (res*255).astype(np.uint8)
cv2.imwrite(res_path, res)
运行结果为:
详细说一下canny(这个确实比较火,之前做项目用过sobel算子,但是后来发现还是canny较多)
这个由于实操过,发现canny效果是真的好,所以我仔细学了学算法思想和步骤。
Canny算法
Canny算法处理图片的过程主要分为五个步骤
1、去噪
图片中的高频信息指颜色快速变化,低频信息指颜色平缓的变化。边缘检测过程中需要检测的图片边缘属于高频信息。而图片中噪声部分也属于高频信息,因此我们需要对图像进行去噪处理。 关于噪音详情可参考: 图像各种噪声及消除方法 Canny算法中用5*5的高斯滤波核来平滑图像,消除噪声。
2、计算梯度值和梯度方向
计算像素梯度的幅值以及方向,常用的算子有Rober,Sobel,计算水平及垂直方向的差分。找出梯度较大的区域,这部分区域属于图像增强的区域,此时得到的边缘信息比较粗大Canny算法运用Sobel来进行一阶偏导的有限差分计算梯度的幅值和方向。
运用一对卷积阵列 (分别作用于x和y方向)
梯度方向近似到四个可能角度之一(一般 0, 45, 90, 135)
3、非极大值抑制
非极大值抑制属于一种边缘细化的方法,梯度大的位置有可能为边缘,在这些位置沿着梯度方向,找到像素点的局部最大值,并将非最大值抑制。通俗的来说,就是获取局部的最大值,将非极大值所对应的灰度值设置为背景像素点。像素邻近区域满足梯度值的局部最优值判断为该像素的边缘,对非极大值相关信息进行抑制。利用这个准则可以剔除大部分的非边缘点。
简单的说就是保留梯度大的像素点,对于那些在边缘旁边的杂散点,梯度相对较小,利用非极大值抑制就可以很好的去除杂散点。
4、双阈值检测
Canny 算法使用了滞后阈值,滞后阈值需要两个阈值(高阈值和低阈值):
双阀值方法,设置一个maxval,以及minval,梯度大于maxval则为强边缘,梯度值介于maxval与minval则为弱边缘点,小于minval为抑制点。
如果某一像素位置的幅值超过高阈值, 该像素被保留为边缘像素。如果某一像素位置的幅值小于低阈值, 该像素被排除。如果某一像素位置的幅值在两个阈值之间,该像素仅仅在连接到一个高于高阈值的像素时被保留。具体可参考:双阈值检测 还有增大minval和增大maxval的效果很直观、方便
5、滞后边缘追踪
滞后边缘追踪,主要处理梯度值位于maxval,minval中的一些像素点。由于边缘是连续的,因此可以认为弱边缘如果为真实边缘则和强边缘是联通的,可由此判断其是否为真实边缘。
对于论文复现的经验与方法
由于参加建模会找很多的论文来看,所以当时也像老师,询问了好多如何找论文,如何看论文的做法。
首先,对于真的想复现的话,建议去下边的链接来看一看,真的会有很大的收获,如果结合这链接来看,一定会对paddle论文的复现是有帮助的,下边是百度的论文复现营,但是可能这个里边的论文比较难,但是工具和方法真的非常有用,强烈推荐。
如何选一篇你想要的论文呢,为了节约时间一定要去看摘要,如果摘要符合要求的话,那一定是你想要的,因为摘要是一篇论文的重中之重,他的核心都在摘要中体现,下边我从摘要中说一说论文。
3、选做3
边缘检测系列3:【HED】 Holistically-Nested 边缘检测 - 飞桨AI Studio
复现论文 Holistically-Nested Edge Detection,发表于 CVPR 2015
一个基于深度学习的端到端边缘检测模型。
解决的问题
解决了这一长期存在的视觉问题中的两个重要问题:
1、整体图像的训练与预测
2、多尺度、多层次的特征学习。
解决的方法
整体嵌套边缘检测(HED),通过一个利用完全卷积神经网络和深度监督网络的深度学习模型来执行图像到图像的预测
解决问题的原理
HED自动学习丰富的层次表示法(在边响应的深度监督指导下),这对于接近人类解决边缘和物体边界检测中的模糊性的能力非常重要。我们显著地提高了BSD500数据集(ODS F分数为.782)和纽约大学深度数据集(ODS F分数为.746)的最新技术,并以改进的速度(每幅图像0.4秒)比一些基于CNN的边缘检测算法快一个数量级。
创新点
(1)端到端:image-to-image
(2)基于FCN和VGG 改进,同时引出6个loss进行优化训练,通过多个side output输出不同scale的边缘,然后通过一个训练的权重融合函数得到最终的边缘输出。可以solve edge 和物体boundaries的ambiguity
(3)样本不平衡处理方法:class-balanced_sigmoid_cross_entropy
4、选做
复现论文 Richer Convolutional Features for Edge Detection,CVPR 2017 发表
一个基于更丰富的卷积特征的边缘检测模型 【RCF】。
解决的问题
边缘检测是视觉任务中非常基础的任务,现有的基于CNN的边缘检测方法有两个明显的问题:
- 现有的方法大多只使用CNN的最后一层conv的结果,忽略了中间层的结果
- 更多的方法集中在探究更深的CNN,但边缘检测是数据比较少,而且容易发生梯度消失的现象
解决的方法
- VGG作为主干网络,每个卷积层捕获的有用信息随着感受野大小的增加而变得更粗糙(以下5条是论文中对网络架构的介绍)
- 提出RCF,切断了所有全连接层、池化层第5层,切除全连接层是为了使用全卷积层,实现image-to-image的预测,加入pool5层会使步幅增加两倍,这通常会导致边缘定位的退化
- VGG16中每个阶段中的每个卷积层都连接到核大小为1×1和通道深度为21的卷积层。每个阶段的特征映射都是用一个eltwise层(相加)来积累的,以获得混合特征。
- 一个1×1个卷积层跟随每个eltwise层。然后,使用一个反卷积层对这个特征映射进行上采样。
- 在每个阶段,交叉熵损失/Sigmoid连接到上采样层。
- 所有上采样层都是级联的。 然后使用1×1卷积从每个阶段融合特征映射。 最后,采用交叉熵损失/Sigmoid得到融合损失/输出
5、选做5
边缘检测系列5:【CED】添加了反向细化路径的 HED 模型 - 飞桨AI Studio (baidu.com)
Crisp Edge Detection(CED)模型是前面介绍过的 HED 模型的另一种改进模型
解决的问题
检测结果没有准确定位边缘像素,这可能是需要清晰边缘输入的对抗性任务。
解决的方法
CED 模型总体基于 HED 模型改造而来,其中做了如下几个改进:
将模型中的上采样操作从转置卷积插值更换为 PixelShuffle
添加了反向细化路径,即一个反向的从高层级特征逐步往低层级特征的边缘细化路径
没有多层级输出,最终的输出为融合了各层级的特征的边缘检测结果
架构图如下
有关栈溢出问题的讨论
首先,鸣谢我的室友, 鸣谢我的室友不是蒋承翰的博客_CSDN博客-领域博主以及HBU_fangerfang的博客_CSDN博客-神经网络与深度学习领域博主对本实验的大力支持。
在这次实验中,可能好多同学运行第一问,运行不出来结果,甚至用jupyter也会导致内核出现问题,有时运行的出来,有时运行不出来。
这次,是我和室友都试了好多算法,最后终于我运行出来了,这次,终于是做了点贡献。
会出现下边的报错。
进程已结束,退出代码-1073741571 (0xC00000FD)
这个其实就是栈溢出的问题,说直白的点就是计算量太大 ,直接超出了硬件的能力。
但是,这个是可以用一些算法来避免的,好的算法,是可以用来提升时间和空间效率的,但是我们试了各种提升效率的算法,还是会栈溢出,这说明其溢出的特别多,一般的算法无法解决。
那我们就要从另一个角度想问题,那就是约束它的计算量,这样就可以算出来了。
所以我们做如下更改(将resize进行更改)。
img = Image.open('number.jpg').convert('L').resize((100,100))
区别就是这样计算量会被强制约束,这样就解决了栈溢出的问题,但是感觉这并不是一个很好的方法。因为感觉,这样会导致图像部分失真。
但是,我们最后测试出了临界为104。
再次鸣谢我的室友不是蒋承翰的博客_CSDN博客-领域博主以及HBU_fangerfang的博客_CSDN博客-神经网络与深度学习领域博主。
总结
这次,我真的写了好久,尤其是查资料的过程,很费劲,但是给大家强烈推荐一个好用的东西,以后复现论文,看飞桨的话,感觉会有帮助,这次总算是还算有收获。
其次,这次由于我之前做项目用过sobel算子,并且当时也做了对比,但是没有研究canny算法,这次好好弄了弄,发现了canny的优越性以及普遍性,感觉学到的好多。
其次,这次,和上次的卷积操作联系起来了,之前是只写了卷积,但是这次是写了卷积层,感觉正的明白了原理,这个是真的学到了好多。
其次,这次又和室友讨论了问题,这次我终于做了贡献,研究出来,所以这次有点开心。
其次,这两天事真的有点多,所以我真的有点累,并且下次我作业争取要早交。
最后,当然是谢谢老师,感谢老师再学习和生活上的关心。