边缘是像素值发生跃迁的位置,是图像的显著特征之一,在图像特征提取,对象检测,模式识别等方面都有重要的作用。
人眼如何识别图像边缘?
比如有一幅图,图里面有一条线,左边很亮,右边很暗,那人眼就很容易识别这条线作为边缘.也就是**像素的灰度值快速变化的地方**.
sobel算子对图像求一阶导数。一阶导数越大,说明像素在该方向的变化越大,边缘信号越强。
因为图像的灰度值都是离散的数字, sobel算子采用离散差分算子计算图像像素点亮度值的近似梯度.
图像是二维的,即沿着宽度/高度两个方向.
我们使用两个卷积核对原图像进行处理:
- 水平方向
- 垂直方向
这样的话,我们就得到了两个新的矩阵,分别反映了每一点像素在水平方向上的亮度变化情况和在垂直方向上的亮度变换情况.
**综合考虑这两个方向的变化**,我们使用以下公式反映某个像素的梯度变化情况.
有时候为了简单起见,也直接用绝对值相加替代
# 索贝尔算子.
import cv2
import numpy as np
#导入图片
img = cv2.imread('./chess.png')#
# x轴方向, 获取的是垂直边缘
dx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=5)
dy = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=5)
# 可利用numpy的加法, 直接整合两张图片
# dst = dx + dy
# 也可利用opencv的加法
dst = cv2.add(dx, dy)
cv2.imshow('dx', np.hstack((dx, dy, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
- 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')#
# x轴方向, 获取的是垂直边缘
dx = cv2.Scharr(img, cv2.CV_64F, 1, 0)
# y轴方向, 获取的是水平边缘
dy = cv2.Scharr(img, cv2.CV_64F, 0, 1)
# 可利用numpy的加法, 直接整合两张图片
# dst = dx + dy
# 也可利用opencv的加法
dst = cv2.add(dx, dy)
cv2.imshow('dx', np.hstack((dx, dy, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
索贝尔算子是模拟一阶求导,导数越大的地方说明变换越剧烈,越有可能是边缘.
那如果继续对f'(t)求导呢?
可以发现"边缘处"的二阶导数=0, 我们可以利用这一特性去寻找图像的边缘. **注意有一个问题,二阶求导为0的位置也可能是无意义的位置**.
- **拉普拉斯算子推导过程**
- 以x方向求解为例:
一阶差分:f'(x) = f(x) - f(x - 1)
二阶差分:f''(x) = f'(x+1) - f'(x) = (f(x + 1) - f(x)) - (f(x) - f(x - 1))
化简后:f''(x) = f(x - 1) - 2 f(x)) + f(x + 1)
同理可得: f''(y) = f(y - 1) - 2 f(y)) + f(y + 1)
把x,y方向的梯度叠加在一起.
$f''(x,y) = f''_x(x,y) + f''_y(x,y)$
$f''(x,y) = f(x - 1, y) - 2 f(x,y)) + f(x + 1, y) + f(x, y - 1) - 2 f(x,y)) + f(x,y + 1)$
$f''(x,y) = f(x - 1, y) + f(x + 1, y) + f(x, y - 1) + f(x,y + 1) - 4 f(x,y)) $
这个等式可以用矩阵写成:
这样就得到了拉普拉斯算子的卷积核即卷积模板.
- 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('dx', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
一般变为灰度图,然后找边缘
*Canny 边缘检测算法* 是 John F. Canny 于 1986年开发出来的一个多级边缘检测算法,也被很多人认为是边缘检测的 *最优算法*, 最优边缘检测的三个主要评价标准是:
> - **低错误率:** 标识出尽可能多的实际边缘,同时尽可能的减少噪声产生的误报。
> - **高定位性:** 标识出的边缘要与图像中的实际边缘尽可能接近。
> - **最小响应:** 图像中的边缘只能标识一次。
- Canny边缘检测的一般步骤
- 在获取了梯度和方向后, 遍历图像, 去除所有不是边界的点.
- 实现方法: 逐个遍历像素点, 判断当前像素点是否是周围像素点中具有相同方向梯度的最大值.
- 下图中, 点A,B,C具有相同的方向, 梯度方向垂直于边缘.
- 判断点A是否为A,B,C中的局部最大值, 如果是, 保留该点;否则,它被抑制(归零)
- 更形象的例子:
Canny(img, minVal, maxVal, ...)
# Canny
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('lena', np.hstack((lena1, lena2)))
cv2.waitKey(0)
cv2.destroyAllWindows()