图像梯度

本文深入探讨了图像梯度的概念及其在边缘检测中的应用,包括方向导数、梯度计算、差分公式、卷积核及Sobel、Scharr算子的详细解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

复习图像梯度,发现有很多需要进一步理解的内容,重新整理一篇


目录

  1. 方向导数和梯度
  2. python实现
  3. 图像梯度的使用

方向导数和梯度

参考:

第七节 方向导数与梯度

偏导数

数字图像的梯度概念(the gradient of the image)

Image gradient

首先介绍数学概念上的导数和梯度的概念,再引申到图像梯度上

方向导数

方向导数:函数在某一点沿某一方向的变换率

设函数 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}} xx0lim=xx0f(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} xz=x0limxf(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} yz=y0limyf(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}) xz=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}) yz=f(x0,y0+1)f(x0,y0)

有限差分

参考:

Finite difference

有限差分

中心差分

差分

有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(xk1) ▽ \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(xk1) δ \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=[xfyf]=xfi +yfj

梯度的模:

∣ 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)=(xf)2+(yf)2

梯度方向:

θ = t a n − 1 ( g y g x ) \theta =tan^{-1}(\frac{g_{y}}{g_{x}}) θ=tan1(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 yf=[1+1]A=2110+1A

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 xf=[1+1]A=21[10+1]A


python实现

参考:Python Numpy gradient源码解析

我实现了一个用差分公式计算的梯度函数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 &amp; 0 &amp; 0\\ 1 &amp; 0 &amp; -1\\ 0 &amp; 0 &amp; 0 \end{bmatrix} 010000010

y求导的卷积核:

[ 0 1 0 0 0 0 0 − 1 0 ] \begin{bmatrix} 0 &amp; 1 &amp; 0\\ 0 &amp; 0 &amp; 0\\ 0 &amp; -1 &amp; 0 \end{bmatrix} 000101000

注意:卷积操作有一个翻转卷积核的动作

使用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 Derivatives

Sobel算子是一个离散差分算子,它是对图像梯度的近似,利用卷积操作来计算水平和垂直方向梯度

水平方向卷积核:

[ − 1 0 + 1 − 2 0 + 2 − 1 0 + 1 ] \begin{bmatrix} -1 &amp; 0 &amp; +1\\ -2 &amp; 0 &amp; +2\\ -1 &amp; 0 &amp; +1 \end{bmatrix} 121000+1+2+1

垂直方向卷积核:

[ − 1 − 2 − 1 0 0 0 − 1 − 2 − 1 ] \begin{bmatrix} -1 &amp; -2 &amp; -1\\ 0 &amp; 0 &amp; 0\\ -1 &amp; -2 &amp; -1 \end{bmatrix} 101202101

OpenCV Sobel

Sobel()

BorderTypes

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

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 &amp; 0 &amp; +1\\ -2 &amp; 0 &amp; +2\\ -1 &amp; 0 &amp; +1 \end{bmatrix} = \begin{bmatrix} 1 &amp; 2 &amp; 1 \end{bmatrix}^{T} \begin{bmatrix} -1 &amp; 0 &amp; 1 \end{bmatrix} 121000+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 &amp; -2 &amp; -1\\ 0 &amp; 0 &amp; 0\\ -1 &amp; -2 &amp; -1 \end{bmatrix}=\begin{bmatrix} -1 &amp; 0 &amp; 1 \end{bmatrix}^{T} \begin{bmatrix} 1 &amp; 2 &amp; 1 \end{bmatrix} 101202101=[101]T[121]

实现Sobel算子相当于先对y轴执行卷积[-1,0,1],在对x轴执行卷积[1,2,1]

注意:输入图像需要先转换成更高精度,输出图像还需要进行数值类型转换

Scharr算子

参考:

Scharr()

为了更好的近似梯度,还可以使用Scharr算子

G x = [ − 3 0 3 − 10 0 10 − 3 0 3 ] G_{x}=\begin{bmatrix} -3 &amp; 0 &amp; 3\\ -10 &amp; 0 &amp; 10\\ -3 &amp; 0 &amp; 3 \end{bmatrix} Gx=31030003103

G x = [ − 3 − 10 − 3 0 0 0 3 10 3 ] G_{x}=\begin{bmatrix} -3 &amp; -10 &amp; -3\\ 0 &amp; 0 &amp; 0\\ 3 &amp; 10 &amp; 3 \end{bmatrix} Gx=30310010303

OpenCV提供了Scharr函数

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

等同于Sobelkszie=-1时的情况

SobelScharr比较

测试代码如下:

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()

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值