复习图像梯度,发现有很多需要进一步理解的内容,重新整理一篇
目录
方向导数和梯度
参考:
数字图像的梯度概念(the gradient of the image)
首先介绍数学概念上的导数和梯度的概念,再引申到图像梯度上
方向导数
方向导数:函数在某一点沿某一方向的变换率
设函数 z = f ( x ) z=f(x) z=f(x)在点 P ( x ) P(x) P(x)的某一领域 U ( p ) U(p) U(p)内有定义,若极限
lim x → x 0 = f ( x ) − f ( x 0 ) x − x 0 \lim_{x \to x_{0}}=\frac{f(x)-f(x_{0})}{x-x_{0}} x→x0lim=x−x0f(x)−f(x0)
存在,则称函数 f f f在点 x 0 x_{0} x0处可导,并称该极限为函数 f f f在点 x 0 x_{0} x0处的一阶导数,记作 f ′ ( x 0 ) f'(x_{0}) f′(x0)
对于二元函数 z = f ( x , y ) z=f(x,y) z=f(x,y)而言,在定义域内的点 ( x 0 , y 0 ) (x_{0}, y_{0}) (x0,y0),固定 y 0 y_{0} y0,对 x 0 x_{0} x0增加增量 △ x \bigtriangleup x △x,那么可得到函数 z z z对于 x x x方向的偏导数:
∂ z ∂ x = lim △ x → 0 f ( x 0 + △ x , y 0 ) ) − f ( x 0 , y 0 ) △ x \frac{\partial z}{\partial x}=\lim_{\bigtriangleup x \to 0}\frac{f(x_{0}+\bigtriangleup x, y_{0}))-f(x_{0}, y_{0})}{\bigtriangleup x} ∂x∂z=△x→0lim△xf(x0+△x,y0))−f(x0,y0)
同样有对 y y y方向的偏导数:
∂ z ∂ y = lim △ y → 0 f ( x 0 , y 0 + △ y ) ) − f ( x 0 , y 0 ) △ y \frac{\partial z}{\partial y}=\lim_{\bigtriangleup y \to 0}\frac{f(x_{0}, y_{0}+\bigtriangleup y))-f(x_{0}, y_{0})}{\bigtriangleup y} ∂y∂z=△y→0lim△yf(x0,y0+△y))−f(x0,y0)
对于图像而言,通常将它作为离散二维信号进行处理,所以使用有限差分计算导数
对 x x x方向的偏导数:
∂ z ∂ x = f ( x 0 + 1 , y 0 ) − f ( x 0 , y 0 ) \frac{\partial z}{\partial x}=f(x_{0}+1,y_{0})-f(x_{0},y_{0}) ∂x∂z=f(x0+1,y0)−f(x0,y0)
对 y y y方向的偏导数:
∂ z ∂ y = f ( x 0 , y 0 + 1 ) − f ( x 0 , y 0 ) \frac{\partial z}{\partial y}=f(x_{0},y_{0}+1)-f(x_{0},y_{0}) ∂y∂z=f(x0,y0+1)−f(x0,y0)
有限差分
参考:
有3种计算差分方式:
- 向前差分(
forward difference
)- △ f ( x k ) = f ( x k + 1 ) − f ( x k ) \bigtriangleup f(x_{k})=f(x_{k+1})-f(x_{k}) △f(xk)=f(xk+1)−f(xk), △ \bigtriangleup △表示向前差分算子
- 向后差分(
backward difference
)- ▽ f ( x k ) = f ( x k ) − f ( x k − 1 ) \bigtriangledown f(x_{k})=f(x_{k})-f(x_{k-1}) ▽f(xk)=f(xk)−f(xk−1), ▽ \bigtriangledown ▽表示向后差分算子
- 中心差分(
central/centered difference
)- δ f ( x k ) = f ( x k + 1 ) − f ( x k − 1 ) 2 \delta f(x_{k})=\frac{f(x_{k+1})-f(x_{k-1})}{2} δf(xk)=2f(xk+1)−f(xk−1), δ \delta δ表示中心差分算子
梯度
数学梯度:是一个向量,由一组正交的方向导数组成,表示函数在该点处的方向导数沿着该方向取得最大值,即函数在该点处沿着该方向变化最快,变化最大。
设函数 f ( x , y ) f(x,y) f(x,y)在平面区域 D D D内有一阶连续偏导数,则对于每一个点 ( x , y ) ∈ D (x,y)\in D (x,y)∈D,都可得到一个向量
g r a d ( f ) = [ g x , g y ] T = [ ∂ f ∂ x ∂ f ∂ y ] = ∂ f ∂ x i ⃗ + ∂ f ∂ y j ⃗ grad(f)=[g_{x},g_{y}]^{T} =\begin{bmatrix} \frac{\partial f}{\partial x}\\ \frac{\partial f}{\partial y} \end{bmatrix} =\frac{\partial f}{\partial x}\vec{i}+\frac{\partial f}{\partial y}\vec{j} grad(f)=[gx,gy]T=[∂x∂f∂y∂f]=∂x∂fi+∂y∂fj
梯度的模:
∣ g r a d ( f ) ∣ = ( ∂ f ∂ x ) 2 + ( ∂ f ∂ y ) 2 \left | grad(f) \right |=\sqrt{(\frac{\partial f}{\partial x})^{2}+(\frac{\partial f}{\partial y})^{2}} ∣grad(f)∣=(∂x∂f)2+(∂y∂f)2
梯度方向:
θ = t a n − 1 ( g y g x ) \theta =tan^{-1}(\frac{g_{y}}{g_{x}}) θ=tan−1(gxgy)
对图像梯度而言,图像上亮度变化值大的地方(也就是图像边缘部分)表示有较大梯度值的地方
对 y y y轴方向求导:
∂ f ∂ y = [ − 1 + 1 ] ∗ A = 1 2 ⋅ [ − 1 0 + 1 ] ∗ A \frac{\partial f}{\partial y}=\begin{bmatrix} -1\\ +1 \end{bmatrix} \ast A =\frac{1}{2}\cdot \begin{bmatrix} -1\\ 0\\ +1 \end{bmatrix} \ast A ∂y∂f=[−1+1]∗A=21⋅⎣⎡−10+1⎦⎤∗A
对 x x x轴方向求导:
∂ f ∂ x = [ − 1 + 1 ] ∗ A = 1 2 ⋅ [ − 1 0 + 1 ] ∗ A \frac{\partial f}{\partial x}=\begin{bmatrix} -1 & +1 \end{bmatrix} \ast A =\frac{1}{2}\cdot \begin{bmatrix} -1 & 0 & +1 \end{bmatrix} \ast A ∂x∂f=[−1+1]∗A=21⋅[−10+1]∗A
python
实现
python
实现
我实现了一个用差分公式计算的梯度函数image_gradient
:
def image_gradient(f, **kwargs):
"""
图像梯度求导
:param f: 类数组
:return: 数组或数组列表,dtype=np.double
可选参数:axis - 空或整数或整数列表
"""
f = np.asanyarray(f, dtype=np.double)
N = f.ndim # number of dimensions
axes = kwargs.pop('axis', None)
if axes is None:
axes = tuple(range(N))
else:
axes = _nx.normalize_axis_tuple(axes, N)
len_axes = len(axes)
outvals = []
# create slice objects --- initially all are [:, :, ..., :]
slice1 = [slice(None)] * N
slice2 = [slice(None)] * N
slice3 = [slice(None)] * N
slice4 = [slice(None)] * N
otype = f.dtype
for axis in axes:
if f.shape[axis] < 2:
raise ValueError(
"Shape of array too small to calculate a numerical gradient, "
"at least 2 elements are required.")
# result allocation
out = np.empty_like(f, dtype=otype)
# Numerical differentiation: 2nd order interior
slice1[axis] = slice(1, -1)
slice2[axis] = slice(None, -2)
slice3[axis] = slice(1, -1)
slice4[axis] = slice(2, None)
# 1D equivalent -- out[0] = (f[1] - f[-1]) / 2
out[tuple(slice1)] = (f[tuple(slice4)] - f[tuple(slice2)]) / 2
slice1[axis] = 0
slice2[axis] = 1
slice3[axis] = 0
# 1D equivalent -- out[0] = (f[1] - f[0]) / (x[1] - x[0])
out[tuple(slice1)] = (f[tuple(slice2)] - f[tuple(slice3)])
slice1[axis] = -1
slice2[axis] = -1
slice3[axis] = -2
# 1D equivalent -- out[-1] = (f[-1] - f[-2]) / (x[-1] - x[-2])
out[tuple(slice1)] = (f[tuple(slice2)] - f[tuple(slice3)])
outvals.append(out)
# reset the slice object in this dimension to ":"
slice1[axis] = slice(None)
slice2[axis] = slice(None)
slice3[axis] = slice(None)
slice4[axis] = slice(None)
if len_axes == 1:
return outvals[0]
else:
return outvals
同样可以使用卷积实现计算图像梯度的功能,比如
对x
求导的卷积核:
[ 0 0 0 1 0 − 1 0 0 0 ] \begin{bmatrix} 0 & 0 & 0\\ 1 & 0 & -1\\ 0 & 0 & 0 \end{bmatrix} ⎣⎡0100000−10⎦⎤
对y
求导的卷积核:
[ 0 1 0 0 0 0 0 − 1 0 ] \begin{bmatrix} 0 & 1 & 0\\ 0 & 0 & 0\\ 0 & -1 & 0 \end{bmatrix} ⎣⎡00010−1000⎦⎤
注意:卷积操作有一个翻转卷积核的动作
使用scipy.signal
库的convolve2d
函数:
signal.convolve2d(img, kernel_x, boundary='symm', mode="same")
可选参数boundary
用于边界填充模式,'symm'
表示镜像模式
可选参数mode
指定结果大小,'same'
表示和输入数组大小一致
发现计算结果和自定义的函数一样
def show_image2(grad_x, grad_y, grad, res_x, res_y, res):
plt.figure(figsize=(10, 5)) # 设置窗口大小
plt.suptitle('image_gradient') # 图片名称
plt.subplot(2, 3, 1)
plt.title('grad_x')
plt.imshow(grad_x, cmap='gray'), plt.axis('off')
plt.subplot(2, 3, 2)
plt.title('gray_y')
plt.imshow(grad_y, cmap='gray'), plt.axis('off')
plt.subplot(2, 3, 3)
plt.title('grad')
plt.imshow(grad, cmap='gray'), plt.axis('off')
plt.subplot(2, 3, 4)
plt.title('res_x')
plt.imshow(res_x, cmap='gray'), plt.axis('off')
plt.subplot(2, 3, 5)
plt.title('res_y')
plt.imshow(res_y, cmap='gray'), plt.axis('off')
plt.subplot(2, 3, 6)
plt.title('res')
plt.imshow(res, cmap='gray'), plt.axis('off')
plt.savefig('./grad_compare.png') # 保存图像
plt.show()
if __name__ == '__main__':
img = get_image()
grad_x = image_gradient(img, axis=1)
grad_y = image_gradient(img, axis=0)
grad = np.sqrt(grad_x ** 2 + grad_y ** 2)
abs_x = np.array(np.abs(grad_x), dtype=np.uint8)
abs_y = np.array(np.abs(grad_y), dtype=np.uint8)
abs_grad = np.array(np.abs(grad), dtype=np.uint8)
kernel_x = [[0, 0, 0], [1, 0, -1], [0, 0, 0]]
kernel_y = [[0, 1, 0], [0, 0, 0], [0, -1, 0]]
res_x = np.array(signal.convolve2d(img, kernel_x, boundary='symm', mode="same"), dtype=np.double)
res_y = np.array(signal.convolve2d(img, kernel_y, boundary='symm', mode="same"), dtype=np.double)
res_x[:, 1:-1] = res_x[:, 1:-1] / 2.0
res_y[1:-1, :] = res_y[1:-1, :] / 2.0
res = np.sqrt(res_x ** 2 + res_y ** 2)
abs_res_x = np.array(np.abs(res_x), dtype=np.uint8)
abs_res_y = np.array(np.abs(res_y), dtype=np.uint8)
abs_res = np.array(np.abs(res), dtype=np.uint8)
print(res_x == grad_x)
print(res_y == grad_y)
show_image2(abs_x, abs_y, abs_grad, abs_res_x, abs_res_y, abs_res)
[[ True True True ... True True True]
[ True True True ... True True True]
[ True True True ... True True True]
...
[ True True True ... True True True]
[ True True True ... True True True]
[ True True True ... True True True]]
[[ True True True ... True True True]
[ True True True ... True True True]
[ True True True ... True True True]
...
[ True True True ... True True True]
[ True True True ... True True True]
[ True True True ... True True True]]
图像梯度的使用
求图像梯度的作用是得到图像边缘信息,以便进一步进行处理,在图象处理领域有一些使用很广泛的类梯度算子,比如Sobel
算子,Scharr
算子
Sobel
算子
参考:
Sobel
算子是一个离散差分算子,它是对图像梯度的近似,利用卷积操作来计算水平和垂直方向梯度
水平方向卷积核:
[ − 1 0 + 1 − 2 0 + 2 − 1 0 + 1 ] \begin{bmatrix} -1 & 0 & +1\\ -2 & 0 & +2\\ -1 & 0 & +1 \end{bmatrix} ⎣⎡−1−2−1000+1+2+1⎦⎤
垂直方向卷积核:
[ − 1 − 2 − 1 0 0 0 − 1 − 2 − 1 ] \begin{bmatrix} -1 & -2 & -1\\ 0 & 0 & 0\\ -1 & -2 & -1 \end{bmatrix} ⎣⎡−10−1−20−2−10−1⎦⎤
OpenCV Sobel
OpenCV
提供了函数cvSobel
来计算梯度
dst = cv.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])
src:输入图像
dst:输出图像,大小和通道与输入图像相同
ddepth:输出图像深度(数值类型),对于uint8将导致数值溢出
dx:x轴方向阶数
dy:y轴方向阶数
ksize:扩展Sobel核的大小,必须是1/3/5/7之一
scale;缩放因子,默认情况下不做缩放
delta:可选的delta值,在将结果存储到dst之前添加到结果中
borderType:像素边界处理方法,看BorderTypes
如果计算x
轴梯度,使用xorder = 1, yorder = 0, ksize = 3
的组合;计算y
轴梯度,使用xorder = 0, yorder = 1, ksize = 3
的组合
同时使用一种更简单的方式近似计算梯度:
G = ∣ G x ∣ + ∣ G y ∣ G = \left | G_{x} \right | + \left | G_{y} \right | G=∣Gx∣+∣Gy∣
代码如下:
...
import sys
import cv2 as cv
def main(argv):
...
# 不对Sobel处理图像进行缩放
scale = 1
# 没有额外添加值
delta = 0
# 将结果图像深度设置更高精度,防止溢出
ddepth = cv.CV_16S
...
...
# Load the image
src = cv.imread(argv[0], cv.IMREAD_COLOR)
...
# 预先进行高斯平滑
src = cv.GaussianBlur(src, (3, 3), 0)
# 获取灰度图像
gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
# 求x轴方向梯度
grad_x = cv.Sobel(gray, ddepth, 1, 0, ksize=3, scale=scale, delta=delta, borderType=cv.BORDER_DEFAULT)
# Gradient-Y
# grad_y = cv.Scharr(gray,ddepth,0,1)
# y轴方向梯度
grad_y = cv.Sobel(gray, ddepth, 0, 1, ksize=3, scale=scale, delta=delta, borderType=cv.BORDER_DEFAULT)
# 计算绝对值,并将结果图像深度转换成uint8
abs_grad_x = cv.convertScaleAbs(grad_x)
abs_grad_y = cv.convertScaleAbs(grad_y)
# 求整个图像梯度
grad = cv.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0)
...
...
Scipy Sobel
Scipy
也提供了ndimage
模块来处理图像,包含了sobel
函数
scipy.ndimage.sobel(input, axis=-1, output=None, mode='reflect', cval=0.0)
input:类数组
axis:计算的方向,默认-1(x轴)
output:数组或数值类型,输入数组表示存放结果在里面,输入数值类型表示结果数组的数值类型,默认结果数组核输入数组的类型一致
mode:字符串或序列,表示计算过程中如何扩展边界
1. reflect:通过对最后一个像素的边缘进行反射来扩展输入
2. constant:通过用cval参数定义的常量填充边缘以外的所有值来扩展输入
3. nearest:通过复制最后一个像素来扩展输入
4. mirror:通过对最后一个像素中心的反射来扩展输入
5. wrap:输入是扩展相反边界像素
cval:填充值
它的实现源码如下:
import numpy
from scipy.ndimage import _ni_support
from scipy.ndimage.filters import correlate1d
@_ni_docstrings.docfiller
def sobel(input, axis=-1, output=None, mode="reflect", cval=0.0):
...
# 将类数组转换成数组
input = numpy.asarray(input)
# 检查待计算轴是否在输入数组内
axis = _ni_support._check_axis(axis, input.ndim)
# 创建输出数组
output = _ni_support._get_output(output, input)
# 设置每一轴的边界计算模式
modes = _ni_support._normalize_sequence(mode, input.ndim)
# 先对指定轴执行相关操作
correlate1d(input, [-1, 0, 1], axis, output, modes[axis], cval, 0)
# 获取另外一个轴
axes = [ii for ii in range(input.ndim) if ii != axis]
for ii in axes:
correlate1d(output, [1, 2, 1], ii, output, modes[ii], cval, 0)
return output
x
轴卷积核可拆分成:
[ − 1 0 + 1 − 2 0 + 2 − 1 0 + 1 ] = [ 1 2 1 ] T [ − 1 0 1 ] \begin{bmatrix} -1 & 0 & +1\\ -2 & 0 & +2\\ -1 & 0 & +1 \end{bmatrix} = \begin{bmatrix} 1 & 2 & 1 \end{bmatrix}^{T} \begin{bmatrix} -1 & 0 & 1 \end{bmatrix} ⎣⎡−1−2−1000+1+2+1⎦⎤=[121]T[−101]
实现Sobel
算子相当于先对x
轴执行卷积[-1,0,1]
,在对y轴执行卷积[1,2,1]
y
轴卷积核可采分成:
[ − 1 − 2 − 1 0 0 0 − 1 − 2 − 1 ] = [ − 1 0 1 ] T [ 1 2 1 ] \begin{bmatrix} -1 & -2 & -1\\ 0 & 0 & 0\\ -1 & -2 & -1 \end{bmatrix}=\begin{bmatrix} -1 & 0 & 1 \end{bmatrix}^{T} \begin{bmatrix} 1 & 2 & 1 \end{bmatrix} ⎣⎡−10−1−20−2−10−1⎦⎤=[−101]T[121]
实现Sobel
算子相当于先对y
轴执行卷积[-1,0,1]
,在对x
轴执行卷积[1,2,1]
注意:输入图像需要先转换成更高精度,输出图像还需要进行数值类型转换
Scharr
算子
参考:
为了更好的近似梯度,还可以使用Scharr
算子
G x = [ − 3 0 3 − 10 0 10 − 3 0 3 ] G_{x}=\begin{bmatrix} -3 & 0 & 3\\ -10 & 0 & 10\\ -3 & 0 & 3 \end{bmatrix} Gx=⎣⎡−3−10−30003103⎦⎤
G x = [ − 3 − 10 − 3 0 0 0 3 10 3 ] G_{x}=\begin{bmatrix} -3 & -10 & -3\\ 0 & 0 & 0\\ 3 & 10 & 3 \end{bmatrix} Gx=⎣⎡−303−10010−303⎦⎤
OpenCV
提供了Scharr
函数
Scharr(src, dst, ddepth, dx, dy, scale, delta, borderType)
等同于Sobel
中kszie=-1
时的情况
Sobel
和Scharr
比较
测试代码如下:
import cv2 as cv
import matplotlib.pyplot as plt
def compare_sobel_scharr():
gray = cv.imread('lena.jpg', 0)
scale = 1
delta = 0
ddepth = cv.CV_16S
sobel_x = cv.Sobel(gray, ddepth, 1, 0, ksize=3, scale=scale, delta=delta, borderType=cv.BORDER_DEFAULT)
sobel_y = cv.Sobel(gray, ddepth, 0, 1, ksize=3, scale=scale, delta=delta, borderType=cv.BORDER_DEFAULT)
# Scharr算子有两种方式可以实现
scharr_x = cv.Sobel(gray, ddepth, 1, 0, ksize=-1, scale=scale, delta=delta, borderType=cv.BORDER_DEFAULT)
scharr_y = cv.Scharr(gray, ddepth, 0, 1, scale=scale, delta=delta, borderType=cv.BORDER_DEFAULT)
abs_sobel_x = cv.convertScaleAbs(sobel_x)
abs_sobel_y = cv.convertScaleAbs(sobel_y)
abs_scharr_x = cv.convertScaleAbs(scharr_x)
abs_scharr_y = cv.convertScaleAbs(scharr_y)
sobel_grad = cv.addWeighted(abs_sobel_x, 0.5, abs_sobel_y, 0.5, 0)
scharr_gray = cv.addWeighted(abs_scharr_x, 0.5, abs_scharr_y, 0.5, 0)
plt.figure(figsize=(10, 5)) # 设置窗口大小
plt.suptitle('sobel-scharr') # 图片名称
plt.subplot(2, 3, 1)
plt.title('sobel_x')
plt.imshow(abs_sobel_x, cmap='gray'), plt.axis('off')
plt.subplot(2, 3, 2)
plt.title('sobel_y')
plt.imshow(abs_sobel_y, cmap='gray'), plt.axis('off')
plt.subplot(2, 3, 3)
plt.title('sobel_grad')
plt.imshow(sobel_grad, cmap='gray'), plt.axis('off')
plt.subplot(2, 3, 4)
plt.title('scharr_x')
plt.imshow(abs_scharr_x, cmap='gray'), plt.axis('off')
plt.subplot(2, 3, 5)
plt.title('scharr_y')
plt.imshow(abs_scharr_y, cmap='gray'), plt.axis('off')
plt.subplot(2, 3, 6)
plt.title('scharr_gray')
plt.imshow(scharr_gray, cmap='gray'), plt.axis('off')
plt.savefig('./compare_sobel_scharr.png') # 保存图像
plt.show()