OpenCV图像梯度、边缘检测、轮廓绘制、凸包检测大合集

一、图像梯度

在图像处理中,「梯度(Gradient)」是一个非常基础但又极其重要的概念。它是图像边缘检测、特征提取、纹理分析等众多任务的核心。梯度的本质是在空间上描述像素灰度值变化的快慢和方向。

但我们如何在图像中计算梯度?又该选择什么样的算子?本文将从梯度的数学定义出发,逐步引入经典的 Sobel 与 Laplacian 算子,带你了解图像梯度的计算原理与实践方式。

1.1 什么是图像梯度?

图像梯度反映的是像素值(灰度或强度)在空间中的变化率。可以类比为地形图中的"坡度":哪里灰度变化剧烈,哪里就是图像的"边缘"。

对于二维灰度图像I(x,y)I(x,y)I(x,y),梯度定义为图像对空间坐标的偏导数组成的向量:
∇I=∂I∂x,∂I∂y \nabla I = \left \\frac{\\partial I}{\\partial x}, \\frac{\\partial I}{\\partial y} \\right ∇I=∂x∂I,∂y∂I

  • ∂I∂x\frac{\partial I}{\partial x}∂x∂I:表示图像在水平方向(x轴)上的变化率;
  • ∂I∂y\frac{\partial I}{\partial y}∂y∂I:表示图像在垂直方向(y轴)上的变化率。

该向量的模长 表示梯度的强度,方向表示灰度变化最剧烈的方向。

1.2 如何计算梯度

由于图像是离散的,我们不能直接求导,而是通过离散卷积实现近似求导

使用cv2.filter2D自定义卷积核

OpenCV中filter2D可以对图形施加自定义的卷积核,是实现梯度算子的基础方法

语法如下所示:

python 复制代码
dst = cv2.filter2D(src, ddepth, kernel[, dst[, anchor[, delta[, borderType]]]])

filter2D函数是用于对图像进行二维卷积(滤波)操作。它允许用户自定义卷积核(kernal)来实现各种图像处理效果,如平滑,锐化,边缘检测。

参数解析:

参数名 类型 说明
src ndarray 输入图像,必须是单通道或多通道(如灰度图或彩色图)
ddepth int 输出图像的深度(如 cv2.CV_64F, -1 表示与原图相同)
kernel ndarray 卷积核(滤波器),必须是浮点型 np.float32np.float64
dst ndarray (可选)输出图像,与 src 同大小
anchor tuple 卷积核锚点,默认 (-1, -1) 表示核中心
delta float 可选偏移值,加到卷积结果上
borderType int 边界填充方式,常见如 cv2.BORDER_DEFAULT(边界反射_101), cv2.BORDER_REPLICATE
python 复制代码
import cv2 as cv
import numpy as np

# 构造图像:中心有明显亮度突变
img = np.array([
    [10, 10, 10, 10, 10, 10, 10],
    [10, 10, 10, 255, 255, 10, 10],
    [10, 10, 10, 255, 255, 10, 10],
    [10, 10, 10, 255, 255, 10, 10],
    [10, 10, 10, 10, 10, 10, 10]
], dtype=np.uint8)

# 使用 Sobel 水平方向边缘检测核
kernel = np.array([[-1, 0, 1],
                   [-2, 0, 2],
                   [-1, 0, 1]], dtype=np.float32)

# 卷积
img2 = cv.filter2D(img, -1, kernel)

print(img2)

结果展示:

python 复制代码
[[  0   0 255 255   0   0   0]
 [  0   0 255 255   0   0   0]
 [  0   0 255 255   0   0   0]
 [  0   0 255 255   0   0   0]
 [  0   0 255 255   0   0   0]]

1.3 常见的梯度算子

1️⃣ Sobel 算子(Sobel Operator)

Sobel 是最常见的梯度算子之一,结合了高斯平滑微分运算,对噪声更鲁棒。

  • 水平方向梯度核:

