04- 图像卷积及图片的模糊和边缘检测 (OpenCV系列) (机器视觉)

知识重点

  • padding指的就是填充的0的圈数
  • 重构图片大小:  img = cv2.resize(img, dsize=(300, 300))
  • 模糊操作:  dst = cv2.filter2D(img, -1, kernel)
kernel = np.ones((5, 5), np.float32)/ 25
dst = cv2.filter2D(img, -1, kernel)  # 卷积操作

模糊操作:

  • 方盒滤波:  dst = cv2.boxFilter(img, -1, (5, 5), normalize = True)   # normalize = True时, a = 1 / (W * H) 滤波器的宽高, 且此时方盒滤波等价于均值滤波 .
  • 均值滤波:  dst = cv2.blur(img, (5, 5))  # blur 模糊不清
  • 高斯滤波:  dst = cv2.GaussianBlur(img, (25, 25), sigmaX = 0)   # 高斯滤波的核心思想是让临近的像素具有更高的重要度. 对周围像素计算加权平均值, 较近的像素具有较大的权重值.
    • 高斯滤波适用于处理有噪点的文件, 降噪.
  • 中值滤波:  dst = cv2.medianBlur(img, 9)    # 取其中的中间值(即中位数)作为卷积后的结果值, 适用于处理有噪声的文件 .
  • 双边滤波对于图像的边缘信息能过更好的保存。其原理为一个与空间距离相关的高斯函数与一个灰度距离相关的高斯函数相乘双边滤波既利用了位置信息又利用了像素信息来定义滤波窗口的权重而高斯滤波只用了位置信息 。
    • dst = cv2.bilateralFilter(img, 7, sigmaColor = 50, sigmaSpace = 20)

边缘检测:

  • 索贝尔算子 : dx = cv2.Sobel(img, -1, dx = 1, dy = 0, ksize = 1)    # sobel算子对图像求一阶导数。一阶导数越大,说明像素在该方向的变化越大,边缘信号越强。
  • 沙尔算子:  dx = cv2.Scharr(img, cv2.CV_64F, dx = 1, dy = 0)    # Scharr算子和Sobel很类似, 只不过使用不同的kernel值, 放大了像素变换的情况:
  • 拉普拉斯算子: 可以同时求两个方向的边缘, 对噪音敏感, 需要先进行去噪再调用拉普拉斯。
    • dst = cv2.Laplacian(img, -1, ksize = 3)    # 适用于处理有噪声的文件
  • Canny: 被很多人认为是边缘检测的 最优算法,  阈值越小, 细节越丰富。
    • lena2 = cv2.Canny(img, 64, 128)


5. 滤波器

5.1 卷积

5.1.1 什么是图片卷积

图像卷积就是卷积核在图像上按行滑动遍历像素时不断的相乘求和的过程 .

                      

5.1.2 步长

步长就是卷积核在图像上移动的步幅. 上面例子中卷积核每次移动一个像素步长的结果, 如果将这个步长修改为2, 结果如何?

为了充分扫描图片, 步长一般设为1.

5.1.3 padding

从上面例子中我们发现, 卷积之后图片的长宽会变小. 如果要保持图片大小不变, 我们需要在图片周围填充0. padding指的就是填充的0的圈数.

我们可以通过公式计算出需要填充的0的圈数.

        

如果要保持卷积之后图片大小不变, 可以得出等式: \small (N + 2P - F + 1) = N从而可以推导出: \small P = \frac{F -1}{2}

5.1.4 卷积核的大小

图片卷积中, 卷积核一般为奇数, 比如 3 * 3, 5 * 5, 7 * 7.为什么一般是奇数呢, 出于以下两个方面的考虑:

  1. 根据上面padding的计算公式, 如果要保持图片大小不变, 采用偶数卷积核的话, 比如4 * 4, 将会出现填充1.5圈零的情况.

  2. 奇数维度的过滤器有中心,便于指出过滤器的位置, 即OpenCV卷积中的锚点.

