opencv学习笔记3:边缘检测(Robert、Sobel算子)

发布于:2025-07-05 ⋅ 阅读:(22) ⋅ 点赞:(0)

目录

一.边缘检测

1.边缘检测概念

2.不同算子简介

二.Robert算子

1.概念

2.整体算法思想

3.计算步骤

(1)梯度计算:

(2)边缘检测:

4.Robert算子 代码1

①cv2.filter2D(image, cv2.CV_32F, kernel_x) 中 cv2.CV_32F 表示32 位浮点数类型

②uint8类型解释

③uint8截断的缺点

④cv2.normalize 等比例归化函数

作用:选择归一化的计算方式。NORM_MINMAX 是最常用的方式之一,它的逻辑是:

6.Robert算子 卷积核的设计理念

简洁Robert算子代码1:

简洁Robert算子代码2:

三.Sobel算子

1.概念

2.计算

4.代码

soble函数

简洁Sobel算子代码:

5.Sobel算子的模拟实现

6.均值滤波的模拟实现


一.边缘检测

1.边缘检测概念

边缘检测:识别图像中亮度或颜色发生显著变化的区域(即边缘)。这些边缘对应着物体的边界、表面法线的突然改变或光照条件的变化。边缘构成了物体的轮廓,有助于快速定位和分离图像中的不同物体。

2.不同算子简介

Sobel算子:
Sobel算子是一种基于梯度的边缘检测方法。它使用两个3x3的卷积核,一个用于检测水平边缘,另一个用于检测垂直边缘。这两个卷积核分别计算图像中每个像素的水平和垂直梯度。然后,通过组合这两个梯度,可以获得每个像素的梯度幅值和方向。梯度值大的像素通常表示边缘。

Laplacian算子:
Laplacian算子是一种二阶导数算子,用于检测图像中的边缘。它对图像进行二次卷积,以查找像素值的变化率。Laplacian算子通常会增强边缘的轮廓,但它不提供边缘的方向信息。因此,它常常需要与其他方法一起使用,如Sobel算子,以获得更精确的边缘信息。

Canny边缘检测:
Canny边缘检测是一种多步边缘检测算法,它具有良好的边缘定位和噪声抑制能力。它的步骤包括:
a. 高斯滤波:首先,对图像进行高斯平滑以减少噪声。
b. 梯度计算:使用Sobel算子计算图像的梯度幅值和方向。
c. 非极大值抑制:在梯度方向上,只保留局部梯度最大的像素,以细化边缘。
d. 滞后阈值:应用双阈值,将像素分类为强边缘、弱边缘和非边缘。
e. 边缘跟踪:通过连接强边缘像素,形成连续的边缘。
 

二.Robert算子

图像可看作二维函数 f(x,y)((x,y) 是坐标,f 是灰度值 )

1.概念

Robert 算子是最早的一种边缘检测算子,它的核心思想是用交叉差分(Cross Difference)来近似图像中的梯度变化,从而检测出灰度突变的位置(即边缘)。将卷积结果对应到图像中卷积核左上角对齐的像素位置 ,来表示该位置的梯度。常用来处理具有陡峭的低噪声图像,当图像边缘接近于正45度或负45度时,该算法处理效果更理想。其缺点是对边缘的定位不太准确,提取的边缘线条较粗。

2.整体算法思想

  1. Robert 算子使用两个2×2的卷积核,分别检测水平垂直方向的边缘
  2. 将两个卷积核分别与图像进行卷积,得到水平梯度G_{x} 和垂直梯度G_{y}
    1. G_{x}卷积后突出水平方向的灰度变化(如从左上到右下的边缘)。
    2. G_{y}卷积后突出垂直方向的灰度变化(如从右上到左下的边缘)。
  3. 将两个方向的梯度合成,得到边缘强度(梯度幅值)公式:Gradient Magnitude=\sqrt[]{G_{x}^{2}+G_{y}^{2}}
  4. 二值化(可选):将梯度幅值与阈值比较,大于阈值的点视为边缘(通常设为白色,值为 255),小于阈值的点视为非边缘(通常设为黑色,值为 0)。

3.计算步骤