Gx=−101−202−101 Gx=\begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix} Gx= −1−2−1000121

  • 垂直方向梯度核:
    Gy=−1−2−1000121 G_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{bmatrix} Gy= −101−202−101

在 OpenCV 中的实现:

语法说明:

python 复制代码
dst = cv2.Sobel(src, ddepth, dx, dy, ksize=3, scale=1, delta=0, borderType=cv2.BORDER_DEFAULT)
参数名 类型 说明
src ndarray 输入图像(通常为灰度图)
ddepth int 输出图像的数据深度(OpenCV 中,-1 表示输出图像的深度与输入图像相同。)
dx int x 方向求导阶数(1 表示对 x 求一阶导),获取的垂直边缘
dy int y 方向求导阶数,获取的水平边缘
ksize int Sobel 核大小(可为 1, 3, 5, 7,常用 3)
scale float 可选缩放因子,对导数结果进行缩放(一般为 1)
delta float 可选偏移量,结果加上 delta(一般为 0)
borderType int 边界填充方式,默认 cv2.BORDER_DEFAULT

示例代码:Sobel算子的使用

python 复制代码
# sobel算子

import cv2 as cv

shudu = cv.imread('../images/shudu.png', cv.IMREAD_GRAYSCALE)

# x方向
dst_x = cv.Sobel(shudu, -1, 1, 0, ksize=3)

# y方向
dst_y = cv.Sobel(shudu, -1, 0, 1, ksize=3)

# x和y方向
dst_xy = cv.Sobel(shudu, -1, 1, 1, ksize=3)

cv.imshow('shudu', shudu)
cv.imshow('dst_x', dst_x)
cv.imshow('dst_y', dst_y)
cv.imshow('dst_xy', dst_xy)
cv.waitKey(0)
cv.destroyAllWindows()

结果输出:

灰度图 dx=1,dy=0(获取垂直边缘) dx=0,dy=1(获取水平边缘) dx=1,dy=1(不建议使用),用Laplacian来获取水平垂直边缘。

dxdy可以都为1,获取的垂直和水平方向上的梯度。dxdy不能都为0。

  • grad_x: 图像在 x 方向的梯度(横向变化)
  • grad_y: 图像在 y 方向的梯度(纵向变化)

我们可以将它们组成一个向量:
G⃗=(grad_x, grad_y) \vec{G} = (grad\_x, \, grad\_y) G =(grad_x,grad_y)

然后,使用勾股定理计算这个向量的长度(也就是梯度强度):
magnitude=grad_x2+grad_y2 \text{magnitude} = \sqrt{grad\_x^2 + grad\_y^2} magnitude=grad_x2+grad_y2


2️⃣ Laplacian 算子(Laplacian Operator)

一、什么是 Laplacian 算子?

Laplacian(拉普拉斯算子)是二阶微分算子,用于度量函数在某点处的"变化率的变化",即函数曲率。

在图像处理中,它能检测图像中灰度变化最显著的地方------边缘 ,尤其是亮度快速变化的区域,对噪声也很敏感。

数学定义如下:
Δf=∂2f∂x2+∂2f∂y2 \Delta f = \frac{\partial^2 f}{\partial x^2} + \frac{\partial^2 f}{\partial y^2} Δf=∂x2∂2f+∂y2∂2f


🧮 二、从一维差分到二维卷积核
1. 一维差分

一阶差分(梯度近似):
f′(x)≈f(x+1)−f(x) f'(x) \approx f(x+1) - f(x) f′(x)≈f(x+1)−f(x)

二阶差分(Laplacian 近似):
f′′(x)≈f(x+1)+f(x−1)−2f(x) f''(x) \approx f(x+1) + f(x-1) - 2f(x) f′′(x)≈f(x+1)+f(x−1)−2f(x)

对应的卷积核(差分模板)为:
k=1,−2,1 k=1,−2,1 k=1,−2,1


2. 推导二维 Laplacian 卷积核

对于二维函数 f(x,y)f(x,y)f(x,y):

