卷积常用于特征提取
实验过程中注意认真体会“特征提取”,弄清楚为什么卷积能够提取特征。
作业1
编程实现
1.图1使用卷积核( 1 −1 ),输出特征图
import torch
import matplotlib.pyplot as plt
import torch.nn.functional as F
import numpy as np
#生成图片
def create_pic():
picture = torch.Tensor([[0,0,0,255,255,255],
[0,0,0,255,255,255],
[0,0,0,255,255,255],
[0,0,0,255,255,255],
[0,0,0,255,255,255]])
return picture
#确定卷积网络
class MyNet(torch.nn.Module):
def __init__(self,kernel,kshape):
super(MyNet, self).__init__()
kernel = torch.reshape(kernel,kshape)
self.weight = torch.nn.Parameter(data=kernel, requires_grad=False)
def forward(self, picture):
picture = F.conv2d(picture,self.weight,stride=1,padding=0)
return picture
#确定卷积层
kernel = torch.tensor([-1.0,1.0])
#更改卷积层的形状适应卷积函数
kshape = (1,1,1,2)
#生成模型
model = MyNet(kernel=kernel,kshape=kshape)
#生成图片
picture = create_pic()
#更改图片的形状适应卷积层
picture = torch.reshape(picture,(1,1,5,6))
output = model(picture)
output = torch.reshape(output,(5,5))
plt.imshow(output,cmap='gray')
plt.show()
运行结果:
2. 图1使用卷积核( − 1 1 ^{1}_{-1} −11 ),输出特征图
kernel = torch.tensor([-1.0,1.0])
#更改卷积和的形状为转置
kshape = (1,1,2,1)
model = MyNet(kernel=kernel,kshape=kshape)
picture = create_pic()
picture = torch.reshape(picture,(1,1,5,6))
output = model(picture)
output = torch.reshape(output,(6,4))
plt.imshow(output,cmap='gray')
plt.show()
运行结果:
3. 图2使用卷积核( 1 − 1 ),输出特征图
#生成图像
def create_pic():
picture = torch.Tensor([[0,0,0,255,255,255],
[0,0,0,255,255,255],
[0,0,0,255,255,255],
[255,255,255,0,0,0],
[255,255,255,0,0,0],
[255,255,255,0,0,0]])
return picture
#确定卷积核
kernel = torch.tensor([-1.0,1.0])
kshape = (1,1,1,2)
#生成模型
model = MyNet(kernel=kernel,kshape=kshape)
picture = create_pic()
picture = torch.reshape(picture,(1,1,6,6))
print(picture)
output = model(picture)
output = torch.reshape(output,(6,5))
print(output)
plt.imshow(output,cmap='gray')
plt.show()
运行结果:
4. 图2使用卷积核( − 1 1 ^{1}_{-1} −11 ),输出特征图
def create_pic():
picture = torch.Tensor([[0,0,0,255,255,255],
[0,0,0,255,255,255],
[0,0,0,255,255,255],
[255,255,255,0,0,0],
[255,255,255,0,0,0],
[255,255,255,0,0,0]])
return picture
kernel = torch.tensor([-1.0,1.0])
kshape = (1,1,2,1)
model = MyNet(kernel=kernel,kshape=kshape)
picture = create_pic()
picture = torch.reshape(picture,(1,1,6,6))
print(picture)
output = model(picture)
output = torch.reshape(output,(5,6))
print(output)
plt.imshow(output,cmap='gray')
plt.show()
运行结果:
5. 图3使用卷积核( 1 − 1 ),( − 1 1 ^{1}_{-1} −11 ),( − 1 1 ^{1}_{-1} −11 1 − 1 ^{-1}_{ 1} 1−1)输出特征图
import numpy as np
import torch
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
w1 = np.array([1, -1], dtype='float32').reshape([1, 1, 1, 2])
w2 = np.array([1, -1], dtype='float32').T.reshape([1, 1, 2, 1])
w3 = np.array([[1,-1,-1,1]],dtype='float32').reshape([1,1,2,2])
print(w3)
w1 = torch.Tensor(w1)
w2 = torch.Tensor(w2)
w3 = torch.Tensor(w3)
conv1 = torch.nn.Conv2d(1, 1, [1, 2])
conv1.weight = torch.nn.Parameter(w1)
conv2 = torch.nn.Conv2d(1, 1, [2, 1])
conv2.weight = torch.nn.Parameter(w2)
conv3 = torch.nn.Conv2d(1,1,[2,2])
conv3.weight = torch.nn.Parameter(w3)
# 创建图像
img = np.ones([9, 9], dtype='float32')
for i in range(7):
img[i+1,i+1]=255.
img[i+1,7-i]=255.
x = img.reshape([1, 1, 9, 9])
x = torch.Tensor(x)
y1 = conv1(x).detach().numpy()
y2 = conv2(x).detach().numpy()
y3 = conv3(x).detach().numpy()
plt.subplot(221).set_title('图3')
plt.imshow(img, cmap='gray')
plt.subplot(222).set_title('图3使用卷积核为(1,-1)结果')
plt.imshow(y1.squeeze(),cmap='gray')
plt.subplot(223).set_title('图3使用卷积核为(1,-1)T结果')
plt.imshow(y2.squeeze(),cmap='gray')
plt.subplot(224).set_title('图3使用卷积核为[[1 -1],[-1 1]]结果')
plt.imshow(y3.squeeze(),cmap='gray')
plt.show()
运行结果:
作业2
一、概念
用自己的语言描述“卷积、卷积核、特征图、特征选择、步长、填充、感受野”。
-
卷积(Convolution):卷积是透过两个函数 f 和 g 生成第三个函数的一种数学算子,表征函数 f 与经过翻转和平移的 g 的乘积函数所围成的曲边梯形的面积。
在图像卷积中,那两个函数分别是原图和卷积核,我们先将卷积核和原始图像对齐,然后计算重叠部分的点积结果,计算完一个后,将卷积核按一定步长向后移动,这样一直重复直到卷积核移动到图像的末尾。
卷积运算的定义式:
图像处理中的卷积:通常对图像进行卷积是指对图像中的所有像素做卷积运算。- 对单色点阵图像中的某一像素g [ x , y ] g[x,y]g[x,y]进行的卷积运算为2元离散卷积。
- f [ m , n ] f[m,n]f[m,n]又称为卷积核、特征图(Feature Map)(实际上也是一个矩阵)
- 如果是彩色图像,则可以转化为三元离散卷积,或者进行3次二元卷积(对于3个颜色通道的图像)
-
卷积核:卷积核就是图像处理时,给定输入图像,输入图像中一个小区域中像素加权平均后成为输出图像中的每个对应像素,其中权值由一个函数定义,这个函数称为卷积核。
-
特征图:多个找出的特征就是特征图。比如一个二维的像素图,每个特征是略小于原图的二维图,特征图就是n个特征叠加的一个三维的图。
-
特征选择:特征选择是指从已有的M个特征中选择N个特征使得系统的特定指标最优化,是从原始特征中选择出一些最有效特征以降低数据集维度的过程,是提高学习算法性能的一个重要手段,也是模式识别中关键的数据预处理步骤。对于一个学习算法来说,好的学习样本是训练模型的关键。
-
步长:滑动卷积核时,我们会先从输入的左上角开始,每次往左滑动一列或者往下滑动一行逐一计算输出,我们将每次滑动的行数和列数称为Stride,在之前的图片中,Stride=1;在下图中,Stride=2。
卷积过程中,有时需要通过padding来避免信息损失,有时也要在卷积时通过设置的步长(Stride)来压缩一部分信息,或者使输出的尺寸小于输入的尺寸。 -
填充:输入图像与卷积核进行卷积后的结果中损失了部分值,输入图像的边缘被“修剪”掉了。这是因为边缘上的像素永远不会位于卷积核中心,而卷积核也没法扩展到边缘区域以外。
这个结果我们是不能接受的,有时我们还希望输入和输出的大小应该保持一致。为解决这个问题,可以在进行卷积操作前,对原矩阵进行边界填充,也就是在矩阵的边界上填充一些值,以增加矩阵的大小。 -
感受野:是指输出特征图上某个像素对应到输入空间(原图)中的区域范围。所以感受野可以理解为特征图像素到输入区域的映射。
感受野的作用:- 一般task要求感受野越大越好,如图像分类中最后卷积层的感受野要大于输入图像,网络深度越深感受野越大性能越好
- 密集预测task要求输出像素的感受野足够的大,确保做出决策时没有忽略重要信息,一般也是越深越好
- 目标检测task中设置anchor要严格对应感受野,anchor太大或偏离感受野都会严重影响检测性能
二、探究不同卷积核的作用
参考:https://setosa.io/ev/image-kernels/
1.边缘检测
边缘检测的目的就是找到图像中亮度变化剧烈的像素点构成的集合,表现出来往往是轮廓。
卷积核:
2.锐化
图像锐化也称边缘增强。锐化技术用于加强图像中的边界和细节信息。由于边界和细节信息对应频域中的高频部分,所以在频域中通常对图像进行高通滤波,在空间域则进行微分处理。
卷积核:
3.模糊
卷积核:
4.浮雕
卷积核:
三、编程实现
1. 实现灰度图的边缘检测、锐化、模糊。(必做)
原图:
import numpy as np
import torch
from torch import nn
from torch.autograd import Variable
from PIL import Image
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 #有中文出现的情况,需要u'内容
# https://blog.csdn.net/weixin_40123108/article/details/83510592
file_path = 'deer.jpg'
im = Image.open(r'C:\Users\86155\Desktop\dog.jpg').convert('L') # 读入一张灰度图的图片
im = np.array(im, dtype='float32') # 将其转换为一个矩阵
print(im.shape[0], im.shape[1])
plt.imshow(im.astype('uint8'), cmap='gray') # 可视化图片
plt.title('原图')
plt.show()
运行结果:
边缘检测:
im = torch.from_numpy(im.reshape((1, 1, im.shape[0], im.shape[1])))
conv1 = nn.Conv2d(1, 1, 3, bias=False) # 定义卷积
sobel_kernel = np.array([[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]], dtype='float32') # 定义轮廓检测算子
sobel_kernel = sobel_kernel.reshape((1, 1, 3, 3)) # 适配卷积的输入输出
conv1.weight.data = torch.from_numpy(sobel_kernel) # 给卷积的 kernel 赋值
edge1 = conv1(Variable(im)) # 作用在图片上
x = edge1.data.squeeze().numpy()
print(x.shape) # 输出大小
plt.imshow(x, cmap='gray')
plt.show()
锐化:
#encoding:utf-8
#By:Eastmount CSDN 2021-07-19
import cv2
import numpy as np
import matplotlib.pyplot as plt
#读取图像
img = cv2.imread(r'C:\Users\86155\Desktop\dog.jpg')
lenna_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
#灰度化处理图像
grayImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#高斯滤波
gaussianBlur = cv2.GaussianBlur(grayImage, (3,3), 0)
#阈值处理
ret, binary = cv2.threshold(gaussianBlur, 127, 255, cv2.THRESH_BINARY)
#Roberts算子
kernelx = np.array([[-1,0],[0,1]], dtype=int)
kernely = np.array([[0,-1],[1,0]], dtype=int)
x = cv2.filter2D(binary, cv2.CV_16S, kernelx)
y = cv2.filter2D(binary, cv2.CV_16S, kernely)
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Roberts = cv2.addWeighted(absX, 0.5, absY, 0.5, 0)
#Prewitt算子
kernelx = np.array([[1,1,1],[0,0,0],[-1,-1,-1]], dtype=int)
kernely = np.array([[-1,0,1],[-1,0,1],[-1,0,1]], dtype=int)
x = cv2.filter2D(binary, cv2.CV_16S, kernelx)
y = cv2.filter2D(binary, cv2.CV_16S, kernely)
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Prewitt = cv2.addWeighted(absX,0.5,absY,0.5,0)
#Sobel算子
x = cv2.Sobel(binary, cv2.CV_16S, 1, 0)
y = cv2.Sobel(binary, cv2.CV_16S, 0, 1)
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Sobel = cv2.addWeighted(absX, 0.5, absY, 0.5, 0)
#拉普拉斯算法
dst = cv2.Laplacian(binary, cv2.CV_16S, ksize = 3)
Laplacian = cv2.convertScaleAbs(dst)
#效果图
titles = ['Source Image', 'Binary Image', 'Roberts Image',
'Prewitt Image','Sobel Image', 'Laplacian Image']
images = [lenna_img, binary, Roberts, Prewitt, Sobel, Laplacian]
for i in np.arange(6):
plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
运行结果:
模糊:
(1)均值模糊
import cv2
import numpy as np
import matplotlib.pyplot as plt
if __name__ == "__main__":
image = cv2.imread(r'C:\Users\86155\Desktop\dog.jpg')
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 此为均值模糊
# (30,1)为一维卷积核,指在x,y方向偏移多少位
dst1 = cv2.blur(image, (30, 1))
# 此为中值模糊,常用于去除椒盐噪声
dst2 = cv2.medianBlur(image, 15)
# 自定义卷积核,执行模糊操作,也可定义执行锐化操作
kernel = np.ones([5, 5], np.float32) / 25
dst3 = cv2.filter2D(image, -1, kernel=kernel)
plt.subplot(2, 2, 1)
plt.imshow(image)
plt.axis('off')
plt.title('Offical')
plt.subplot(2, 2, 2)
plt.imshow(dst1)
plt.axis('off')
plt.title('Box blur')
plt.subplot(2, 2, 3)
plt.imshow(dst2)
plt.axis('off')
plt.title('median blur')
plt.subplot(2, 2, 4)
plt.imshow(dst3)
plt.axis('off')
plt.title('defined blur')
plt.show()
运行结果:
(2)高斯模糊
# 图像模糊处理
# 高斯模糊 gaussian blur
# 使用自编写高斯噪声及自编写高斯模糊函数与自带高斯函数作效果对比
import cv2
import numpy as np
import matplotlib.pyplot as plt
def clamp(pv):
if pv > 255:
return 255
if pv < 0:
return 0
else:
return pv
def gaussian_noise(image): # 加高斯噪声
h, w, c = image.shape
for row in range(h):
for col in range(w):
s = np.random.normal(0, 20, 3)
b = image[row, col, 0] # blue
g = image[row, col, 1] # green
r = image[row, col, 2] # red
image[row, col, 0] = clamp(b + s[0])
image[row, col, 1] = clamp(g + s[1])
image[row, col, 2] = clamp(r + s[2])
dst = cv2.GaussianBlur(image, (15, 15), 0) # 高斯模糊
return dst, image
if __name__ == "__main__":
src = cv2.imread(r'C:\Users\86155\Desktop\dog.jpg')
plt.subplot(2, 2, 1)
plt.imshow(src)
plt.axis('off')
plt.title('Offical')
output, noise = gaussian_noise(src)
cvdst = cv2.GaussianBlur(src, (15, 15), 0) # 高斯模糊
plt.subplot(2, 2, 2)
plt.imshow(noise)
plt.axis('off')
plt.title('Gaussian Noise')
plt.subplot(2, 2, 3)
plt.imshow(output)
plt.axis('off')
plt.title('Gaussian Blur')
plt.subplot(2, 2, 4)
plt.imshow(cvdst)
plt.axis('off')
plt.title('defined blur by opencv')
plt.show()
运行结果:
(3)运动模糊
# 图像模糊处理
# 运动模糊,亦称动态模糊,motion blur
# 运动模糊:由于相机和物体之间的相对运动造成的模糊
import numpy as np
import cv2
import matplotlib.pyplot as plt
def motion_blur(image, degree=12, angle=45):
image = np.array(image)
# 这里生成任意角度的运动模糊kernel的矩阵, degree越大,模糊程度越高
M = cv2.getRotationMatrix2D((degree / 2, degree / 2), angle, 1)
motion_blur_kernel = np.diag(np.ones(degree))
motion_blur_kernel = cv2.warpAffine(motion_blur_kernel, M, (degree, degree))
motion_blur_kernel = motion_blur_kernel / degree
blurred = cv2.filter2D(image, -1, motion_blur_kernel)
# convert to uint8
cv2.normalize(blurred, blurred, 0, 255, cv2.NORM_MINMAX)
blurred = np.array(blurred, dtype=np.uint8)
return blurred
if __name__ == "__main__":
img = cv2.imread(r'C:\Users\86155\Desktop\dog.jpg')
dst = motion_blur(img)
plt.subplot(1, 2, 1)
plt.imshow(img)
plt.axis('off')
plt.title('Offical')
plt.subplot(1, 2, 2)
plt.imshow(dst)
plt.axis('off')
plt.title('Motion blur')
plt.show()
运行结果:
2. 调整卷积核参数,测试并总结。(必做)
设置步长为2:
步长为3:
步长为4:
总结
步长小,提取的特征会更全面,但同时可能造成计算量增大,甚至过拟合等问题。步长大,计算量会下降,但很有可能错失一些有用的特征。随着步长的变大,像素点逐渐减少,并且图像提取的边界越来越模糊。
设置padding=2
padding=5
padding=8
总结
加入padding后图像的边缘数据也能被利用到,这样才能更好的扩张整张图像的边缘特征,保持了边界的信息。
3. 使用不同尺寸图片,测试并总结。(必做)
原图:
边缘检测:
锐化:
高斯模糊:
4. 探索更多类型卷积核。(选做)
上面已经用过了均值模糊、高斯模糊、运动模糊,这里看一下浮雕:
sobel_kernel = np.array([[-2, -1, 0],
[-1, 1, 1],
[0, 1, 2]], dtype='float32') # 定义轮廓检测算子
运行结果:
5. 尝试彩色图片边缘检测。(选做)
显示彩色原图:
import numpy as np
import torch
from torch import nn
from torch.autograd import Variable
from PIL import Image
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 #有中文出现的情况,需要u'内容
# https://blog.csdn.net/weixin_40123108/article/details/83510592
file_path = 'a.webp'
im = Image.open(file_path).convert('RGB') # 读入一张灰度图的图片
im = np.array(im, dtype='float32') # 将其转换为一个矩阵
print(im.shape[0], im.shape[1])
plt.imshow(im.astype('uint8'), cmap='gray') # 可视化图片
plt.title('原图')
plt.show()
运行结果:
import numpy as np
import torch
from torch import nn
from torch.autograd import Variable
from PIL import Image
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 #有中文出现的情况,需要u'内容
# https://blog.csdn.net/weixin_40123108/article/details/83510592
file_path = 'a.webp'
im = Image.open(file_path).convert('L') # 读入一张灰度图的图片
im = np.array(im, dtype='float32') # 将其转换为一个矩阵
print(im.shape[0], im.shape[1])
plt.imshow(im.astype('uint8'), cmap='gray') # 可视化图片
plt.title('原图')
plt.show()
im = torch.from_numpy(im.reshape((1, 1, im.shape[0], im.shape[1])))
conv1 = nn.Conv2d(1, 1, 3, bias=False) # 定义卷积
sobel_kernel = np.array([[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]], dtype='float32') # 定义轮廓检测算子
sobel_kernel = sobel_kernel.reshape((1, 1, 3, 3)) # 适配卷积的输入输出
conv1.weight.data = torch.from_numpy(sobel_kernel) # 给卷积的 kernel 赋值
edge1 = conv1(Variable(im)) # 作用在图片上
x = edge1.data.squeeze().numpy()
print(x.shape) # 输出大小
plt.imshow(x, cmap='gray')
plt.title('边缘检测')
plt.show()
运行结果:
只显示彩色原图,显示到处理过后的图片时就报错,不知道为啥,还没有解决。
总结
本次作业的心得体会,重点谈谈卷积能够提取特征的原理。
本次作业主要实现了卷积的应用,更加深刻的了解了卷积等一系列的概念,以及如何实现图像的边缘检测和锐化和模糊。调整卷积核参数,可以通过改变卷积核的步长,改变padding等主要是对cv2函数进行改变使图片发生变化。这次实验也让我对卷积的理解更为深入了,也知道如何利用不同的卷积核达到不同的处理图像效果。
卷积能够提取特征的原理:特征是数字图像映射到计算机处理的矩阵,而每个矩阵的数值就是一个特征点,由一幅图像组成的整个特征矩阵就是一个特征图,每输入网络的点针对神经网络而言都是一个特征,不同维度的特征就是不同维度的特征向量。故卷积、CNN并不是完全说是提取特征,而是对特征的一种处理或者说是转变,所以卷积和卷积神经网络不过是针对图像方面的特征,处理起来更符合能达到预期结果。
卷积神经网络CNN实际上是通过带有label的训练数据来train由一个个特征判断系统中的运算法则,对于图像特征提取而言,系统的运算法则可能比较复杂,通过卷积的方式将复杂判断系统的运算法则变为一个个易于计算的卷积核,从而更方便地提取图像特征,完成对图像的分析与处理。
参考
彻底理解卷积
CNN基础知识——卷积(Convolution)、填充(Padding)、步长(Stride)
感受野
卷积神经网络提取特征原理