OpenCV Tutorials 06 - Canny边缘检测和图像金字塔

1,909 阅读8分钟

Canny边缘检测和图像金字塔

一、Canny边缘检测

首先我们要了解Canny边缘检测的基本概念和 cv.Canny()函数。其用法为:

  • cv.Canny( image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]] ) -> edges
  • cv.Canny( dx, dy, threshold1, threshold2[, edges[, L2gradient]] ) -> edges

1. 基本概念

Canny 边缘检测算法是一种流行的边缘检测算法,由 John f发明。Canny基于以下几个步骤:

  1. 这是一个多流程的算法,并且我们会执行每一个流程

  2. 减噪:

    • 由于边缘检测容易受到图像中的噪声影响,第一步是用5x5高斯滤波器去除图像中的噪声。之前已经提及滤波器的用法。
  3. 寻找图像的灰度梯度:

    • 接下来在水平和垂直方向上使用 Sobel 算子对平滑后的图像进行滤波,以获得水平方向 (Gx) 和垂直方向 (Gy) 的一阶导数。 从这两张图像中,我们可以找到每个像素的边缘梯度和方向,如下所示:

      EdgeGradient(G)=Gx2+Gy2EdgeGradient (G)=\sqrt{G_{x}^{2}+G_{y}^{2}}

       Angle (θ)=tan1(GyGx) \text { Angle }(\theta)=\tan ^{-1}\left(\frac{G_{y}}{G_{x}}\right)

      梯度方向始终垂直于边缘。 它被舍入到代表垂直、水平和两个对角线方向的四个角度之一。

  4. 非极大值的抑制

    • 在得到梯度大小和方向之后,对图像进行全面扫描,以去除可能不构成边缘的任何不需要的像素。对于这一点,在每个像素,像素检查是否是一个局部最大值在其邻近的方向上的梯度。查看下面的图片:

      nms.jpg A 点在边上(垂直方向)。梯度方向与边缘垂直。点 b 和点 c 在梯度方向上。所以点 a 与点 b 和点 c 一起检查,看它是否形成了一个局部最大值。如果是这样,则考虑下一阶段,否则,它将被抑制(置为零)。简而言之,你得到的结果是一个二值图像的“薄边缘”

  5. 滞后阈值(双边阈值)

    • 这个阶段决定了哪些边是真正的边缘,哪些不是。为此,我们需要两个阈值,minVal 和 maxVal。任何灰度梯度大于 maxVal 的边都肯定是边,而那些低于 minVal 的边肯定是非边,所以要丢弃。那些处于这两个阈值之间的是分类边缘或非边缘基于他们的连通性。如果它们连接到“确定边缘”像素,它们就被认为是边缘的一部分。否则,它们也会被丢弃。请看下面的图片:

      hysteresis.jpg

      边缘 A 高于 maxVal,因此被认为是“确定边缘”。 虽然边 C 低于 maxVal,但它连接到边 A,因此也被视为有效边,我们得到了完整的曲线。 但是边B,虽然在minVal之上,并且和边C在同一个区域,但是它没有连接到任何“确定边”,所以它被丢弃了。 因此,我们必须相应地选择 minVal 和 maxVal 以获得正确的结果,这一点非常重要。也就是说即使大于minVal也不一定是边缘,只有大于maxVal或者与大于maxVal的点相连(但梯度值要大于minVal)才是边缘而被保留

      这个阶段也去除了小像素噪声,得到的边缘是(连续)长线。

2. OpenCV中的边缘检测

OpenCV 将上述所有内容放在单个函数 cv.Canny() 中。 我们将看到如何使用它。 第一个参数是我们的输入图像。 第二个和第三个参数分别是我们的 minVal 和 maxVal。 第四个参数是aperture_size: 它是用于定义图像梯度的 Sobel 核的大小的参数, 默认值为 3。最后一个参数是 L2gradient,它指定了求梯度幅度的方程。 如果为True,则使用上述更准确的方程,否则使用此函数:Edge_Gradient(G)=|Gx| + |Gy| 来代替上面的边缘梯度求解的欧氏距离。 默认情况下,它是 False(计算速度更快)。