水平方向二阶导数:
∂2f∂x2≈f(x+1,y)+f(x−1,y)−2f(x,y) \frac{\partial^2 f}{\partial x^2} \approx f(x+1, y) + f(x-1, y) - 2f(x, y) ∂x2∂2f≈f(x+1,y)+f(x−1,y)−2f(x,y)

垂直方向二阶导数:
∂2f∂y2≈f(x,y+1)+f(x,y−1)−2f(x,y) \frac{\partial^2 f}{\partial y^2} \approx f(x, y+1) + f(x, y-1) - 2f(x, y) ∂y2∂2f≈f(x,y+1)+f(x,y−1)−2f(x,y)

将它们相加:
Δf(x,y)≈f(x+1,y)+f(x−1,y)+f(x,y+1)+f(x,y−1)−4f(x,y) \Delta f(x, y) \approx f(x+1, y) + f(x-1, y) + f(x, y+1) + f(x, y-1) - 4f(x, y) Δf(x,y)≈f(x+1,y)+f(x−1,y)+f(x,y+1)+f(x,y−1)−4f(x,y)

这就是最常见的 4 邻域 Laplacian 模板:
k=0101−41010 k = \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix} k= 0101−41010


3. 加上对角(斜对角)项:8 邻域

如果你想让算子对角方向也敏感,可以扩展为:
k=1111−81111 k = \begin{bmatrix} 1 & 1 & 1 \\ 1 & -8 & 1 \\ 1 & 1 & 1 \end{bmatrix} k= 1111−81111

这种核能更广泛捕捉到不同方向的边缘,但也更敏感。

OpenCV 使用方式:

python 复制代码
cv2.Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
参数 含义
src 输入图像,必须是灰度图
ddepth 输出图像的深度,常用 cv2.CV_64F,避免溢出
ksize 卷积核大小,必须是奇数,一般设为 1 表示使用标准核(上面那个)
scale 缩放梯度值,默认 1
delta 可选的偏移值,默认 0
borderType 边缘填充方式,默认 cv2.BORDER_DEFAULT

与 Sobel 不同,Laplacian 不区分方向,输出的是一种方向无关的边缘响应

示例代码
python 复制代码
# Laplacian算子
import cv2 as cv

shudu = cv.imread('../images/shudu.png', cv.IMREAD_GRAYSCALE)

# Laplacian算子
dst = cv.Laplacian(shudu, -1, ksize=1)
cv.imshow('shudu', shudu)
cv.imshow('dst', dst)
cv.waitKey(0)
cv.destroyAllWindows()
灰度图 Laplacian算子

二、图像边缘检测

2.1. 什么是图像边缘?

从数学角度来看,图像边缘是图像灰度函数的一阶导数(梯度)取得极大值的位置,或二阶导数(Laplacian)为零的地方。

我们把二维图像 f(x,y)f(x,y)f(x,y) 看作一个连续函数,图像的变化速率(即灰度变化)就是它的梯度:
∇f=(∂f∂x,∂f∂y) \nabla f = \left( \frac{\partial f}{\partial x}, \frac{\partial f}{\partial y} \right) ∇f=(∂x∂f,∂y∂f)

梯度的模长即为边缘强度:
∣∇f∣=(∂f∂x)2+(∂f∂y)2 |\nabla f| = \sqrt{ \left( \frac{\partial f}{\partial x} \right)^2 + \left( \frac{\partial f}{\partial y} \right)^2 } ∣∇f∣=(∂x∂f)2+(∂y∂f)2


2. 2. 边缘检测的整体流程图

原始图像 高斯滤波\n去噪 Sobel卷积\n计算梯度与方向 非极大值抑制\n细化边缘 双阈值筛选\n连接边缘 输出边缘图像


2. 3. 高斯滤波去噪

边缘检测属于一种"锐化"操作,容易放大噪声。为此,第一步通常使用高斯滤波对图像进行平滑处理,消除小范围内的噪点干扰:

