本文从原理及代码方面讲解图像梯度与边缘检测
梯度是数学中的常用术语,其本质是一个向量,代表的是,函数在某个位置导数取得最大值,即函数的变化速度最大。一般情况下,我们用倒三角表示每个函数的梯度。对于一个线性函数来说,梯度就是其导数,可以可拓展来细细想想,这里就不多说。
图像的梯度自然是与梯度有联系的,但是两个并不是一个概念的。对于图像来说,将其梯度视为一个二维的离散函数,或者说白一点就是视为一个二维数组,每个位置有一个值,从(0,0)开始遍历,利用所有值从头至尾绘制一个函数图像,而图像梯度就是利用这样一幅图像求的。
假设离散函数图像如下。
那么梯度图线也很容易得出。
图像梯度主要应用在边缘检测上,而图像的边缘检测应用非常广,一般的目标识别包括一些其他功能都需要用到边缘检测。这里解释一下为什么边缘检测会需要图像梯度,用一张图来举例子。
这样一幅图,为什么能说,我们一眼能看出这个熊的边缘,其实并不是因为其他景象模糊(当然也有一定关系)但是本质上是因为熊的边缘像素点到周围环境的像素点变化很大,突然之间的大变化,就体现出了边缘。再者说,为什么我们头发和脸很容易区别而皮肤黑一些的朋友却可能在很远的地方将皮肤看成头发,也是如此。虽然说是很直观的感受,但是这里的原理却很有意思。一旦变化很大,那么图像的梯度图在这里就会出现很大的波峰。所以可以利用梯度图求出边缘。
Sobel算子和Scharr算子是OpenCV提供常用与计算边缘的算子。
Sobel算子是高斯模糊与微分操作的结合体,因此它的抗噪声能力非常好,同时我们可以设置求导的方向,卷积核的大小等,当设置卷积核的大小为-1时,那么就会使用Scharr算子。在小图时,Scharr滤波的效果更好。3x3的Scharr滤波器卷积核如下:
拉普拉斯算子(Laplacian算子)
拉普拉斯算子可以使用二阶导数的形式定义,可假设其离散实现类似于二阶Sobel导数,其实OpenCV在调用拉普拉斯算子时,本质也是调用着Sobel算子。其公式如下:
这里注意算子的使用,卷积的过程,都是可以自己写出来的,因此可以在调用API用利用filter2D做卷积计算,进行对比效果。
#图像梯度 可以使用filter2D 对比。
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
#记载图像
def load_iamge():
src=cv.imread('num.jpg')
src=cv.resize(src,(512,512))
return src
#sobel算子和Scharr算子
def sobi_charr_operator(image):
#1 0 表示只求x方向
x=cv.Scharr(image,cv.CV_64F,1,0)
x=cv.convertScaleAbs(x)
#1 0表示只求y方向
y=cv.Scharr(image,cv.CV_64F,0,1)#,ksize=3
y=cv.convertScaleAbs(y)
#1 1表示求xy方向即二阶导 还有另一种方法,加权求和
#x_y=cv.Sobel(image,cv.CV_64F,1,1,ksize=3)
x_y=cv.addWeighted(x,0.5,y,0.5,0)
return x,y,x_y
x,y,x_y=sobi_charr_operator(load_iamge())
cv.imshow('x',x)
cv.imshow('y',y)
cv.imshow('x_y',x_y)
cv.waitKey(0)
cv.destroyAllWindows()
效果如下:
#图像梯度 可以使用filter2D 对比。
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
#记载图像
def load_iamge():
src=cv.imread('num.jpg')
src=cv.resize(src,(512,512))
return src
#sobel算子和Scharr算子
def sobi_charr_operator(image):
#1 0 表示只求x方向
x=cv.Sobel(image,cv.CV_64F,1,0)
x=cv.convertScaleAbs(x)
#1 0表示只求y方向
y=cv.Sobel(image,cv.CV_64F,0,1)#,ksize=3
y=cv.convertScaleAbs(y)
#1 1表示求xy方向即二阶导 还有另一种方法,加权求和
#x_y=cv.Sobel(image,cv.CV_64F,1,1,ksize=3)
x_y=cv.addWeighted(x,0.5,y,0.5,0)
return x,y,x_y
x,y,x_y=sobi_charr_operator(load_iamge())
cv.imshow('x',x)
cv.imshow('y',y)
cv.imshow('x_y',x_y)
cv.waitKey(0)
cv.destroyAllWindows()
作对比可以明显发现,这种小图,Scharr算子的处理确实要好于Sobel算子
#图像梯度 可以使用filter2D 对比。
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
#记载图像
def load_iamge():
src=cv.imread('num.jpg')
src=cv.resize(src,(512,512))
return src
#laplacian算子
def laplacian_operator(image):
image=cv.Laplacian(image,cv.CV_64F)
image=cv.convertScaleAbs(image)
cv.imshow('input_1',image)
cv.waitKey(0)
cv.destroyAllWindows()
laplacian_operator(load_image())
cv.Scharr()方法
cv.Sobel()方法
cv.convertScaleAbs()方法
cv.addWeighted()
cv.Laplacian()
Canny边缘检于在1986年提出来,但是一直沿用至今,它拥有之前讲解到的方法没有考虑到的优点。
非极大值抑制(NMS)
可以去这里细细了解。
Canny边缘检测大概分为几步。
这里注意高低阈值处理其实可以简单的解释。
如果一个像素的梯度大于上限值,则被认为是边缘像素,保留。
如果小于下限阈值,则被抛弃。
那么如果该点的梯度位于两者之间,则当其与高于上限值的像素点连接时我们才保留,否则删除。
#Canny 边缘检测
import cv2 as cv
import numpy as np
#加载图像
def load_image():
src=cv.imread('con_1.jpg')
src=cv.resize(src,(512,512))
cv.imshow('src',src)
return src
#求边缘
def edge_demo(image):
#模糊
gau=cv.GaussianBlur(image,(3,3),0)
#灰度图
grey=cv.cvtColor(gau,cv.COLOR_BGR2GRAY)
#求图像梯度 cannyAPi要求不能为浮点数
x=cv.Sobel(grey,cv.CV_16SC1,1,0)
y=cv.Sobel(grey,cv.CV_16SC1,0,1)
#canny边缘检测
edge=cv.Canny(x,y,50,150)
cv.imshow('edge',edge)
dst=cv.bitwise_and(image,image,mask=edge)
cv.imshow('dst',dst)
cv.waitKey(0)
cv.destroyAllWindows()
edge_demo(load_image())
结果如下:
cv.Canny(x,y,50,150)
有几个需要注意的点
今天的内容就到这里,边缘检测属于必须要掌握的重要技能,因此不清楚的还需要去网上查询其他的资料。