一、图像梯度
在图像处理中,「梯度(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
可以对图形施加自定义的卷积核,是实现梯度算子的基础方法
语法如下所示:
dst = cv2.filter2D(src, ddepth, kernel[, dst[, anchor[, delta[, borderType]]]])
filter2D
函数是用于对图像进行二维卷积(滤波)操作。它允许用户自定义卷积核(kernal)来实现各种图像处理效果,如平滑,锐化,边缘检测。
参数解析:
参数名 | 类型 | 说明 |
---|---|---|
src |
ndarray |
输入图像,必须是单通道或多通道(如灰度图或彩色图) |
ddepth |
int |
输出图像的深度(如 cv2.CV_64F , -1 表示与原图相同) |
kernel |
ndarray |
卷积核(滤波器),必须是浮点型 np.float32 或 np.float64 |
dst |
ndarray |
(可选)输出图像,与 src 同大小 |
anchor |
tuple |
卷积核锚点,默认 (-1, -1) 表示核中心 |
delta |
float |
可选偏移值,加到卷积结果上 |
borderType |
int |
边界填充方式,常见如 cv2.BORDER_DEFAULT (边界反射_101), cv2.BORDER_REPLICATE |
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)
结果展示:
[[ 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 中的实现:
语法说明:
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
算子的使用
# 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来获取水平垂直边缘。 |
dx
,dy
可以都为1,获取的垂直和水平方向上的梯度。dx
,dy
不能都为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 使用方式:
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 不区分方向,输出的是一种方向无关的边缘响应。
示例代码
# 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. 边缘检测的整体流程图
2. 3. 高斯滤波去噪
边缘检测属于一种“锐化”操作,容易放大噪声。为此,第一步通常使用高斯滤波对图像进行平滑处理,消除小范围内的噪点干扰:
blur = cv2.GaussianBlur(img, (5, 5), 1.4)
高斯核示例(5x5):
1273[1474141626164726412674162616414741] \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
梯度值与方向
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)
目的:只保留梯度方向上的局部极大值点,细化边缘线条。
步骤如下:
- 对于每一个像素,查找其在梯度方向上的邻接像素。
- 如果当前像素的梯度值不是三者中最大的,就将其抑制为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)
非极大值抑制后,图像中仍有很多边缘片段。通过设定高低两个阈值,连接可靠的边缘:
- 高于高阈值 → 强边缘(保留)
- 低于低阈值 → 弱边缘(舍弃)
- 介于之间 → 如果与强边缘连接,则保留;否则丢弃
推荐设置
edges = cv2.Canny(img, threshold1=50, threshold2=150)
阈值比建议控制在 2:1 到 3:1 之间。
2.7. Canny 算子:全流程封装
OpenCV 内置的 Canny 算子封装了所有步骤:
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 提供了非常方便的函数:
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 绘制轮廓
查找到轮廓后,可以使用以下函数将轮廓画出来:
cv2.drawContours(image, contours, contourIdx, color, thickness)
参数说明:
参数名 | 含义 |
---|---|
image |
输入/输出图像(会被修改) |
contours |
找到的轮廓点数组 |
contourIdx |
要绘制的轮廓索引(-1 表示绘制所有) |
color |
轮廓线颜色(BGR) |
thickness |
线条粗细,负值表示填充区域 |
3.5 实战代码示例:
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(nlogn)O(n \log n)O(nlogn);
- 适合中大型数据;
- 实现相对复杂,但效率更高;
- 对输入数据分布较敏感(最坏 O(n2)O(n^2)O(n2))。
函数一览
函数 | 功能 |
---|---|
cv2.findContours() |
获取轮廓点 |
cv2.convexHull() |
根据轮廓点获取凸包点 |
cv2.polylines() |
根据点集绘制折线(或闭合多边形) |
# 获取凸包点
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) |
目标检测 | 将不规则轮廓转为规则包围多边形 |
图像压缩 | 简化轮廓特征 |
安全区域 | 包围任意散点区域 |