(1)梯度计算

  • Robert算子通过计算图像中灰度值的局部变化来估计梯度。边缘通常表示为灰度值的突变,因此,边缘检测算法的核心是计算图像中灰度变化的大小和方向

  • Robert算子使用两个简单的卷积核(或掩模)来计算图像在水平和垂直方向上的梯度。这两个卷积核分别是

  • G_{x}、 G_{y} 就是 Robert 算子的卷积核,它们是 预先定义好的小矩阵 ,作用是和图像做卷积运算,用来 “探测” 图像里水平、垂直方向的灰度变化。
  • 比如G_{x}这个核,会和图像每个像素周围区域做乘法 + 求和,突出水平方向的灰度差异;G_{y} 则突出垂直方向的差异。

梯度幅值:用上面这两个卷积核分别和图像卷积后,会得到水平方向梯度 G_{x}、垂直方向梯度G_{y}。但边缘检测要看 “变化有多明显”,所以需要把这两个方向的梯度 合成一个 “强度值” ,也就是梯度幅值(常见计算方式: \sqrt[]{G_{x}^{2}+G_{y}^{2}},或者简化为 \left | G_{x} \right |+ \left | G_{y} \right |

(2)边缘检测

  • 根据梯度幅值进行边缘检测。通常会应用一个阈值,将幅值高于阈值的像素标记为边缘像素,从而得到二值化的边缘图像。

4.Robert算子 代码1

代码1(用公式\sqrt[]{G_{x}^{2}+G_{y}^{2}}


import cv2
import numpy as np

# 读取图像(以灰度模式读取)
image = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE)

# 检查图像是否成功加载
if image is None:
    print("无法读取图像,请检查文件路径!")
else:
    # 定义Robert算子的卷积核
    kernel_x = np.array([[1, 0], [0, -1]], dtype=np.float32)  # 水平方向
    kernel_y = np.array([[0, 1], [-1, 0]], dtype=np.float32)  # 垂直方向

    # 应用卷积核
    grad_x = cv2.filter2D(image, cv2.CV_32F, kernel_x)
    grad_y = cv2.filter2D(image, cv2.CV_32F, kernel_y)

    # 计算梯度幅值(边缘强度)对矩阵每个元素进行 平方和开根号运算
    grad_magnitude = cv2.magnitude(grad_x, grad_y)

    #4. 转换为8位图像(便于显示)
    grad_magnitude_8u = np.uint8(grad_magnitude)  # 直接截断转成uint8(可能丢失信息)
    # 先不要管这个
    # 更好的方法:归一化到0-255范围
    # grad_magnitude_8u = cv2.normalize(grad_magnitude, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)

    # 显示原始图像和处理后的边缘图像
    cv2.imshow('Original Image', image)
    cv2.imshow('Robert Edge Detection', grad_magnitude_8u)
    
    # 等待按键关闭窗口
    cv2.waitKey(0)
    cv2.destroyAllWindows()

细节解释:

cv2.filter2D(image, cv2.CV_32F, kernel_x) 中 cv2.CV_32F 表示32 位浮点数类型

cv2.CV_32F 是 OpenCV 里用来 指定图像数据类型 的标识,代表 “32 位浮点数类型(32 - bit float)” 。如果这里用-1代替cv2.CV_32F, 则输出与输入图像image类型相同,默认是uint8,而cv2.magnitude参数类型通常为float32float64,则会导致参数类型错误。

像边缘检测、卷积运算这类操作,计算过程中可能产生小数(比如梯度值、卷积核加权和),用 float 类型(32 位浮点)存储能保留这些小数,避免截断误差。如果用 uint8(8 位整数),计算时小数会直接丢失,导致结果错误或精度下降。

②uint8类型解释

它是 “无符号 8 位整数类型” 的简称,占用 1 个字节(8 个二进制位) ,能表示的数值范围是 0 ~ 255(因为 8 位二进制最大是 11111111,对应十进制 255 )。

③uint8截断的缺点

grad_magnitude_8u = np.uint8(grad_magnitude)  # 直接截断转成uint8(可能丢失信息)

不管浮点数原本的范围,直接把小数部分砍掉(取整),然后把整数部分硬塞进 uint8 的表示范围(0 - 255)。一旦整数部分超过 255,就会 “取模 256”(或理解为 “循环截断” ),导致数值失真。假设 grad_magnitude 里有个像素值是 258.5,它的整数部分是 258。但 uint8 最大只能存 255,直接截断时,会用 258 % 256 = 2(取模运算),最终这个值就变成 2 存入 uint8 变量。

④cv2.normalize 等比例归化函数

grad_magnitude_8u = cv2.normalize(
    grad_magnitude,  # 输入:要处理的矩阵(浮点型梯度幅值)
    None,            # 输出:默认填 None,让函数自动创建输出矩阵
    0, 255,          # 归一化目标范围:把数据缩放到 0~255
    cv2.NORM_MINMAX, # 归一化方式:按最小-最大值缩放
    dtype=cv2.CV_8U  # 输出数据类型:转成 8 位无符号整数(uint8)
)
作用:选择归一化的计算方式NORM_MINMAX 是最常用的方式之一,它的逻辑是:
  1. 先找到输入矩阵的最小值(min)最大值(max)
  2. 然后把每个元素 x 按公式缩放: 这样就能把原始数据均匀 “挤压” 到 0~255 范围,保留数据的相对差异。

举例:

6.Robert算子 卷积核的设计理念

在图像处理中,边缘方向定义为灰度变化最剧烈的方向(如从黑到白的过渡方向),而梯度方向则是与边缘方向垂直的方向(指向灰度增加最快的方向)。

水平梯度核的目标是检测垂直边缘(即边缘方向为水平,如 | )。此时灰度变化发生在水平方向(如左黑右白),梯度方向为水平(从左向右)。

Robert 算子使用两个 2×2 的卷积核,分别检测图像中不同方向上的灰度变化。这两个卷积核的设计是为了近似计算图像在 45° 和 135° 方向上的一阶导数。通过计算这两个方向上的灰度差值(即梯度),可以判断图像中是否存在边缘。如果在某个位置处,这两个方向上的灰度变化较大,那么该位置就可能存在边缘。

简洁Robert算子代码1:

(用公式\sqrt[]{G_{x}^{2}+G_{y}^{2}}

import cv2
import numpy as np
import matplotlib.pyplot as plt

img=cv2.imread('Y:\\pycharm\\lion.png',cv2.IMREAD_GRAYSCALE)
if img is None:
    print("无法读取")
else:
    kernel_x=np.array([[1,0],[0,-1]],dtype=np.float32)
    kernel_y=np.array([[0,-1],[1,0]],dtype=np.float32)
    grad_x=cv2.filter2D(img,cv2.CV_32F,kernel_x)
    grad_y=cv2.filter2D(img,cv2.CV_32F,kernel_y)
    grad_magnitude=cv2.magnitude(grad_x,grad_y)
    grad_magnitude_uint8=np.uint8(grad_magnitude)

    plt.subplot(121),plt.imshow(img,cmap='gray')
    plt.title('Origin'),plt.xticks([]),plt.yticks([])
    plt.subplot(122),plt.imshow( grad_magnitude_uint8,cmap='gray')
    plt.title('new'),plt.xticks([]),plt.yticks([])
    plt.show()

 dst = cv2.addWeighted(src1, alpha, src2, beta, gamma)函数

参数 含义
src1 第一个输入数组(如图像),必须与 src2 大小和通道数相同。
alpha src1 的权重系数,取值范围通常为 [0, 1]。例如:
0.5 表示保留 50% 的 src1
src2 第二个输入数组,与 src1 类型相同。
beta src2 的权重系数,取值范围通常为 [0, 1]。例如:
0.5 表示保留 50% 的 src2
gamma

加到每个总和的标量值(亮度调整)。例如:
10 会使结果整体变亮。gamma 参数取 0 表

不对加权求和后的结果进行额外的亮度调整

简洁Robert算子代码2:

用公式\left | G_{x} \right |+ \left | G_{y} \right |

import cv2
import numpy as np
import matplotlib.pyplot as plt

img=cv2.imread('Y:\\pycharm\\lion.png',cv2.IMREAD_GRAYSCALE)
if img is None:
    print("无法读取")
else:
    kernel_x=np.array([[1,0],[0,-1]],dtype=np.float32)
    kernel_y=np.array([[0,-1],[1,0]],dtype=np.float32)
    grad_x=cv2.filter2D(img,cv2.CV_32F,kernel_x)
    grad_y=cv2.filter2D(img,cv2.CV_32F,kernel_y)
    grad_x=cv2.convertScaleAbs(grad_x)
    grad_y=cv2.convertScaleAbs(grad_y)
    grad_xy=cv2.addWeighted(grad_x,1,grad_y,1,0)
    plt.subplot(121),plt.imshow(img,cmap='gray')
    plt.title('Origin'),plt.xticks([]),plt.yticks([])
    plt.subplot(122),plt.imshow(grad_xy,cmap='gray')
    plt.title('new'),plt.xticks([]),plt.yticks([])
    plt.show()

三.Sobel算子

1.概念

Sobel 算子是一个主要用作边缘检测的离散微分算子 (discrete differentiation operator)。 它Sobel算子结合了高斯平滑和微分求导,用来计算图像灰度函数的近似梯度。

2.计算

水平变化: 将 I 与一个奇数大小的内核进行卷积。比如,当内核大小为3时, 的计算结果为:

垂直变化: 将: I 与一个奇数大小的内核进行卷积。比如,当内核大小为3时,  的计算结果为:

在图像的每一点,结合以上两个结果求出近似梯度:

另外有时,也可用下面更简单公式代替:

4.代码

用公式\left | G_{x} \right |+ \left | G_{y} \right |

网上的例程模版:

import cv2
import numpy as np

# 读取图像
zhaoyun = cv2.imread('zhaoyun1.jpg',0)
# Sobel边缘检测
zhaoyun_x_64 = cv2.Sobel(zhaoyun,cv2.CV_64F,dx=1,dy=0)# 水平方向
zhaoyun_x_full = cv2.convertScaleAbs(zhaoyun_x_64)    #取绝对值并转为
zhaoyun_y_64 = cv2.Sobel(zhaoyun,cv2.CV_64F,dx=0,dy=1)# 垂直方向
zhaoyun_y_full = cv2.convertScaleAbs(zhaoyun_y_64)

zhaoyun_xy_sobel_full = cv2.addWeighted(zhaoyun_x_full,1,zhaoyun_y_full,1,0)

cv2.imshow('zhaoyun',zhaoyun)
cv2.imshow('zhaoyun_xy_sobel_full',zhaoyun_xy_sobel_full)
cv2.waitKey(0)

soble函数

sobel算子
cv2.Sobel(src, ddepth, dx, dy[, ksize[, scale[, delta[, borderType]]]])
参数:
src:输入图像
ddepth: 输出图像的深度(可以理解为数据类型),-1表示与原图像相同的深度
            这里填CV_32F,CV_64F都行
dx,dy:当组合为dx=1,dy=0时求x方向的一阶导数,
当组合为dx=0,dy=1时求y方向的一阶导数(如果同时为1,通常效果不佳)

ksize:(可选参数)Sobel算子的大小,必须是1,3,5或者7,默认为3。

简洁Sobel算子代码:

import cv2
import numpy as np
import matplotlib.pyplot as plt

img=cv2.imread('Y:\\pycharm\\lion.png',cv2.IMREAD_GRAYSCALE)
if img is None:
    print("无法读取")
else:
    soblex=cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
    soblex_full=cv2.convertScaleAbs(soblex)
    sobley=cv2.Sobel(img,cv2.CV_64F,0,1,ksize=3)
    sobley_full=cv2.convertScaleAbs(sobley)
    new=cv2.addWeighted(soblex_full,1,sobley_full,1,0)

    plt.subplot(121),plt.imshow(img,cmap='gray')
    plt.title('origin'),plt.xticks([]),plt.yticks([])
    plt.subplot(122), plt.imshow(new, cmap='gray')
    plt.title('new'), plt.xticks([]), plt.yticks([])
    plt.show()

5.Sobel算子的模拟实现

mySobel函数模拟实现了cv2.Sobel函数的逻辑

# sobel模拟实现
import cv2
import numpy as np
import matplotlib.pyplot as plt

#sobel模拟函数
def mySobel(img, kernel, height, width):
    #储存卷积操作后新图的列表
    mySobel=np.zeros((height,width),dtype=np.float32)
    # 零填充法
    arr = np.zeros((height + 2, width + 2), dtype=np.float32)
    for i in range(height):
        for j in range(width):
            arr[i + 1, j + 1] = img[i, j]
    #卷积操作
    for i in range(1, height + 1):
        for j in range(1, width + 1):
            mySobel[i-1][j-1]=(#这里格式务必从新行开始
            arr[i - 1][j - 1] * kernel[0][0]
            + arr[i - 1][j] * kernel[0][1]
            +arr[i - 1][j + 1] * kernel[0][2]
            +arr[i][j - 1] * kernel[1][0]
            +arr[i][j] * kernel[1][1]
            +arr[i][j + 1] * kernel[1][2]
            +arr[i + 1][j - 1] * kernel[2][0]
            +arr[i + 1][j] * kernel[2][1]
            +arr[i + 1][j + 1] * kernel[2][2]
            )
    return mySobel

img = cv2.imread('Y:\\pycharm\\lion.png', cv2.IMREAD_GRAYSCALE)
height, width = img.shape[:2]
if img is None:
    print("无法读取")
else:
    kernel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32)
    kernel_y = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32)
    sobel_x = mySobel(img, kernel_x, height, width)
    sobel_x_full = cv2.convertScaleAbs(sobel_x)
    sobel_y = mySobel(img, kernel_y, height, width)
    sobel_y_full = cv2.convertScaleAbs(sobel_y)
    new = cv2.addWeighted(sobel_x_full, 1, sobel_y_full, 1, 0)

    plt.subplot(121), plt.imshow(img, cmap='gray')
    plt.title('origin'), plt.xticks([]), plt.yticks([])
    plt.subplot(122), plt.imshow(new, cmap='gray')
    plt.title('new'), plt.xticks([]), plt.yticks([])
    plt.show()