5.1.5 卷积案例

  • filter2D(src, ddepth, kernel[, dst[, anchor[, delta[, borderType]]]])

    • ddepth是卷积之后图片的位深, 即卷积之后图片的数据类型, 一般设为-1, 表示和原图类型一致.

    • kernel是卷积核大小, 用元组或者ndarray表示, 要求数据类型必须是float型.

    • anchor 锚点, 即卷积核的中心点, 是可选参数, 默认是(-1,-1)

    • delta 可选参数, 表示卷积之后额外加的一个值, 相当于线性方程中的偏差, 默认是0.

    • borderType 边界类型.一般不设.

# opencv 中的卷积操作
import cv2
import numpy as np

img = cv2.imread('./dog.jpeg')
# kernel 必须是float类型
kernel = np.ones((5, 5), np.float32)/ 25

# 卷积操作
dst = cv2.filter2D(img, -1, kernel)

cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
  • 卷积核为 5*5, 将所有数据均匀分布, 以周边25个元素分别取 1/25 然后合并来得到中间值.

浮雕效果操作

# opencv 中的卷积操作
import cv2
import numpy as np

gray_img = cv2.imread('./dog.jpeg')
# gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# kernel 必须是float类型
# kernel = np.ones((5, 5), np.float32)/ 25
kernel = np.array([[-4, 1, 0], [-1, 1, 1], [1, 1, 1]])
# 卷积操作
dst = cv2.filter2D(gray_img, -1, kernel)