python 复制代码
blur = cv2.GaussianBlur(img, (5, 5), 1.4)

高斯核示例(5x5):
12731474141626164726412674162616414741 \frac{1}{273} \begin{bmatrix} 1 & 4 & 7 & 4 & 1\\ 4 & 16 & 26 & 16 & 4\\ 7 & 26 & 41 & 26 & 7\\ 4 & 16 & 26 & 16 & 4\\ 1 & 4 & 7 & 4 & 1 \end{bmatrix} 2731 1474141626164726412674162616414741


2.4. Sobel算子计算梯度与方向

📌 Sobel 卷积核

用于计算图像在水平与垂直方向上的一阶导数:

  • 水平(x方向)

Gx=−101−202−101 G_x = \begin{bmatrix} -1 & 0 & 1\\ -2 & 0 & 2\\ -1 & 0 & 1 \end{bmatrix} Gx= −1−2−1000121

  • 垂直(y方向)

Gy=−1−2−1000121 G_y = \begin{bmatrix} -1 & -2 & -1\\ 0 & 0 & 0\\ 1 & 2 & 1 \end{bmatrix} Gy= −101−202−101

梯度值与方向

python 复制代码
grad_x = cv2.Sobel(blur, cv2.CV_64F, 1, 0)
grad_y = cv2.Sobel(blur, cv2.CV_64F, 0, 1)
magnitude = cv2.magnitude(grad_x, grad_y)
angle = cv2.phase(grad_x, grad_y, angleInDegrees=True)
  • 梯度幅值(强度):

G=Gx2+Gy2 G = \sqrt{G_x^2 + G_y^2} G=Gx2+Gy2

  • 梯度方向:

θ=arctan⁡(GyGx) \theta = \arctan\left( \frac{G_y}{G_x} \right) θ=arctan(GxGy)


2.5. 非极大值抑制(NMS)

目的:只保留梯度方向上的局部极大值点,细化边缘线条

步骤如下:

  1. 对于每一个像素,查找其在梯度方向上的邻接像素。
  2. 如果当前像素的梯度值不是三者中最大的,就将其抑制为0。

为了比较非整数方向上的像素值,需要使用线性插值

得到θ\thetaθ的值之后,就可以对边缘方向进行分类,为了简化计算过程,一般将其归为四个方向:水平方向、垂直方向、45°方向、135°方向。并且:

当θ\thetaθ值为-22.5°~22.5°,或-157.5°~157.5°,则认为边缘为水平边缘;

当法线方向为22.5°~67.5°,或-112.5°~-157.5°,则认为边缘为45°边缘;

当法线方向为67.5°~112.5°,或-67.5°~-112.5°,则认为边缘为垂直边缘;

当法线方向为112.5°~157.5°,或-22.5°~-67.5°,则认为边缘为135°边缘;


2.6. 双阈值连接(Hysteresis)

非极大值抑制后,图像中仍有很多边缘片段。通过设定高低两个阈值,连接可靠的边缘:

  • 高于高阈值 → 强边缘(保留)
  • 低于低阈值 → 弱边缘(舍弃)
  • 介于之间 → 如果与强边缘连接,则保留;否则丢弃

推荐设置

python 复制代码
edges = cv2.Canny(img, threshold1=50, threshold2=150)

阈值比建议控制在 2:1 到 3:1 之间。


2.7. Canny 算子:全流程封装

OpenCV 内置的 Canny 算子封装了所有步骤:

python 复制代码
edges = cv2.Canny(image, 50, 150)

参数说明:

  • image: 输入灰度/二值化图像
  • threshold1: 低阈值,用于决定可能的边缘点。
  • threshold2: 高阈值,用于决定强边缘点。

2.8. 总结

步骤 作用 工具/算子
高斯滤波 平滑图像,去除噪声 cv2.GaussianBlur
梯度计算 提取边缘强度与方向 cv2.Sobel
非极大值抑制 边缘细化 自定义插值
双阈值链接 连接可靠边缘,抑制伪边缘 cv2.Canny

