04 OpenCV--图像预处理之图像梯度处理、图像边缘检测、图像轮廓检测、凸包的相关操作

发布于:2025-07-29 ⋅ 阅读:(26) ⋅ 点赞:(0)

目录

1 图像梯度处理

图像梯度

垂直边缘提取

Sobel算子

Laplacian算子

2 图像边缘检测

1 高斯滤波

2 计算图像的梯度与方向

3 非极大值抑制

4 双阈值筛选

3 图像轮廓

轮廓查找

1 轮廓检索模式

1.1 RETR_EXTERNAL

1.2 RETR_LIST

1.3 RETR_TREE

1.4 RETR_CCOMP

2 轮廓存储方法

绘制轮廓

4 凸包

1.穷举法

2.QuickHull法

获取凸包点

绘制凸包


1 图像梯度处理

图像梯度

  1. 图像即函数: 将一幅灰度图像看作一个二维函数 I(x, y),其中 (x, y) 是像素坐标,I 是该点的像素值(如0-255)。

  2. 梯度定义: 在微积分中,一个标量函数 f(x, y) 的梯度是一个向量: ∇f = [∂f/∂x, ∂f/∂y]^T 其中:

    • ∂f/∂x 是函数在 x 方向的变化率(水平方向导数)。

    • ∂f/∂y 是函数在 y 方向的变化率(垂直方向导数)。

  3. 图像梯度: 对于图像 I(x, y)

    • 梯度向量 G G = [Gx, Gy]^T = [∂I/∂x, ∂I/∂y]^T

      • Gx:图像在水平方向 (x) 上的导数(近似值),反映了垂直边缘(左右亮度变化)。

      • Gy:图像在垂直方向 (y) 上的导数(近似值),反映了水平边缘(上下亮度变化)。

    • 梯度幅值(强度) |G| |G| = sqrt(Gx² + Gy²) 或近似为 |Gx| + |Gy|。它表示该点变化的剧烈程度。幅值越大,表示该点的亮度变化越突然、越显著,通常对应边缘区域。。

    • 梯度方向 θ θ = arctan(Gy / Gx)(注意处理分母为0的情况)。它表示该点变化最快的方向(垂直于边缘的方向)。例如,垂直边缘的梯度方向是水平的(左黑右白边缘,方向向右;左白右黑边缘,方向向左)

垂直边缘提取

滤波是应用卷积来实现的,卷积的关键就是卷积核,我们来考察下面这个卷积核:

$$
k1=\left[\begin{array}{c c c}{{-1}}&{{0}}&{{1}}\\ {{-2}}&{{0}}&{{2}}\\ {{-1}}&{{0}}&{{1}}\end{array}\right]
$$

这个核是用来提取图片中的垂直边缘的,看下图:

当前列左右两侧的元素进行差分,由于边缘的值明显小于(或大于)周边像素,所以边缘的差分结果会明显不同,这样就提取出了垂直边缘。

同理,把上面那个矩阵转置一下,就是提取水平边缘。这种差分操作就称为图像的梯度计算:

$$
k2=\left[\begin{array}{c c c}{{-1}}&{{-2}}&{{-1}}\\ {{0}}&{{0}}&{{0}}\\ {{1}}&{{2}}&{{1}}\end{array}\right]
$$


cv2.filter2D(src, ddepth, kernel)

filter2D函数是用于对图像进行二维卷积(滤波)操作。

  • src: 输入图像,一般为numpy数组。

  • ddepth: 输出图像的深度,可以是负值(表示与原图相同)、正值或其他特定值(常用-1 表示输出与输入具有相同的深度)。

  • kernel: 卷积核,一个二维数组(通常为奇数大小的方形矩阵),用于计算每个像素周围邻域的加权先

用数组模拟一下

import cv2 as cv
import numpy as np
# 模拟一张图像,灰度图
img = np.array([
    [20,  20,  20,  20,  20, 200, 200, 200, 200, 200],  # 垂直边缘(左暗右亮)
    [20,  20,  20,  20,  20, 200, 200, 200, 200, 200],  # 垂直边缘
    [20,  50,  80, 110, 140, 170, 200, 230, 255, 255],  # 线性渐变(左到右变亮)
    [255, 230, 200, 170, 140, 110, 80,  50,  20,  20],  # 线性渐变(左到右变暗)
    [20,  20,  20,  20, 200, 200, 200, 200, 200, 200],  # 垂直边缘(中间跳变)
    [200, 200, 200, 200, 20,  20,  20,  20,  20,  20],  # 垂直边缘(中间跳变,反向)
    [20,  20,  20,  20,  20,  20,  20,  20,  20,  20],  # 全暗区域
    [200, 200, 200, 200, 200, 200, 200, 200, 200, 200]  # 全亮区域
], dtype=np.uint8)
# 定义卷积核,
kernel=np.array([[-1,0,1],
                 [-2,0,2],
                 [-1,0,1]],dtype=np.float32)