cv2.imshow('gray_img', np.hstack((gray_img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()

锐化效果操作

# opencv 中的卷积操作
import cv2
import numpy as np

img = cv2.imread('./dog.jpeg')
# kernel 必须是float类型
# kernel = np.ones((5, 5), np.float32)/ 25
kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
# 卷积操作
dst = cv2.filter2D(img, -1, kernel)

cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()

5.2 方盒滤波与均值滤波

  • boxFilter(src, ddepth, ksize[, dst[, anchor[, normalize[, borderType]]]])  方盒滤波.

    • 方盒滤波的卷积核的形式如下:

    • normalize = True时, a = 1 / (W * H) 滤波器的宽高

    • normalize = False是. a = 1

    • 一般情况我们都使用normalize = True的情况. 这时 方盒滤波 等价于 均值滤波

# opencv 中的卷积操作
import cv2
import numpy as np

img = cv2.imread('./dog.jpeg')
# 不用手动创建卷积核,只需要告诉方盒滤波,卷积核的大小
dst = cv2.boxFilter(img, -1, (5, 5), normalize = True)  # destination 目标

cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()

  •  blur(src, ksize[, dst[, anchor[, borderType]]]) 均值滤波.
# 均值滤波
import cv2
import numpy as np

img = cv2.imread('./dog.jpeg')
# 不用手动创建卷积核,只需要告诉方盒滤波,卷积核的大小
# 均值滤波没有 位深这个操作
dst = cv2.blur(img, (5, 5))  # blur 模糊不清

cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()

5.3 高斯滤波

高斯滤波的核心思想是让临近的像素具有更高的重要度. 对周围像素计算加权平均值, 较近的像素具有较大的权重值.要理解高斯滤波首先要知道什么是高斯函数.高斯函数在是符合高斯分布(也叫正态分布)的数据的概率密度函数.画出来长这样子:

高斯函数的特点是以x轴某一点(这一点称为均值)为对称轴, 越靠近中心数据发生的概率越高, 最终形成一个两边平缓, 中间陡峭的钟型(有的地方也叫帽子)图形.

高斯函数的一般形式为:

高斯滤波就是使用符合高斯分布的卷积核对图片进行卷积操作. 所以高斯滤波的重点就是如何计算符合高斯分布的卷积核, 即高斯模板.

假定中心点的坐标是(0,0),那么取距离它最近的8个点坐标,为了计算,需要设定σ的值。假定σ=1.5,则模糊半径为1的高斯模板就算如下:

我们可以观察到越靠近中心, 数值越大, 越边缘的数值越小.符合高斯分布的特点.

通过高斯函数计算出来的是概率密度函数, 所以我们还要确保这九个点加起来为1,这9个点的权重总和等于0.4787147,因此上面9个值还要分别除以0.4787147,得到最终的高斯模板。

注: 有些整数高斯模板是在归一化后的高斯模板的基础上每个数除上左上角的值, 然后取整.

 有了卷积核, 计算高斯滤波.假设现有9个像素点,灰度值(0-255)的高斯滤波计算如下:

将这9个值加起来,就是中心点的高斯滤波的值。对所有点重复这个过程,就得到了高斯模糊后的图像。

  • GaussianBlur(src, ksize, sigmaX[, dst[, sigmaY[, borderType]]])

    • kernel 高斯核的大小.

    • sigmaX, X轴的标准差

    • sigmaY, Y轴的标准差, 默认为0, 这时sigmaY = sigmaX

    • 如果没有指定sigma值, 会分别从ksize的宽度和高度中计算sigma.

  • 选择不同的sigma值会得到不同的平滑效果, sigma越大, 平滑效果越明显.

import cv2
import numpy as np
img = cv2.imread('./lena.png')
print(img.shape)
img = cv2.resize(img, dsize=(300, 300))

dst = cv2.GaussianBlur(img, (25, 25), sigmaX = 0)
# 可以不指定 sigma,, 指定为0, 使用ksize 指定
dst1 = cv2.GaussianBlur(img, (25, 25), sigmaX = 1)
dst2 = cv2.GaussianBlur(img, (25, 25), sigmaX = 3)
dst3 = cv2.GaussianBlur(img, (25, 25), sigmaX = 5)

cv2.imshow('img', np.hstack((img, dst, dst1, dst2, dst3)))
cv2.waitKey(0)
cv2.destroyAllWindows()
  •  当sigma 为0时, 自动模糊 .

  •  没有指定sigma时, ksize越大, 平滑效果越明显
import cv2
import numpy as np
img = cv2.imread('./lena.png')
print(img.shape)
img = cv2.resize(img, dsize=(300, 300))

dst = cv2.GaussianBlur(img, (3, 3), sigmaX = 3)
# 可以不指定 sigma,, 指定为0, 使用ksize 指定
dst1 = cv2.GaussianBlur(img, (7, 7), sigmaX = 3)
dst2 = cv2.GaussianBlur(img, (15, 15), sigmaX = 3)
dst3 = cv2.GaussianBlur(img, (63, 63), sigmaX = 3)

cv2.imshow('img', np.hstack((img, dst, dst1, dst2, dst3)))
cv2.waitKey(0)
cv2.destroyAllWindows()

 高斯滤波实战

  • 适用于处理有噪声的文件, 减少噪点 .
import cv2
import numpy as np

img = cv2.imread('./gaussian.png')  # 适用于处理有噪声的文件
dst = cv2.GaussianBlur(img, (5,5), sigmaX = 1)
# 可以不指定 sigma,, 指定为0, 使用ksize 指定
dst = cv2.GaussianBlur(img, (25, 25), sigmaX = 0)

cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()

5.4 中值滤波

中值滤波原理非常简单, 假设有一个数组[1556789], 取其中的中间值(即中位数)作为卷积后的结果值即可. 中值滤波对胡椒噪音(也叫椒盐噪音)效果明显.

#  中值滤波
import cv2
import numpy as np

img = cv2.imread('./papper.png')  # 适用于处理有噪声的文件
dst = cv2.medianBlur(img, 9)     # 中值滤波

cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()

5.5 双边滤波

双边滤波对于图像的边缘信息能过更好的保存。其原理为一个与空间距离相关的高斯函数与一个灰度距离相关的高斯函数相乘

双边滤波本质上是高斯滤波, 双边滤波和高斯滤波不同的就是: 双边滤波既利用了位置信息又利用了像素信息来定义滤波窗口的权重。而高斯滤波只用了位置信息 。

两者权重系数相乘,得到最终的卷积模板。由于双边滤波需要每个中心点邻域的灰度信息来确定其系数,所以其速度与比一般的滤波慢很多,而且计算量增长速度为核大小的平方,双边滤波可以保留边缘, 同时可以对边缘内的区域进行平滑处理. 双边滤波的作用就相当于做了美颜。

bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]])

  • sigmaColor是计算像素信息使用的sigma

  • sigmaSpace是计算空间信息使用的sigma