三、图像轮廓提取与绘制

图像轮廓是计算机视觉中一个非常关键的概念,它广泛应用于目标检测、图像分割、形状分析等领域。

3.1 什么是轮廓(Contours)

轮廓是将具有相同灰度值的像素点连接成线的过程。在图像中,轮廓通常用于表示物体的边界或形状。

轮廓与边缘的区别:

  • 边缘是强度变化的位置(如 Canny)
  • 轮廓是封闭的路径,更强调形状和结构
  • 边缘可能是离散点,轮廓是连续曲线

示意图:


3.2 寻找轮廓的流程

轮廓提取的流程通常如下:

graph TD A[彩色图像] --> B[灰度化] B --> C[二值化] C --> D[查找轮廓 \n cv2.findContours()]

3.3 OpenCV 提供了非常方便的函数:

python 复制代码
contours, hierarchy = cv2.findContours(image, mode, method)
3.3.1 参数说明:
参数 说明
image 输入图像,必须是二值图像
mode 轮廓检索模式(如下表)
method 轮廓逼近方法(如下表)
contours 返回的轮廓点坐标数组列表
hierarchy 返回轮廓间的层级结构

3.3.2 mode 参数解释(轮廓层次结构)
mode 值 含义
RETR_EXTERNAL 只提取最外层轮廓(最常用)
RETR_LIST 提取所有轮廓,但不构建父子关系
RETR_CCOMP 提取所有轮廓,并将外层和内层分层保存
RETR_TREE 提取所有轮廓并构建完整层次树结构

层次结构说明图(RETR_TREE):

复制代码
hierarchy[i] = [next, previous, child, parent]

3.3.3 method 参数解释(轮廓点存储方式)
method 值 含义
CHAIN_APPROX_NONE 保存所有边界点
CHAIN_APPROX_SIMPLE 压缩冗余点,只保留关键点(如直线只保留端点)
CHAIN_APPROX_TC89_L1 使用 Teh-Chin 链码逼近算法,效率更高(较少使用)

3.4 绘制轮廓

查找到轮廓后,可以使用以下函数将轮廓画出来:

python 复制代码
cv2.drawContours(image, contours, contourIdx, color, thickness)

参数说明

参数名 含义
image 输入/输出图像(会被修改)
contours 找到的轮廓点数组
contourIdx 要绘制的轮廓索引(-1 表示绘制所有)
color 轮廓线颜色(BGR)
thickness 线条粗细,负值表示填充区域

3.5 实战代码示例:

python 复制代码
import cv2 as cv
from socks import PRINTABLE_PROXY_TYPES

# 读取图像
img = cv.imread('../images/num.png')

# 转换为灰度图像
img_gray =cv.cvtColor(img,cv.COLOR_BGR2GRAY)

#二值化
_,img_binary = cv.threshold(img_gray,127,255,cv.THRESH_BINARY_INV)

# 寻找轮廓
counters,hierarchy = cv.findContours(img_binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)

print(counters)
print(len(counters))
print('-------')
print(hierarchy)

# 绘制轮廓
cv.drawContours(img,counters,-1,(0,255,0),3,cv.LINE_AA)
cv.imshow('img',img)
cv.waitKey(0)
cv.destroyAllWindows()

3.6 小贴士:轮廓查找注意事项

  • 🔸 输入图像必须为二值图像(黑白),推荐使用 cv2.threshold()
  • 🔸 可以先做边缘检测(如 Canny),再轮廓提取。
  • 🔸 cv2.findContours() 会修改原图像,最好用拷贝版本。
  • 🔸 drawContours() 可以搭配 boundingRect()minAreaRect() 等函数做目标框选。

3.7总结

步骤 内容
1️⃣ 灰度化原图
2️⃣ 二值化处理
3️⃣ 使用 cv2.findContours 提取轮廓
4️⃣ 使用 cv2.drawContours 绘制轮廓
5️⃣ 可结合形状分析、ROI 提取等进一步处理