import cv2 as cv
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
def cv_show(name, img):
    cv.imshow(name, img)
    cv.waitKey(0)
    cv.destroyAllWindows()
def compare(imgs):
  #  for i in range(len(imgs)):
 #       imgs[i][:,-3:-1,:] = [255,255,255]
    res = np.hstack(imgs)
    cv_show('Compare', res)
# 边缘检测输入灰度图
img = cv.imread('xy.png',0)

# 滞后阈值(双边)的上下限设置的越小则保留的细节就越多
res = cv.Canny(img, 10, 50)
compare([img, res])

Canny边缘检测.PNG

# 尝试一下对视频进行边缘检测
cap = cv.VideoCapture('betterme.mp4')

if not cap.isOpened():
    exit()
    
while 1:
    ret, frame = cap.read()
    if ret == None:
        break
    gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
    res = cv.Canny(gray, 20, 60)
    cv.imshow('Canny_Video', res)
    if cv.waitKey(1) & 0xFF == 27:
        break
cv.destroyAllWindows()
cap.release()

视频CannyPNG.PNG

二、图像金字塔

本小节主要包括图像金字塔的基础概念,以及两个函数 cv.pyrUp(), cv.pyrDown()。用法如下

  1. cv.pyrUp( src[, dst[, dstsize[, borderType]]] ) -> dst
  2. cv.pyrDown( src[, dst[, dstsize[, borderType]]] ) -> dst

1. 基本概念

通常,我们曾经使用恒定大小的图像。 但在某些情况下,我们需要处理不同分辨率的(同一)图像。 例如,在搜索图像中的某些内容时,例如人脸,我们不确定该对象在所述图像中的大小。 在这种情况下,我们需要创建一组具有不同分辨率的相同图像并在所有图像中搜索对象。 这些具有不同分辨率的图像集称为图像金字塔(因为当它们被保存在一个堆栈中,最高分辨率的图像在底部,最低分辨率的图像在顶部时,它看起来像一个金字塔,但是整个金字塔是倒过来的,使用pyrDown会降低分辨度)。

图像金字塔有两种: 1)高斯金字塔金字塔和2)拉普拉斯金字塔

高斯金字塔中的较高级别(低分辨率)是通过删除较低级别(较高分辨率)图像中的连续行和列而形成的。 然后,较高级别的每个像素由具有高斯权重的底层 5 个像素的贡献形成。 通过这样做,M×N图像变成M/2×N/2图像。 所以面积减少到原来面积的四分之一。 它被称为八度。 随着我们前往金字塔的上层(即分辨率降低),每层像素点的个数会越来越少。 同样,在扩展时,每个级别的面积变成 4 倍。 我们可以使用 cv.pyrDown() 和 cv.pyrUp() 函数找到高斯金字塔。

img = cv.imread('lena.png')

layer_8 = cv.pyrDown(img)
layer_4 = cv.pyrDown(layer_8)
layer_2 = cv.pyrDown(layer_4)
layer_1 = cv.pyrDown(layer_2)

cv.imshow('Origin',img)
cv.imshow('layer_8',layer_8)
cv.imshow('layer_4',layer_4)
cv.imshow('layer_2',layer_2)
cv.imshow('layer_1',layer_1)

cv.waitKey(0)
cv.destroyAllWindows()

图像金字塔.PNG

# 接下来使用图像方法,逆用


layer_2 = cv.pyrUp(layer_1)
layer_4 = cv.pyrUp(layer_2)
layer_8 = cv.pyrUp(layer_4)
layer_16 = cv.pyrUp(layer_8)

cv.imshow('Origin',img)
cv.imshow('layer_16',layer_16)
cv.imshow('layer_8',layer_8)
cv.imshow('layer_4',layer_4)
cv.imshow('layer_2',layer_2)
cv.imshow('layer_1',layer_1)

cv.waitKey(0)
cv.destroyAllWindows()

图像金字塔放大.PNG

可以看出,一旦降低了图像分辨率,再使用pyrUp也不能恢复原有的清晰度了