import cv2
import numpy as np

img = cv2.imread('./lena.png')  # 适用于处理有噪声的文件
#  双边滤波
dst = cv2.bilateralFilter(img, 7, sigmaColor = 50, sigmaSpace = 20)

cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()

5.6 索贝尔(sobel)算子

边缘是像素值发生跃迁的位置,是图像的显著特征之一,在图像特征提取,对象检测,模式识别等方面都有重要的作用。比如有一幅图,图里面有一条线,左边很亮,右边很暗,那人眼就很容易识别这条线作为边缘.也就是像素的灰度值快速变化的地方.

sobel算子对图像求一阶导数一阶导数越大,说明像素在该方向的变化越大,边缘信号越强。因为图像的灰度值都是离散的数字, sobel算子采用离散差分算子计算图像像素点亮度值的近似梯度.  图像是二维的, 即沿着宽度/高度两个方向. 我们使用两个卷积核对原图像进行处理:

  • 水平方向

  • 垂直方向

这样的话,我们就得到了两个新的矩阵,分别反映了每一点像素在水平方向上的亮度变化情况和在垂直方向上的亮度变换情况.综合考虑这两个方向的变化,我们使用以下公式反映某个像素的梯度变化情况.

        \small G= \sqrt{G^2_x+G^2_y}

有时候为了简单起见,也直接用绝对值相加替代 \small G = |G_X| + |G_Y|.​

import cv2
import numpy as np

img = cv2.imread('./chess.png')  # 适用于处理有噪声的文件
img = cv2.resize(img, dsize=(450, 450))
# 计算X轴方向的梯度,只有垂直方向的边缘
dx = cv2.Sobel(img, -1, dx = 1, dy = 0, ksize = 1)
# 计算Y轴方向的梯度, 只有水平方向的边缘
dy = cv2.Sobel(img, -1, dx = 0, dy = 1, ksize = 5)
# 使用sobel 算子,别忘了把x, y 的梯度合并在一起
# dst = cv2.add(dx, dy)
dst = cv2.addWeighted(dx, 2, dy, 2, gamma = 1)  # 等同于cv2.add()

cv2.imshow('dx:', np.hstack((img, dx, dy, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()

5.7 沙尔(Scharr)算子

  • Scharr(src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]])

  • 内核大小为 3 时, 以上Sobel内核可能产生比较明显的误差(毕竟,Sobel算子只是求取了导数的近似值)。 为解决这一问题,OpenCV提供了 Scharr函数,但该函数仅作用于大小为3的内核。该函数的运算与Sobel函数一样快,但结果却更加精确.

  • Scharr算子和Sobel很类似, 只不过使用不同的kernel值, 放大了像素变换的情况:

  • Scharr算子只支持3 * 3 的kernel所以没有kernel参数了.

  • Scharr算子只能求x方向或y方向的边缘.

  • Sobel算子的ksize设为-1就是Scharr算子.

  • Scharr擅长寻找细小的边缘, 一般用的较少.

import cv2
import numpy as np

img = cv2.imread('./lena.png')  # 适用于处理有噪声的文件
img = cv2.resize(img, dsize=(450, 450))
# 计算X轴方向的梯度,只有垂直方向的边缘
dx = cv2.Scharr(img, cv2.CV_64F, dx = 1, dy = 0)
# 计算Y轴方向的梯度, 只有水平方向的边缘
dy = cv2.Scharr(img, cv2.CV_64F, dx = 0, dy = 1)
# 使用sobel 算子,别忘了把x, y 的梯度合并在一起
# dst = cv2.add(dx, dy)
dst = cv2.addWeighted(dx, 2, dy, 2, gamma = 1)

cv2.imshow('img:', img)
cv2.imshow('img', np.hstack((dx, dy, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()

5.8 拉普拉斯算子

索贝尔算子是模拟一阶求导,导数越大的地方说明变换越剧烈,越有可能是边缘.

那如果继续对f'(t)求导呢?

可以发现"边缘处"的二阶导数=0, 我们可以利用这一特性去寻找图像的边缘. 注意有一个问题,二阶求导为0的位置也可能是无意义的位置.

  • 拉普拉斯算子推导过程

    • 以x方向求解为例:  \small f''(x) = f(x - 1) - 2 f(x)) + f(x + 1)

    • 同理可得:   f''(y) = f(y - 1) - 2 f(y)) + f(y + 1)

    • 把x,y方向的梯度叠加在一起:

          \small f''(x,y) = \left[\begin{matrix}0 & 1 & 0\\1 & -4 & 1\\0 & 1 & 0\end{matrix}\right] \bigodot \left[\begin{matrix}f(x-1, y-1) & f(x, y-1) & f(x+1,y-1)\\f(x-1,y) & f(x,y) & f(x+1,y)\\f(x-1,y+1) & f(x,y+1) & f(x+1,y+1)\end{matrix}\right]

  • Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])

  • 可以同时求两个方向的边缘 .

  • 对噪音敏感, 一般需要先进行去噪再调用拉普拉斯 .

