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

发布于:2025-07-27 ⋅ 阅读:(18) ⋅ 点赞:(0)

一、图像梯度

在图像处理中,「梯度(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=[xI,yI]

  • ∂I∂x\frac{\partial I}{\partial x}xI:表示图像在水平方向(x轴)上的变化率;
  • ∂I∂y\frac{\partial I}{\partial y}yI:表示图像在垂直方向(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.float32np.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= 121000121

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

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

结果输出:

image-20250726152449232 image-20250726152520688 image-20250726152546696 image-20250726152615977
灰度图 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=x22f+y22f


🧮 二、从一维差分到二维卷积核
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(x1)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) x22ff(x+1,y)+f(x1,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) y22ff(x,y+1)+f(x,y1)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(x1,y)+f(x,y+1)+f(x,y1)4f(x,y)
这就是最常见的 4 邻域 Laplacian 模板:
k=[0101−41010] k = \begin{bmatrix} 0 & 1 & 0 \\ 1 & -4 & 1 \\ 0 & 1 & 0 \end{bmatrix} k= 010141010


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

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

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

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()
image-20250726161527158 image-20250726161549023
灰度图 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=(xf,yf)
梯度的模长即为边缘强度:
∣∇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=(xf)2+(yf)2


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

原始图像
高斯滤波\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} 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= 121000121

  • 垂直(y方向)

Gy=[−1−2−1000121] G_y = \begin{bmatrix} -1 & -2 & -1\\ 0 & 0 & 0\\ 1 & 2 & 1 \end{bmatrix} Gy= 101202101

梯度值与方向

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°边缘;

image-20250726162939631


2.6. 双阈值连接(Hysteresis)

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

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

image-20250726163059933

推荐设置

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)
  • 轮廓是封闭的路径,更强调形状和结构
  • 边缘可能是离散点,轮廓是连续曲线

示意图:

image-20250726164247720


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

image-20250726165323496


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() 根据点集绘制折线(或闭合多边形)
# 获取凸包点
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 结果效果

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

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

如图所示:

image-20250726165945562


4.5 应用场景总结

应用领域 使用场景
手势识别 识别手指个数:凸包与缺陷分析(defects)
目标检测 将不规则轮廓转为规则包围多边形
图像压缩 简化轮廓特征
安全区域 包围任意散点区域

网站公告

今日签到

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