结果: 

 

6.均值滤波的模拟实现

与上面sobel模拟实现差不多,都是设置相应的卷积核,然后都执行卷积操作,这里的myfilter实际就是上面的mySobel。

import cv2
import numpy as np
import matplotlib.pyplot as plt
def add_peppersalt_noise(img,n=10000):
    result=img.copy()
    h,w=img.shape[:2]
    for i in range(n):
        x=np.random.randint(1,w)
        y=np.random.randint(1,h)
        if np.random.randint(0,2)==0:
            result[x,y]=0
        else:
            result[x,y]=255
    return result

def myfilter(img,kernel,height,width):
    dst = np.zeros((height, width), dtype=np.float32)
    # 零填充法
    arr = np.zeros((height + 2, width + 2), dtype=np.float32)
    for i in range(height):
        for j in range(width):
            arr[i + 1, j + 1] = img[i, j]
    # 卷积操作
    for i in range(1, height + 1):
        for j in range(1, width + 1):
            dst[i - 1][j - 1] = (  # 这里格式务必从新行开始
                    arr[i - 1][j - 1] * kernel[0][0]
                    + arr[i - 1][j] * kernel[0][1]
                    + arr[i - 1][j + 1] * kernel[0][2]
                    + arr[i][j - 1] * kernel[1][0]
                    + arr[i][j] * kernel[1][1]
                    + arr[i][j + 1] * kernel[1][2]
                    + arr[i + 1][j - 1] * kernel[2][0]
                    + arr[i + 1][j] * kernel[2][1]
                    + arr[i + 1][j + 1] * kernel[2][2]
            )
    return dst

img=cv2.imread('Y:\\pycharm\\lion.png',cv2.IMREAD_GRAYSCALE)
height, width = img.shape[:2]
if img is None:
    print("无法读取图像")
else:
    img1 = add_peppersalt_noise(img)
    kernel=np.ones((3,3),dtype=np.float32)/25
    dst=myfilter(img1,kernel,height,width)
    plt.subplot(121),plt.imshow(img1,cmap='gray')
    plt.title("origin"),plt.xticks([]),plt.yticks([])
    plt.subplot(122), plt.imshow(dst, cmap='gray')
    plt.title("dst"), plt.xticks([]), plt.yticks([])
    plt.show()


网站公告

今日签到

点亮在社区的每一天
去签到