import cv2
import numpy as np

img = cv2.imread('./chess.png')  # 适用于处理有噪声的文件
dst = cv2.Laplacian(img, -1, ksize = 3)

cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()

  •  lena 图片使用拉普拉斯算子 的结果

  • 中值滤波及拉普拉斯算子
import cv2
import numpy as np

img = cv2.imread('./papper.png')  # 适用于处理有噪声的文件
# 中值滤波
dst = cv2.medianBlur(img, 9)
lap = cv2.Laplacian(dst, -1, ksize = 3)  # 拉普拉斯算子

cv2.imshow('img', np.hstack((img, dst, lap)))
cv2.waitKey(0)
cv2.destroyAllWindows()

5.9 边缘检测Canny

Canny 边缘检测算法 是 John F. Canny 于 1986年开发出来的一个多级边缘检测算法,也被很多人认为是边缘检测的 最优算法, 最优边缘检测的三个主要评价标准是:

  • 低错误率: 标识出尽可能多的实际边缘,同时尽可能的减少噪声产生的误报。

  • 高定位性: 标识出的边缘要与图像中的实际边缘尽可能接近。

  • 最小响应: 图像中的边缘只能标识一次。

  • Canny边缘检测的一般步骤:

    • 去噪. 边缘检测容易受到噪声影响, 在进行边缘检测前通常需要先进行去噪, 一般用高斯滤波去除噪声.

    • 计算梯度: 对平滑后的图像采用sobel算子计算梯度和方向.

    • 非极大值抑制

      • 在获取了梯度和方向后, 遍历图像, 去除所有不是边界的点.

      • 实现方法: 逐个遍历像素点, 判断当前像素点是否是周围像素点中具有相同方向梯度的最大值.

      • 下图中, 点A,B,C具有相同的方向, 梯度方向垂直于边缘.

      • 判断点A是否为A,B,C中的局部最大值, 如果是, 保留该点;否则,它被抑制(归零)

    • 滞后阈值

  • Canny(img, minVal, maxVal, ...)

import cv2
import numpy as np

img = cv2.imread('./lena.png')
# 阈值越小,细节越丰富
lena1 = cv2.Canny(img, 100, 200)
lena2 = cv2.Canny(img, 64, 128)

cv2.imshow('img', img)
cv2.imshow('lena', np.hstack((lena1, lena2)))
cv2.waitKey(0)
cv2.destroyAllWindows()

import cv2
import numpy as np

img = cv2.imread('./chess.png')
# 阈值越小,细节越丰富
lena1 = cv2.Canny(img, 100, 200)
lena2 = cv2.Canny(img, 64, 128)

cv2.imshow('img', img)
cv2.imshow('lena', np.hstack((lena1, lena2)))
cv2.waitKey(0)
cv2.destroyAllWindows()

  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值