# 二维卷积操作 cv.filter2D(src,ddepth,kernel)
img2=cv.filter2D(img,-1,kernel)
# 打印卷积后的图
print(img2)
[[  0   0   0   0 255 255   0   0   0   0]
 [  0  60  60  60 255 255  60  55  25   0]
 [  0  65  60  60 240 240  60  50  20   0]
 [  0   0   0 120 120   0   0   0   0   0]
 [  0   0   0 120 120   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]
 [  0   0   0   0   0   0   0   0   0   0]]

Sobel 和 Prewitt 算子是图像处理中常用的梯度算子,用于检测图像中的边缘信息。它们通过卷积操作计算图像在水平和垂直方向的灰度变化率,从而突出边缘区域

Sobel算子

在二维图像中,梯度是一个向量,包含水平方向(x)和垂直方向(y)的分量:

  • 水平梯度(Gx):检测垂直边缘(如竖直线条)。

  • 垂直梯度(Gy):检测水平边缘(如水平线条)。

边缘的强度(梯度幅值)为:

$$
\text{G} = \sqrt{G_x^2 + G_y^2} \approx |G_x| + |G_y|\
$$

在梯度处理方式这个组件中,当参数filter_method选择Sobel时,其他参数的含义如下所述:

sobel_image = cv2.Sobel(src, ddepth, dx, dy, ksize)

  • src:输入图像(单通道,如灰度图)。

  • ddepth:输出图像的深度(数据类型),常用取值:

    • -1:表示输出与输入图像深度相同(如 uint8

  • dx:x 方向的导数阶数(通常取 0 或 1)。

    • dx=1:计算 x 方向梯度(水平边缘)。

    • dx=0:不计算 x 方向梯度。

  • dy:y 方向的导数阶数(通常取 0 或 1)。

    • dy=1:计算 y 方向梯度(垂直边缘)。

    • dy=0:不计算 y 方向梯度。

  • ksize:Sobel 核的大小,必须为 1、3、5 或 7

Sobel 算子在计算梯度时引入了权重系数,对中心像素周围的邻域进行加权差分,增强了抗噪性。其卷积核设计如下:

x 方向(水平梯度)

$$
\text{Sobel}_x = \begin{bmatrix} -1 & 0 & 1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \end{bmatrix}
$$

y 方向(垂直梯度)

$$
\text{Sobel}_y = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \end{bmatrix}\
$$

import cv2 as cv
import numpy as np
#读取图片
img = cv.imread('../images/shudu.png',cv.IMREAD_GRAYSCALE)
#sobel cv.Sbel(src,ddepth,dx,dy,ksize=3,scale=1,delta=0,borderType=cv.BORDER_DEFAULT)
#dx = 1,dy=0 求x方向梯度,提取垂直梯度
dst = cv.Sobel(img,-1,1,0,ksize = 3)
#dy = 1,dx=0 求y方向梯度,提取水平梯度
dst1 = cv.Sobel(img,-1,0,1,ksize=3)
#dx = 1,dy=1 求x和y方向梯度,提取对角线梯度
dst2 = cv.Sobel(img,-1,1,1,ksize=3)
cv.imshow('src',img)
cv.imshow('dst',dst)
cv.imshow('dst1',dst1)
cv.imshow('dst2',dst2)
​
cv.waitKey(0)
cv.destroyAllWindows()

Laplacian算子

拉普拉斯算子是一种二阶微分算子,用于测量图像中灰度值的曲率。对于二维图像 (I(x,y)),拉普拉斯算子定义为:

$$
\nabla^2 I = \frac{\partial^2 I}{\partial x^2} + \frac{\partial^2 I}{\partial y^2}\
$$

其卷积核设计如下:

x 方向(水平梯度)

$$
Prewitt_x = \begin{bmatrix} -1 & 0 & 1 \\ -1 & 0 & 1 \\ -1 & 0 & 1 \end{bmatrix}\
$$

y 方向(垂直梯度)

$$
{Prewitt}_y = \begin{bmatrix} -1 & -1 & -1 \\ 0 & 0 & 0 \\ 1 & 1 & 1 \end{bmatrix}\
$$

在离散图像中,拉普拉斯算子可通过卷积核近似实现,常用的 3×3 拉普拉斯核:

(四邻域)

$$
\begin{bmatrix} 0 & -1 & 0 \\ -1 & 4 & -1 \\ 0 & -1 & 0 \end{bmatrix}\
$$

(八邻域,包含对角线):

$$
\begin{bmatrix} -1 & -1 & -1 \\ -1 & 8 & -1 \\ -1 & -1 & -1 \end{bmatrix}
$$

cv2.Laplacian(src, ddepth)

  • src:输入图像(单通道,如灰度图)。

  • ddepth:输出图像的深度(数据类型),常用取值:

    • -1:表示输出与输入图像深度相同(如 uint8

  • ksize:拉普拉斯核的大小,必须为 1、3、5 或 7(默认值为 1)。

import cv2 as cv
# 加载图片
img = cv.imread('../images/shudu.png',cv.IMREAD_GRAYSCALE)
# 拉普拉斯算子 cv.Laplacian(src,ddepth,ksize[,scale[,delta[,borderType]]])
dst = cv.Laplacian(img,-1)
cv.imshow('src',img)
cv.imshow('dst',dst)
cv.waitKey()
cv.destroyAllWindows()

2 图像边缘检测

完整流程步骤:

1 高斯滤波

使用高斯核等方法减少噪声,避免虚假边缘

2 计算图像的梯度与方向

这里使用了sobel算子来计算图像的梯度值

首先使用sobel算子计算中心像素点的两个方向上的梯度G_{x}和G_{y},然后就能够得到其具体的梯度值:

$$
G={\sqrt{G_{x}{}^{2}+G_{y}{}^{2}}}
$$

在OpenCV中,默认使用G=|G_{x}+G_{y}|来计算梯度值。

梯度的方向表示变化最快的方向角度:

$$
\theta = \arctan2\left( \frac{\partial f}{\partial y}, \frac{\partial f}{\partial x} \right)\
$$

为了简化计算过程,一般将其归为四个方向:水平方向、垂直方向、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°边缘;

3 非极大值抑制

排除伪边缘点

非极大值抑制的核心思想是:保留局部梯度方向上的最大值点,抑制其他非极大值点,主要用于细化边缘,将模糊的边缘响应转化为精确的单像素边缘。

对于每个像素点,检查其梯度方向上的相邻点:

  • 如果当前点的梯度幅值大于相邻点,则保留该点。

  • 否则,抑制(置为 0)该点。

4 双阈值筛选

双阈值筛选是 Canny 边缘检测算法中的核心步骤,用于确定哪些边缘是真实有效的,哪些是虚假的。该方法通过设置两个阈值(高阈值和低阈值),通常高阈值与低阈值的比例为 2:1 至 3:1,对梯度幅值进行分类,既能保留真实边缘,又能抑制噪声引起的虚假响应。

  • 低于低阈值的像素被排除。

  • 介于两者之间的像素,如果它能连接到一个高于阈值的边缘时,则被认为是边缘像素

因此,上图中的A和C是边缘,B不是边缘。因为C虽然不超过最高阈值,但其与A相连,所以C是边缘。

edges = cv2.Canny(image, threshold1, threshold2)

  • image:输入的灰度/二值化图像数据。

  • threshold1:低阈值,用于决定可能的边缘点。

  • threshold2:高阈值,用于决定强边缘点。

import cv2 as cv
# 读取图片
img = cv.imread('../images/image.png',cv.IMREAD_GRAYSCALE)
img = cv.resize(img,(1000,600))
#二值化处理
_,thresh = cv.threshold(img,127,255,cv.THRESH_OTSU,cv.THRESH_BINARY)
​
#使用Canny边缘检测
edges = cv.Canny(thresh,50,150)
cv.imshow('edges',edges)
cv.waitKey(0)
cv.destroyAllWindows()

3 图像轮廓

轮廓(Contour)是图像中连续的点集,用于表示物体的边界。

  • 边缘:图像中局部灰度变化剧烈的像素点集合(可能不连续)。

  • 轮廓连续的、闭合的边缘,用于表示物体的完整边界。

轮廓检测流程

  • 图像预处理(灰度化、高斯模糊)。

  • 边缘检测(如 Canny 算法)。

  • 轮廓查找(基于边缘二值图)。

  • 轮廓绘制或分析。

轮廓查找

contours,hierarchy = cv2.findContours(image,mode,method)

  • 参数

    • image:输入的二值图像(通常由 Canny 等边缘检测算法生成)。

    • mode:轮廓检索模式(如 cv2.RETR_TREEcv2.RETR_EXTERNAL)。

    • method:轮廓近似方法(如 cv2.CHAIN_APPROX_SIMPLEcv2.CHAIN_APPROX_NONE)。

  • 返回值

    • contours:检测到的轮廓列表,每个轮廓是一组点的数组。

    • hierarchy:轮廓的层级关系(可选)。

    • 对于第i条轮廓,hierarchy[i][0], hierarchy[i][1] , hierarchy[i][2] , hierarchy[i][3]分别表示其后一条轮廓、前一条轮廓、(同层次的第一个)子轮廓、父轮廓的索引(如果没有相应的轮廓,则对应位置为-1)。

选择合适的轮廓存储方法:

  1. 只需要外轮廓RETR_EXTERNAL(如计算物体总面积)
  2. 不需要层级关系RETR_LIST(如轮廓描边、特征提取)
  3. 需要完整嵌套关系RETR_TREE(如分析文档结构、嵌套图形)
  4. 简单内外层区分RETR_CCOMP(如带孔洞物体的分割)
1 轮廓检索模式

mode参数共有四个选项分别为:RETR_LIST,RETR_EXTERNAL,RETR_CCOMP,RETR_TREE。

1.1 RETR_EXTERNAL
  • 特点:只返回最外层轮廓,忽略所有内部轮廓(如孔洞)。

  • 层级结构:所有轮廓的 ParentFirst_Child 均为 -1

contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
1.2 RETR_LIST
  • 特点:检测所有轮廓,但不建立层级关系,所有轮廓平级存储。

  • 层级结构NextPrevious 指向同级轮廓,ParentFirst_Child 均为 -1

contours, hierarchy = cv2.findContours(binary, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
1.3 RETR_TREE
  • 特点:检测所有轮廓,并重建完整的层级树,每个轮廓都有明确的父子关系。

  • 层级结构:严格的树状结构,从顶层轮廓到最深层子轮廓。

contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
1.4 RETR_CCOMP
  • 特点:检测所有轮廓,但只建立两层结构:顶层为外部轮廓,第二层为内部孔洞。

  • 层级 0:所有外部轮廓(最外层的边界)。

  • 层级 1:所有内部轮廓(孔洞或嵌套的区域)。

contours, hierarchy = cv2.findContours(binary, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)

2 轮廓存储方法

轮廓存储方法(Contour Approximation Methods)用于控制轮廓点的存储精度和效率。

method参数一般有三个选项:CHAIN_APPROX_NONE、CHAIN_APPROX_SIMPLE、CHAIN_APPROX_TC89_L1。

  • CHAIN_APPROX_NONE 表示将所有的轮廓点都进行存储

  • CHAIN_APPROX_SIMPLE 表示只存储有用的点,比如直线只存储起点和终点,四边形只存储四个顶点,默认使用这个方法;

import cv2 as cv
#读图
img = cv.imread('../images/1.jpg')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
#二值化
_,binary = cv.threshold(gray,127,255,cv.THRESH_BINARY_INV)
​
#查找轮廓
contours,hierarchy = cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
print('轮廓数量:',len(contours))
print(hierarchy)
轮廓数量: 351
[[[  1  -1  -1  -1]
  [  2   0  -1  -1]
  [  3   1  -1  -1]
  ...
  [349 347  -1  -1]
  [350 348  -1  -1]
  [ -1 349  -1  -1]]]

绘制轮廓

轮廓找出来后,其实返回的是一个轮廓点坐标的列表,因此我们需要根据这些坐标将轮廓画出来,因此就用到了绘制轮廓的方法。

cv2.drawContours(image, contours, contourIdx, color, thickness)

  • image:要绘制轮廓的目标图像。

  • contours:从 cv2.findContours() 获取的轮廓列表,由点坐标构成的二维数组(numpy数组)。

  • contourIdx:要绘制的轮廓索引(-1 表示绘制所有轮廓)

  • color:绘制轮廓的颜色,可以是 BGR 值或者是灰度值(对于灰度图像)。

  • thickness:轮廓线的宽度,如果是正数,则画实线;如果是负数,则填充轮廓内的区域。

import cv2 as cv
#读图
img = cv.imread('../images/num.png')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
#二值化
_,binary = cv.threshold(gray,127,255,cv.THRESH_BINARY_INV)
​
#查找轮廓
contours,hierarchy = cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
​
#绘制轮廓
cv.drawContours(img,contours,-1,(0,0,255),5)
​
cv.imshow('img',img)
cv.waitKey(0)
cv.destroyAllWindows()

4 凸包

凸包其实是将一张图片中物体的最外层的点连接起来构成的凸多边形,它能包含物体中所有的内容。

给定平面上的点集 S,其凸包是包含 S 的最小凸多边形,满足多边形内任意两点的连线都在多边形内部。凸包的顶点均来自点集 S

  • 穷举法

  • QuickHull法

1.穷举法

  • 将集中的点进行两两配对,并进行连线,对于每条直线,检查其余所有的点是否处于该直线的同一侧,如果是,那么说明构成该直线的两个点就是凸包点,其余的线依次进行计算,从而获取所有的凸包点。

  • 对于点集 S 中的每对点 (p, q)也就是对于每个其他点 r,计算向量叉积

    $$
    (q - p) \times (r - p)\
    $$

     

    std=|向量a|*|向量b|*sin(θ),能控制std的正负的只能是θ,如果计算出来的std的正负都相同,说明这些点都在这条直线的同一侧,那么这两个点就是凸包的边界点。将满足条件的边连接成凸包

  • 实现简单,适用于小规模数据。

    效率极低,不适合大规模点集。

2.QuickHull法

  • 将所有点放在二维坐标系中,找到最左和最右的点 p 和 q,它们一定是凸包的顶点。

    (此时整个点集被分为两部分,直线上为上包,直线下为下包)

  • 递归处理每一部分,找到距离直线最远的点 r,将 r 加入凸包。

  • 继续分割子问题,直到没有点为止。

我们以点集来举例,假如有这么一些点,其分布如下图所示:

我用彩色线和数字表示顺序:

那么经过凸包检测并绘制之后,其结果应该如下图所示:

获取凸包点

cv2.convexHull(points)

  • points:输入参数,图像的轮廓

import cv2 as cv
#读图
img = cv.imread('../images/tu.png')
#灰度化
img = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
#二值化
_,binary = cv.threshold(img,127,255,cv.THRESH_BINARY)
#查找轮廓
contours,hierachy = cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
​
#获取凸包
hull = cv.convexHull(contours[0])
print(hull)
[[[378 113]]

 [[422 157]]

 [[430 217]]

 [[375 274]]

 [[221 316]]

 [[ 87 233]]

 [[ 85 163]]

 [[ 92  88]]

 [[317  61]]]

绘制凸包

cv2.polylines(image, pts, isClosed, color, thickness=1)

  • image:要绘制线条的目标图像,OpenCV格式的二维图像数组(如numpy数组)。

  • pts:凸包的二维 numpy 数组,每个元素是一维数组,代表一个多边形的一系列顶点坐标。[pts1, pts2, ...]

  • isClosed:布尔值,表示是否闭合多边形,如果为 True,会在最后一个顶点和第一个顶点间自动添加一条线段,形成封闭的多边形。

  • color:线条颜色,BGR 元组(如 (255, 0, 0) 表示蓝色)或灰度值(单通道图像)

  • thickness(可选):线条宽度,默认值为1。

import cv2 as cv
#读图
img = cv.imread('../images/tu.png')
#灰度化
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
#二值化
_,binary = cv.threshold(gray,127,255,cv.THRESH_BINARY)
#查找轮廓
contours,hierachy = cv.findContours(binary,cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
​
#获取凸包
hull = cv.convexHull(contours[0])
​
#绘制凸包
cv.polylines(img,[hull],True,(0,255,0),5)
​
cv.imshow('tu',img)
cv.waitKey(0)
cv.destroyAllWindows()


网站公告

今日签到

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