拉普拉斯金字塔是由高斯金字塔形成的。 没有专门的功能。 拉普拉斯金字塔图像仅类似于边缘图像。 它的大部分元素都是零。 它们用于图像压缩。 拉普拉斯金字塔中的一个层是由高斯金字塔中的该层级与高斯金字塔中其上层的扩展版本(pryUp)之间的差。 拉普拉斯水平的三个水平将如下所示(调整对比度以增强内容):

laplacian_pyramid[i] = Gauss[i] - pryUp(Gauss[i-1] ),由这个式子可以得出,拉普拉斯金字塔的层数比高斯金字塔层数少一

# 结果演示
img = cv.imread('lena.png',0)

layer_8 = cv.pyrDown(img)
layer_4 = cv.pyrDown(layer_8)
layer_2 = cv.pyrDown(layer_4)
layer_1 = cv.pyrDown(layer_2)

lap_8 = layer_8 - cv.pyrUp(layer_4)
lap_4 = layer_4 - cv.pyrUp(layer_2)
lap_2 = layer_2 - cv.pyrUp(layer_1)


cv.imshow('lap_8',lap_8)
cv.imshow('lap_4',lap_4)
cv.imshow('lap_2',lap_2)

cv.waitKey(0)
cv.destroyAllWindows()

拉普拉斯金字塔.PNG

三、使用图像金字塔来进行图像混合

金字塔的一种应用是图像混合。 例如,在图像拼接中,您需要将两张图像堆叠在一起,但由于图像之间的不连续性,它可能看起来不太好。 在这种情况下,使用 Pyramids 的图像混合可以让您无缝混合,而不会在图像中留下太多数据。 一个典型的例子是混合两种水果,橙子和苹果。 现在查看结果本身以了解我在说什么:

orapple.jpg

通过上面的流程小结,我们可以总结出融合步骤:

  1. 加载苹果和橙子的两个图像
  2. 找到苹果和橙子的高斯金字塔(在这个特定的例子中,层级数是6)
  3. 从高斯金字塔中,找到它们的拉普拉斯金字塔
  4. 将苹果的左半部分和橙子的右半部分混合
  5. 拉普拉斯金字塔的层次最后从这个联合图像金字塔中,重建原始图像。
A = cv.imread('xy.png')
B = cv.imread('lena.png')
A = cv.resize(A, B.shape[:2])
# generate Gaussian pyramid for A
G = A.copy()
gpA = [G]
for i in range(6):
    # 更新迭代的图片分辨度层级
    G = cv.pyrDown(G)
    gpA.append(G)
# generate Gaussian pyramid for B
G = B.copy()
gpB = [G]
for i in range(6):
    G = cv.pyrDown(G)
    gpB.append(G)
    
    
# generate Laplacian Pyramid for A
lpA = [gpA[5]]
for i in range(5,0,-1):
    GE = cv.pyrUp(gpA[i])
    L = cv.subtract(gpA[i-1],GE)
    lpA.append(L)
# generate Laplacian Pyramid for B
lpB = [gpB[5]]
for i in range(5,0,-1):
    GE = cv.pyrUp(gpB[i])
    L = cv.subtract(gpB[i-1],GE)
    lpB.append(L)
    
    
# Now add left and right halves of images in each level
# 这一步的结果是得到融合结果图的拉普拉斯金字塔层级
LS = []
for la,lb in zip(lpA,lpB):
    rows,cols,dpt = la.shape
    ls = np.hstack((la[:,0:cols//2], lb[:,cols//2:]))
    LS.append(ls)
# 由拉普拉斯金字塔层级构造出最终的结果    
# now reconstruct
#  从最底层开始,不断的放大后与上层相加得到最后结果
ls_ = LS[0]
for i in range(1,6):
    ls_ = cv.pyrUp(ls_)
    ls_ = cv.add(ls_, LS[i])

    
# 直接拼接得到的结果
real = np.hstack((A[:,:cols//2],B[:,cols//2:]))

cv.imwrite('Pyramid_blending2.jpg',ls_)
cv.imwrite('Direct_blending.jpg',real)

compare([real, ls_])

图像融合.PNG

1. 图像融合的补充知识:

pages.cs.wisc.edu/~csverma/CS…