四、绘制凸包

我们已经知道了如何获取轮廓点(contours)以及如何通过 cv2.convexHull() 得到 凸包点集。接下来,我们通过绘图的方式将凸包显示出来。


4.1 算法特点

在计算几何中,**穷举法(Brute Force)QuickHull是两种常见的凸包(Convex Hull)**构造算法,它们各有优缺点,适用于不同场景。下面为你简要整理两者特点,并通过表格进行对比:

1. 穷举法(Brute Force)

原理

遍历所有点对,判断这条边是否是凸包边:即判断所有其他点是否都在该边的同一侧。若是,则保留该边。

特点

  • 算法思想简单直观
  • 时间复杂度较高 :O(n3)O(n^3)O(n3);
  • 适合教学/小规模数据集
  • 实现容易理解,但不适合大数据场景。

2. QuickHull 算法

原理

类似快速排序的分治思想。先找出最左和最右的两个点作为"线段",划分上下两部分递归寻找最外层点,逐步构造出凸包。

特点

  • 平均性能优良 ,时间复杂度大约为 O(nlog⁡n)O(n \log n)O(nlogn);
  • 适合中大型数据
  • 实现相对复杂,但效率更高;
  • 对输入数据分布较敏感(最坏 O(n2)O(n^2)O(n2))。

函数一览

函数 功能
cv2.findContours() 获取轮廓点
cv2.convexHull() 根据轮廓点获取凸包点
cv2.polylines() 根据点集绘制折线(或闭合多边形)
python 复制代码
# 获取凸包点
import cv2 as cv

# 读取图像
image_tu = cv.imread('../images/tu.png')

# 转换为灰度图像
image_gray = cv.cvtColor(image_tu, cv.COLOR_BGR2GRAY)

# 二值化处理
_,image_binary = cv.threshold(image_gray,127,255,cv.THRESH_BINARY)

# 寻找轮廓
counters,_ = cv.findContours(image_binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)

# 获取凸包
convex_hull= []
for cnt in counters:
    convex_hull.append(cv.convexHull(cnt))

cv.polylines(image_tu,convex_hull,True,(255,0,0),3,cv.LINE_AA)
cv.imshow('binary',image_binary)
cv.imshow('tu',image_tu)
cv.waitKey(0)
cv.destroyAllWindows()

4.4 结果效果

假设你的原始图像中有一个不规则物体,该代码会:

  • 提取其轮廓
  • 计算包住这个物体的最小凸多边形(凸包)
  • 用线条将这个凸包标出

如图所示:


4.5 应用场景总结

应用领域 使用场景
手势识别 识别手指个数:凸包与缺陷分析(defects)
目标检测 将不规则轮廓转为规则包围多边形
图像压缩 简化轮廓特征
安全区域 包围任意散点区域
相关推荐
Coffeeee9 小时前
帮你快速理解AI Agent之我想招个Android实习生
android·人工智能·agent
新新技术迷9 小时前
AI聊天自动跟随滚动,附回到底部按钮
人工智能
先锋部队9 小时前
用Web Worker解析AI返回的大文本不卡UI
人工智能
把你拉进白名单9 小时前
8.OpenClaw源码解析——三层洋葱重试
人工智能·llm·agent
用户632415031789 小时前
拖文档进AI对话框解析,前端要处理哪些脏活
人工智能
姗姗来迟了9 小时前
AI回答里的引用来源卡片,前端怎么做
人工智能
用户7106207733409 小时前
Codex-端口配置错误排查案例(stream disconnected before completion)
人工智能
IT_陈寒10 小时前
JavaScript的默认参数挖坑实录,我掉进去了
前端·人工智能·后端
米小虾11 小时前
多Agent系统编排详解:从架构设计到代码实现
人工智能·agent
米小虾11 小时前
多Agent系统的编排:架构、协议与企业级应用
人工智能·agent