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
可以对图形施加自定义的卷积核,是实现梯度算子的基础方法
语法如下所示:
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 cvimport 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
dx
int
dy
int
ksize
int
scale
float
delta
float
borderType
int
cv2.BORDER_DEFAULT
示例代码:Sobel
算子的使用
# sobel算子import cv2 as cvshudu = 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
,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
scale
delta
borderType
cv2.BORDER_DEFAULT
与 Sobel 不同,Laplacian 不区分方向,输出的是一种方向无关的边缘响应。
示例代码
# Laplacian算子import cv2 as cvshudu = 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()


二、图像边缘检测
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. 边缘检测的整体流程图
#mermaid-svg-K8tLy32DJDprH2ts {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-K8tLy32DJDprH2ts .error-icon{fill:#552222;}#mermaid-svg-K8tLy32DJDprH2ts .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-K8tLy32DJDprH2ts .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-K8tLy32DJDprH2ts .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-K8tLy32DJDprH2ts .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-K8tLy32DJDprH2ts .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-K8tLy32DJDprH2ts .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-K8tLy32DJDprH2ts .marker{fill:#333333;stroke:#333333;}#mermaid-svg-K8tLy32DJDprH2ts .marker.cross{stroke:#333333;}#mermaid-svg-K8tLy32DJDprH2ts svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-K8tLy32DJDprH2ts .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-K8tLy32DJDprH2ts .cluster-label text{fill:#333;}#mermaid-svg-K8tLy32DJDprH2ts .cluster-label span{color:#333;}#mermaid-svg-K8tLy32DJDprH2ts .label text,#mermaid-svg-K8tLy32DJDprH2ts span{fill:#333;color:#333;}#mermaid-svg-K8tLy32DJDprH2ts .node rect,#mermaid-svg-K8tLy32DJDprH2ts .node circle,#mermaid-svg-K8tLy32DJDprH2ts .node ellipse,#mermaid-svg-K8tLy32DJDprH2ts .node polygon,#mermaid-svg-K8tLy32DJDprH2ts .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-K8tLy32DJDprH2ts .node .label{text-align:center;}#mermaid-svg-K8tLy32DJDprH2ts .node.clickable{cursor:pointer;}#mermaid-svg-K8tLy32DJDprH2ts .arrowheadPath{fill:#333333;}#mermaid-svg-K8tLy32DJDprH2ts .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-K8tLy32DJDprH2ts .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-K8tLy32DJDprH2ts .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-K8tLy32DJDprH2ts .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-K8tLy32DJDprH2ts .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-K8tLy32DJDprH2ts .cluster text{fill:#333;}#mermaid-svg-K8tLy32DJDprH2ts .cluster span{color:#333;}#mermaid-svg-K8tLy32DJDprH2ts div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-K8tLy32DJDprH2ts :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}原始图像高斯滤波\\n去噪Sobel卷积\\n计算梯度与方向非极大值抑制\\n细化边缘双阈值筛选\\n连接边缘输出边缘图像
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}27311474141626164726412674162616414741
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+Gy2G = \\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 TDA[彩色图像] --> 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 参数解释(轮廓层次结构)
RETR_EXTERNAL
RETR_LIST
RETR_CCOMP
RETR_TREE
层次结构说明图(RETR_TREE):
hierarchy[i] = [next, previous, child, parent]
3.3.3 method 参数解释(轮廓点存储方式)
CHAIN_APPROX_NONE
CHAIN_APPROX_SIMPLE
CHAIN_APPROX_TC89_L1
3.4 绘制轮廓
查找到轮廓后,可以使用以下函数将轮廓画出来:
cv2.drawContours(image, contours, contourIdx, color, thickness)
参数说明:
image
contours
contourIdx
color
thickness
3.5 实战代码示例:
import cv2 as cvfrom 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总结
cv2.findContours
提取轮廓cv2.drawContours
绘制轮廓四、绘制凸包
我们已经知道了如何获取轮廓点(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 结果效果
假设你的原始图像中有一个不规则物体,该代码会:
- 提取其轮廓
- 计算包住这个物体的最小凸多边形(凸包)
- 用线条将这个凸包标出
如图所示: