传统计算机视觉
目录
- 传统计算机视觉
- 图像和像素
- 直方图与点运算
- 邻域运算与图像滤波(Image Filtering)
- 边缘检测与图像梯度
- 特征点提取与描述
- 图像形态学处理与分割基础
- 特征匹配(Feature Matching)
- 结束任务
- 实践总结
传统计算机视觉
图像和像素
在深入学习复杂的算法之前,我们必须先了解计算机视觉处理的对象:数字图像。
1. 数字图像
对于计算机而言,图像不是我们肉眼看到的连续的画面,而是由有限数量的元素组成的二维矩阵,这些元素被称为像素 (Pixel)。
-
二维矩阵: 图像可以被看作是一个巨大的表格,有行(高度)和列(宽度)。
-
像素 (Pixel): 矩阵中的每一个“格子”就是一个像素点,它是图像的最小单位。每个像素点存储了该位置的颜色信息或灰度信息。
2. 图像的类型
在传统CV中,我们最常处理以下两种图像类型:
A. 灰度图像 (Grayscale Image)
-
组成: 每个像素只包含一个数值,通常在 0 到 255 之间。
-
含义: 这个数值代表该点的亮度(或称灰度)。
-
0 表示 纯黑色 (Black)
-
255 表示 纯白色 (White)
-
中间值 表示不同程度的灰色。
-
-
存储: 只需要一个二维矩阵($H \times W$)。
B. 彩色图像 (Color Image)
-
组成: 每个像素包含三个独立的数值,分别代表红 (Red)、绿 (Green)、蓝 (Blue) 三种颜色分量,简称 RGB。
-
含义: 这三个数值(通常也是 0-255)组合起来,就能表示出自然界中的几乎所有颜色。
- 例如,(255, 0, 0) 是纯红色;(0, 0, 0) 是黑色;(255, 255, 255) 是白色。
-
存储: 需要一个三维矩阵($H \times W \times 3$),其中 3 就是指 R、G、B 这三个通道 (Channel)
3. 图像预处理的起点:操作像素
明白了图像就是数字矩阵后,传统CV的第一步——图像预处理,就变得容易理解了。许多预处理操作本质上都是对这些像素值进行数学运算。
基础操作示例:
-
图像反转 (Image Inversion): 将亮的地方变暗,暗的地方变亮。
-
对于一个灰度图像的像素值 $P$,其反转后的像素值 $P'$ 为:
$$P' = 255 - P$$
-
比如,亮度为 10 的黑点 (接近黑) 变成 245 (接近白),亮度为 200 的灰点变成 55。
-
-
图像缩放 (Scaling): 改变图像的尺寸,这涉及到插值计算,即根据周围像素的值来估计新位置像素的值。
- 类型转化 :绿光敏感 $$P_{\text{灰度}} = 0.299 \times R + 0.587 \times G + 0.114 \times B$$
直方图与点运算
1. 图像直方图 (Image Histogram)
直方图是计算机视觉中最基本、最重要的一种统计工具。
A. 直方图
对于一个灰度图像(像素值范围 $0 \sim 255$),直方图是一个柱状图,它统计了图像中每个灰度级出现的频率或次数。
-
横轴 (X 轴): 表示灰度级(从 0 到 255)。
-
纵轴 (Y 轴): 表示该灰度级对应的像素数量。
B. 直方图的意义
通过观察直方图,我们可以快速了解图像的整体特性:
| 直方图特征 | 图像表现 | 调整需求 |
|---|---|---|
| 集中在低灰度区 (0 附近) | 图像整体偏暗,缺乏高亮细节。 | 需要提高亮度。 |
| 集中在高灰度区 (255 附近) | 图像整体偏亮或曝光过度。 | 可能需要降低亮度。 |
| 分布集中在一个窄小的区域 | 对比度低,画面“灰蒙蒙”的,细节不清晰。 | 需要增强对比度。 |
| 分布均匀,覆盖整个 0~255 范围 | 对比度高,细节丰富,视觉效果好。 | 理想的图像。 |
直方图是很多高级图像处理算法(比如图像分割、阈值处理)的基础。
2. 点运算 (Point Operations)
点运算,顾名思义,是只根据输入图像中一个点的像素值,来决定输出图像中对应点的像素值的运算。它不考虑周围像素的值。
A. 亮度调整 (Brightness Adjustment)
这是最简单的点运算,通过对所有像素值统一进行加法或减法实现。
-
设输入像素值为 $P_{\text{in}}$,输出像素值为 $P_{\text{out}}$,调节量为 $b$: $$P_{\text{out}} = P_{\text{in}} + b$$
-
当 $b > 0$ 时: 图像变亮。
-
当 $b < 0$ 时: 图像变暗。
-
注意: 运算结果必须进行截断 (Clipping),确保 $0 \le P_{\text{out}} \le 255$。例如,如果 $P_{\text{in}} = 240$,$b = 30$,那么 $P_{\text{out}}$ 应该是 255,而不是 270。
B. 对比度调整 (Contrast Adjustment)
通过对像素值进行乘法运算来增强或减弱对比度。
-
设调节因子为 $\alpha$ ($\alpha > 0$): $$P_{\text{out}} = \alpha \times P_{\text{in}}$$
-
当 $\alpha > 1$ 时: 像素值之间的差距被放大,对比度增强。
-
当 $\alpha < 1$ 时: 像素值之间的差距被缩小,对比度减弱。
-
同样需要进行 $0 \sim 255$ 的截断处理。
C. 直方图均衡化 (Histogram Equalization)
直方图均衡化是一种自动增强对比度的技术。
-
目的: 自动找到一个变换函数,将图像的原始直方图拉伸并使其尽量均匀地分布在整个 $0 \sim 255$ 灰度范围内。
-
效果: 它可以显著地增强那些原来对比度较低的图像的细节,特别是在医学图像处理和旧照片增强中非常有用。
邻域运算与图像滤波(Image Filtering)
邻域运算是传统计算机视觉的核心,它允许我们利用像素周围的信息来计算和修改当前像素的值。
1. 邻域运算
邻域运算(或称区域运算)的核心思想是:输出图像中一个像素的值,取决于输入图像中该像素及其周围邻近像素的值。(不会改变大小)
这个“周围邻近像素”的区域,我们称之为邻域 (Neighborhood)。
- 实现工具: 我们使用一个小的矩阵,称为核 (Kernel)、滤波器 (Filter) 或模板 (Mask),在整个图像上滑动,并进行卷积 (Convolution) 运算。实际上,卷积输出通常会改变图像大小,因为卷积移动受原图像边界影响,当然,我们通常采用填充(padding)来保持大小不变。
2. 图像滤波 (Image Filtering)
图像滤波是邻域运算最常见的应用。它的主要目的是:突出图像的某些特征或抑制图像中的噪声。
根据处理效果,图像滤波可以分为两大类:平滑滤波(去噪、模糊)和锐化滤波(突出边缘、细节)。
3. 平滑滤波 (Smoothing Filters)
平滑滤波主要用于降噪(去除图像中的随机噪声)和模糊。
A. 均值滤波 (Mean Filter / Box Filter)
-
核: 一个所有元素值都相等的矩阵(如 3x3 矩阵,每个元素是 $1/9$)。
-
原理: 用邻域内所有像素值的平均值来替代中心像素的值。
-
效果:
-
优点: 简单高效,能有效去除高斯(随机)噪声。
-
缺点: 会导致图像细节模糊,特别是边缘信息丢失严重。
-
B. 高斯滤波 (Gaussian Filter)
- 核: 权重不是平均分配的,而是根据高斯分布(钟形曲线)计算得出的。距离中心像素越近的像素,权重越大。 高斯核的权重分布模仿了自然界中常见的钟形曲线。对于图像中的一个点 $(x, y)$,它的权重值 $G(x, y)$ 由以下公式给出:
$$G(x, y) = \frac{1}{2\pi\sigma^2} e^{-\frac{x^2 + y^2}{2\sigma^2}}$$
$x$ 和 $y$:表示像素点距离中心点的水平和垂直距离。
$\sigma$ (Sigma):是标准差,它是高斯滤波器的关键参数,决定了权重衰减的速度,也就是滤波器的平滑程度和作用范围。
$\sigma$ 越大:权重分布越平坦,滤波器的作用范围越大,图像越模糊。
$\sigma$ 越小:权重分布越集中在中心,作用范围小,模糊程度低。
-
原理: 是一种加权平均,它比均值滤波更自然、平滑。
-
效果:
- 优点: 降噪效果好,同时相比均值滤波,它对图像的模糊效应更柔和、更符合视觉习惯。它是实现图像尺度空间(Scale-Space)表示的基础。
-
实际 3x3 高斯核示例
-
假设我们有一个 $\sigma = 0.8$ 的 3x3 高斯核(为方便展示,这里是未归一化的整数近似值):
| 1 | 2 | 1 |
|---|---|---|
| 2 | 4 | 2 |
| 1 | 2 | 1 |
中心 (4) 权重最高。
角落 (1) 权重最低。
归一化系数(所有元素之和):$1 + 2 + 1 + 2 + 4 + 2 + 1 + 2 + 1 = 16$
归一化后的核:将每个元素除以 16。
$$K_{\text{Gaussian}} = \frac{1}{16} \begin{bmatrix} 1 & 2 & 1 \ 2 & 4 & 2 \ 1 & 2 & 1 \end{bmatrix}$$
- 为什么高斯滤波有用?谈谈我的理解:当高斯滤波器滑过图像时,它会强调中心像素点,但同时也会顾及周围像素。当它处理噪声点时,虽然中心突出了,但是这只是少数噪点(而均值滤波中噪声点对其他点的危害是同样比重的,虽然这两种滤波都会对噪点有稀释);当处理正常点时,中心突出能够避免周围噪声干扰。比如有100个像素点,10个噪点,高斯滤波只会有10个输出明显噪点,而均值滤波则会有远高于10个的输出噪点(原来10个噪点的边缘都会被影响)
C. 中值滤波 (Median Filter)
-
核: 同样关注一个邻域窗口(如 3x3)。
-
原理: 不计算平均值,而是将邻域内的所有像素值进行排序,然后用中间值来替代中心像素的值。
-
效果:
-
优点: 对椒盐噪声 (Salt-and-Pepper Noise)(即图像中随机出现的黑白点)具有极强的抑制能力,因为它会直接“忽略”掉极端值。
-
缺点: 可能会在平坦区域产生一些失真。
-
特别强调: 中值滤波是非线性滤波的一种,它在去噪的同时能更好地保留图像的边缘,因为它不会对边缘处的急剧变化进行平均化处理。
-
4. 锐化滤波 (Sharpening Filters)
锐化滤波主要用于增强图像的边缘和细节。
-
原理: 锐化操作通常通过微分(或梯度)来实现。在离散图像中,微分是用差分来近似的。图像变化剧烈的地方(即边缘)差分值大;变化平坦的地方(即区域内部)差分值小。
-
拉普拉斯算子 (Laplacian Operator):
-
这是一个非常著名的二阶微分算子,常用于边缘检测和锐化。
-
核示例 (3x3):
$$\begin{bmatrix} 0 & 1 & 0 \ 1 & -4 & 1 \ 0 & 1 & 0 \end{bmatrix} \quad \text{或} \quad \begin{bmatrix} 1 & 1 & 1 \ 1 & -8 & 1 \ 1 & 1 & 1 \end{bmatrix}$$
-
-
效果: 计算结果的绝对值越大,表示该点越可能是边缘点。通过将拉普拉斯结果与原图叠加,可以实现锐化。
边缘检测与图像梯度
现在,我们将进入特征提取的领域,这标志着我们从“去噪”到“理解”的转变。
1. 什么是边缘 (Edge)?
在数字图像中,边缘指的是图像中像素值发生剧烈变化(即不连续)的区域。
- 例如,从一个物体的表面(亮)过渡到背景(暗)的地方。
- 边缘是图像中物体轮廓的集合,是图像中最具信息量的结构之一。
2. 图像梯度 (Image Gradient)
边缘的本质是灰度值的变化率。在数学上,变化率是通过微分来描述的。在离散的数字图像中,我们用梯度 (Gradient) 来近似微分。
图像梯度是一个向量,它包含两个重要的信息:
- 梯度大小 (Magnitude): 像素变化的速度有多快(即边缘的强度)。
- 梯度方向 (Direction): 像素值变化的方向(即边缘的方向,通常垂直于边缘方向)。
对于一个图像函数 $I(x, y)$,它的梯度 $\nabla I$ 可以表示为:
$$\nabla I(x, y) = \begin{bmatrix} G_x \ G_y \end{bmatrix} = \begin{bmatrix} \frac{\partial I}{\partial x} \ \frac{\partial I}{\partial y} \end{bmatrix}$$
- $G_x$: 图像在水平方向($x$ 方向)的变化率。
- $G_y$: 图像在垂直方向($y$ 方向)的变化率。
梯度的大小 (Magnitude) $M$ 用于衡量边缘的强度:
$$M = \sqrt{G_x^2 + G_y^2}$$
梯度方向 (Direction) $\theta$ 给出边缘的方向:
$$\theta = \arctan \left( \frac{G_y}{G_x} \right)$$
3. 经典的边缘检测算子 (Edge Detectors)
为了计算 $G_x$ 和 $G_y$,我们需要使用卷积核(即我们第三课学习的邻域运算)。这些核就是著名的微分算子。
A. Sobel 算子
Sobel 算子是最常用的梯度近似核之一。它兼顾了微分(用于边缘检测)和平滑(用于抑制噪声)。
微分(提取边缘)
首先,Sobel 算子的主要目的是计算图像的梯度(即微分或差分),以检测边缘。
- 一个最简单的差分核(用于提取 $x$ 方向梯度)是:
$$K_{\text{SimpleDiff}} = \begin{bmatrix} -1 & 0 & 1 \end{bmatrix}$$
- 当这个核在图像上滑动时,它计算的是右边像素值减去左边像素值,完美地近似了微分。
现在,我们把这个思想扩展到二维,一个 $3 \times 3$ 的简单差分核(只考虑最中心的行)可以写成:$$\begin{bmatrix} ? & ? & ? \ -1 & 0 & 1 \ ? & ? & ? \end{bmatrix}$$
如果 Sobel 算子只是简单地在所有行上重复这个 $[-1, 0, 1]$,它就会变成:
$$K_{\text{PureDiff}} = \begin{bmatrix} -1 & 0 & 1 \ -1 & 0 & 1 \ -1 & 0 & 1 \end{bmatrix}$$
这个纯差分核虽然能完美提取梯度,但它有一个致命弱点:对噪声过于敏感。 图像中的任何微小噪声都会被微分操作放大,导致检测到大量虚假边缘。
为了解决这个问题,Sobel 算子引入了平滑(Smoothing)操作。
Sobel 算子(以 $G_x$ 为例)如下:
$$K_x = \begin{bmatrix} -1 & 0 & 1 \ \mathbf{-2} & \mathbf{0} & \mathbf{2} \ -1 & 0 & 1 \end{bmatrix}$$
你可以将 Sobel 算子看作是两个独立操作的乘积:
- 水平方向:差分操作 $\begin{bmatrix} -1 & 0 & 1 \end{bmatrix}$ (提取梯度)
- 垂直方向:平滑操作 $\begin{bmatrix} 1 \ 2 \ 1 \end{bmatrix}$ (高斯滤波进行平滑)
核的分解:
$$K_x = \begin{bmatrix} -1 \ -2 \ -1 \end{bmatrix} \times \begin{bmatrix} 1 & 0 & -1 \end{bmatrix} \quad \text{(或转置后相乘)}$$
- 在垂直方向($y$ 轴),它使用了 $\begin{bmatrix} 1 \ 2 \ 1 \end{bmatrix}$ 的权重。
为什么是 $\begin{bmatrix} 1 \ 2 \ 1 \end{bmatrix}$?
$\begin{bmatrix} 1 & 2 & 1 \end{bmatrix}$ 是一个一维高斯滤波器核(非归一化)。
- $G_x$(水平方向微分核): 强调垂直边缘
$$K_x = \begin{bmatrix} -1 & 0 & 1 \ -2 & 0 & 2 \ -1 & 0 & 1 \end{bmatrix}$$
- $G_y$(垂直方向微分核): 强调水平边缘
$$K_y = \begin{bmatrix} -1 & -2 & -1 \ 0 & 0 & 0 \ 1 & 2 & 1 \end{bmatrix}$$
步骤:
- 用 $K_x$ 对图像卷积,得到 $G_x$ 图像。
- 用 $K_y$ 对图像卷积,得到 $G_y$ 图像。
- 结合 $G_x$ 和 $G_y$ 计算最终的边缘强度 $M$。
B. Canny 边缘检测器(最优边缘检测器)
Canny 算法被公认为是一种最优的边缘检测算法,因为它满足了三个核心标准:良好的检测率、良好的定位准确性、最小化虚假边缘。
Canny 算法包含四个关键步骤:
- 高斯平滑: 预先降噪,避免噪声引起虚假边缘。
- 计算梯度强度和方向: 使用类似 Sobel 的算子计算 $M$ 和 $\theta$。
- 非极大值抑制 (Non-Maximum Suppression, NMS): 细化边缘。在梯度方向上,只保留梯度局部最大的像素点(即最陡峭的点),抑制其他点,从而将粗边缘细化为单像素宽的细边缘。
- 双阈值连接 (Hysteresis Thresholding): 使用高低两个阈值 $T_{high}$ 和 $T_{low}$ 来确定最终的边缘:
- 强度 $> T_{high}$ 的点:确定为边缘点。
- 强度 $< T_{low}$ 的点:确定为非边缘点。
- 强度在 $T_{low}$ 和 $T_{high}$ 之间的点:只有当它们与确定边缘点相连时,才被保留为边缘点。这确保了边缘的连续性。
特征点提取与描述
1. 特征点 (Feature Point)
特征点(也叫兴趣点 Interest Point)是图像中具有显著性、可重复性、不变性的像素点或小区域。它们是图像中最有信息量、最容易在不同图像中被识别出来进行匹配的点。
通常,特征点可以分为几类:
A. 角点 (Corner)
- 定义: 像素值在两个主要方向(水平和垂直)上都发生剧烈变化的点。
- 特性: 角点的位置精度高,并且具有天然的方向性。
- 应用: 图像配准(Image Registration)、物体跟踪、三维重建。
B. 斑点/区域 (Blob/Region)
- 定义: 具有特定形状或尺寸的局部区域,通常与周围环境有明显的对比度差异。
- 特性: 具有尺度不变性(在不同缩放级别下都能找到)。
- 应用: 物体识别、目标检测。
2. 经典角点检测:Harris 角点检测器
Harris 角点检测器是传统 CV 中最著名、最鲁棒的角点检测算法,至今仍被用于很多基础应用。
0. 基本思想
Harris 算法的核心思想是:如果在图像中的某个像素点 $(x, y)$ 附近放置一个小的滑动窗口,无论这个窗口向哪个方向移动(注意,这里是双向滑动),窗口内的像素灰度值都会发生显著变化,那么这个点就是角点。
区域独特性 (Uniqueness of Information)
想象一下图像中的三种基本区域:
| 区域类型 | 移动窗口时的灰度变化 E(Δx,Δy) | 特性 |
|---|---|---|
| 平坦区域 (Flat) | 任何方向移动,灰度变化都非常小。 | 不可追踪: 区域内所有点都长一样,你无法确定窗口移动后停在哪里。 |
| 边缘区域 (Edge) | 沿着边缘方向移动,灰度变化非常小。 | 一维追踪: 只能沿着垂直于边缘的方向进行追踪和定位,沿着边缘方向则无法定位。 |
| 角点区域 (Corner) | 任何方向移动,灰度变化都非常大。 | 二维追踪: 信息独一无二,无论图像如何小幅度移动,我们都能确定它位于窗口内的哪个新位置。 |
1. 预处理:计算图像梯度
Harris 算法的第一步是获取图像的梯度信息,通常使用我们学过的 Sobel 算子:
-
计算图像在水平方向的梯度 $I_x$。
-
计算图像在垂直方向的梯度 $I_y$。
2. 核心:定义移动窗口的能量(灰度变化)
考虑一个以像素 $(x, y)$ 为中心的小窗口 $W$。如果我们将这个窗口向任意方向 $(\Delta x, \Delta y)$ 移动一小段距离,窗口内所有像素的灰度变化 $E(\Delta x, \Delta y)$ 可以通过泰勒级数近似计算。
$$E(\Delta x, \Delta y) = \sum_{u, v \in W} w(u, v) [I(u+\Delta x, v+\Delta y) - I(u, v)]^2$$
-
$I(u, v)$: 窗口内像素点的原始灰度值。
-
$w(u, v)$: 窗口函数(通常是高斯函数),用于给中心附近的像素更高的权重,以增加鲁棒性。
经过复杂的泰勒级数展开和简化,这个能量函数 $E(\Delta x, \Delta y)$ 可以被近似为二次形式:
$$E(\Delta x, \Delta y) \approx [\Delta x, \Delta y] \cdot M \cdot \begin{bmatrix} \Delta x \ \Delta y \end{bmatrix}$$
这里的 $M$ 就是算法的核心——结构张量 (Structure Tensor)。
3. 结构张量 (Structure Tensor) $M$
结构张量 $M$ 是一个 $2 \times 2$ 的对称矩阵,它捕捉了窗口内所有像素的梯度分布信息。
$$M = \sum_{u, v \in W} w(u, v) \begin{bmatrix} I_x^2 & I_x I_y \ I_x I_y & I_y^2 \end{bmatrix}$$
-
$A = \sum w(u, v) I_x^2$ (表示水平方向梯度的强度总和)
-
$C = \sum w(u, v) I_y^2$ (表示垂直方向梯度的强度总和)
-
$B = \sum w(u, v) I_x I_y$ (表示水平和垂直梯度之间的相关性)
$$M = \begin{bmatrix} A & B \ B & C \end{bmatrix}$$
4. 关键:特征值 $\lambda_1$ 和 $\lambda_2$ 的物理意义
对 $M$ 矩阵进行特征值分解,得到两个特征值 $\lambda_1$ 和 $\lambda_2$。这两个特征值具有重要的物理意义:
-
$\lambda_1$ 和 $\lambda_2$ 的大小: 表示 $E(\Delta x, \Delta y)$ 的变化率(即灰度变化有多剧烈)。
-
特征向量的方向: 指示灰度变化最快和最慢的方向。
判断标准:
-
平坦区域: 无论往哪个方向移动,灰度变化都很小。 $\lambda_1$ 和 $\lambda_2$ 都很小。
-
边缘区域: 沿着边缘方向移动,灰度变化小;垂直于边缘方向移动,灰度变化大。一个特征值大,另一个特征值小(例如 $\lambda_1 \gg \lambda_2 \approx 0$)。
-
角点区域: 无论往哪个方向移动,灰度变化都很大。 $\lambda_1$ 和 $\lambda_2$ 都很大。
5. Harris 角点响应函数 (Corner Response Function, R)
计算特征值 $\lambda_1$ 和 $\lambda_2$ 的计算量很大。Harris 提出了一种更高效的方法,使用矩阵的行列式 (Determinant) 和迹 (Trace) 来近似这个判断标准:
$$R = \det(M) - k \cdot (\text{trace}(M))^2$$
-
$\det(M) = AC - B^2 = \lambda_1 \lambda_2$
-
$\text{trace}(M) = A + C = \lambda_1 + \lambda_2$
-
$k$: 经验常数,通常在 $[0.04, 0.06]$ 之间取值。它用于调节对角点锐利度的敏感性。
| R 值结果 | 特征值 (λ1,λ2) | 图像结构 |
|---|---|---|
| $R \approx 0$ | $\lambda_1 \approx 0, \lambda_2 \approx 0$ | 平坦区域 |
| $R < 0$ | $\lambda_1 \gg \lambda_2 \approx 0$ 或 $\lambda_2 \gg \lambda_1 \approx 0$ | 边缘区域 |
| $R$ 为大的正值 | $\lambda_1$ 和 $\lambda_2$ 都很大 | 角点区域 |
6. 后处理:筛选和定位角点
最后,Harris 算法使用以下步骤提取最终的角点:
-
阈值化: 设定一个阈值 $T$,保留所有 $R > T$ 的像素点。
-
非极大值抑制 (Non-Maximum Suppression, NMS): 由于真正的角点在 $R$ 响应图上是一个“小山丘”而不是一个点,我们需要对 $R$ 图进行 NMS 处理。在一个小邻域内,只保留 $R$ 值最大的那个点,从而确保每个角点只被检测一次。
3. Shi-Tomasi角点检测
一、算法背景与动机
1. Harris算法的局限性
Harris角点检测虽然经典,但存在一些问题: - 响应函数$R = \det(M) - k \cdot \text{trace}(M)^2$中的$k$需要经验调整 - 角点质量评估不够直观 - 无法直接控制返回的角点数量
2. Shi-Tomasi的改进思路
1994年,Jianbo Shi和Carlo Tomasi在论文《Good Features to Track》中提出:
"与其使用复杂的响应函数,不如直接分析自相关矩阵$M$的特征值"
核心思想:角点的质量由最小特征值决定: - 如果$\min(\lambda_1, \lambda_2)$很大 → 是好的角点 - 如果$\min(\lambda_1, \lambda_2)$很小 → 不是好的角点
二、数学原理深度解析
1. 自相关矩阵$M$的特征值分析
回顾自相关矩阵: $$M = \sum_{x,y} w(x, y) \begin{bmatrix} I_x^2 & I_xI_y \ I_xI_y & I_y^2 \end{bmatrix}$$
特征值$\lambda_1$和$\lambda_2$表示两个主方向的灰度变化强度:
特征值空间:
λ₂
↑
│ 角点区域
│ (λ₁大, λ₂大)
│
├───────────── 边缘区域
│ (λ₁大, λ₂小)
│
└─────────────→ λ₁
平坦区域
(λ₁小, λ₂小)
2. Shi-Tomasi响应函数
$$R_{\text{ST}} = \min(\lambda_1, \lambda_2)$$
为什么是最小特征值? - 在角点区域:$\lambda_1$和$\lambda_2$都大 → $\min(\lambda_1, \lambda_2)$也大 - 在边缘区域:一个特征值大,一个特征值小 → $\min(\lambda_1, \lambda_2)$小 - 在平坦区域:两个特征值都小 → $\min(\lambda_1, \lambda_2)$小
这样,只需要一个阈值就能区分角点和其他区域!
三、算法实现步骤
import cv2
import numpy as np
import matplotlib.pyplot as plt
def shi_tomasi_detailed(image_path, max_corners=100, quality_level=0.01, min_distance=10):
"""
Shi-Tomasi角点检测详细实现
参数详解:
max_corners: 返回的最大角点数量(0表示无限制)
quality_level: 角点质量水平(0-1),值越小要求越高
min_distance: 角点之间的最小欧氏距离(像素)
"""
# 1. 读取图像
img = cv2.imread(image_path)
if img is None:
raise ValueError(f"无法读取图像: {image_path}")
# 2. 转换为灰度
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 3. Shi-Tomasi角点检测
# cv2.goodFeaturesToTrack() 就是Shi-Tomasi的实现
corners = cv2.goodFeaturesToTrack(gray, max_corners, quality_level, min_distance)
# 4. 转换为整数坐标
corners_int = np.int0(corners) if corners is not None else []
# 5. 绘制角点
img_marked = img.copy()
for corner in corners_int:
x, y = corner.ravel()
# 绘制不同大小的圆表示响应强度
cv2.circle(img_marked, (x, y), 5, (0, 255, 0), -1) # 内圆
cv2.circle(img_marked, (x, y), 7, (255, 0, 0), 2) # 外圆边框
return {
'image': img_marked,
'corners': corners,
'corners_int': corners_int,
'gray': gray,
'corner_count': len(corners_int) if corners_int is not None else 0
五、算法内部工作原理(手动实现)
def shi_tomasi_manual_implementation(image, max_corners=100, quality_level=0.01, min_distance=10):
"""
手动实现Shi-Tomasi算法,理解内部原理
"""
# 1. 计算图像梯度
I_x = cv2.Sobel(image, cv2.CV_32F, 1, 0, ksize=3)
I_y = cv2.Sobel(image, cv2.CV_32F, 0, 1, ksize=3)
# 2. 计算自相关矩阵元素
Ixx = I_x * I_x
Iyy = I_y * I_y
Ixy = I_x * I_y
# 3. 高斯平滑(窗口函数)
window_size = 3
kernel = cv2.getGaussianKernel(window_size, 1.5)
kernel_2d = kernel * kernel.T
Sxx = cv2.filter2D(Ixx, -1, kernel_2d)
Syy = cv2.filter2D(Iyy, -1, kernel_2d)
Sxy = cv2.filter2D(Ixy, -1, kernel_2d)
# 4. 计算每个像素的响应值(最小特征值)
height, width = image.shape
R = np.zeros_like(image, dtype=np.float32)
for y in range(height):
for x in range(width):
# 构建自相关矩阵
M = np.array([
[Sxx[y, x], Sxy[y, x]],
[Sxy[y, x], Syy[y, x]]
])
# 计算特征值
eigenvalues = np.linalg.eigvals(M)
# Shi-Tomasi响应:最小特征值
R[y, x] = np.min(eigenvalues)
# 5. 非极大值抑制
corners = []
# 根据quality_level设置阈值
max_response = np.max(R)
threshold = quality_level * max_response
# 创建响应图副本用于非极大值抑制
R_suppressed = R.copy()
# 找到所有超过阈值的点
candidate_points = np.argwhere(R > threshold)
# 按响应值排序
candidate_responses = R[candidate_points[:, 0], candidate_points[:, 1]]
sorted_indices = np.argsort(-candidate_responses) # 降序
for idx in sorted_indices:
if len(corners) >= max_corners:
break
y, x = candidate_points[idx]
# 检查最小距离
too_close = False
for corner in corners:
cy, cx = corner
dist = np.sqrt((x - cx)**2 + (y - cy)**2)
if dist < min_distance:
too_close = True
break
if not too_close:
corners.append([x, y]) # 注意:OpenCV使用(x, y)顺序
return np.array(corners, dtype=np.float32).reshape(-1, 1, 2), R
六、特征值计算优化
在实际实现中,直接计算特征值开销大,使用以下公式:
$$\min(\lambda_1, \lambda_2) = \frac{1}{2} \left[ (\lambda_1 + \lambda_2) - \sqrt{(\lambda_1 - \lambda_2)^2} \right]$$
其中:(利用$A - I \lambda$ 的行列式为0求解) - $\lambda_1 + \lambda_2 = \text{trace}(M) = S_{xx} + S_{yy}$ - $\lambda_1 - \lambda_2 = \sqrt{(S_{xx} - S_{yy})^2 + 4S_{xy}^2}$
所以: $$\min(\lambda_1, \lambda_2) = \frac{1}{2} \left[ (S_{xx} + S_{yy}) - \sqrt{(S_{xx} - S_{yy})^2 + 4S_{xy}^2} \right]$$
def compute_shi_tomasi_response_optimized(Sxx, Syy, Sxy):
"""
优化计算Shi-Tomasi响应值(避免特征值分解)
"""
trace = Sxx + Syy
diff = Sxx - Syy
discriminant = np.sqrt(diff**2 + 4 * Sxy**2)
# 最小特征值
lambda_min = 0.5 * (trace - discriminant)
# 避免数值误差导致的负数
lambda_min = np.maximum(lambda_min, 0)
return lambda_min
七、Shi-Tomasi的优点总结
- 更稳定的角点质量评估
- 直接使用最小特征值,物理意义明确
-
不需要经验参数$k$
-
更好的控制能力
- 可以精确控制返回的角点数量
-
质量阈值直观(百分比形式)
-
计算效率
- 响应计算比Harris更简单
-
非极大值抑制更有效
-
在实际应用中的优势
- 在视觉跟踪(如KLT跟踪器)中表现更好
- 角点分布更均匀(得益于最小距离约束)
你说得对!我跳过了FAST角点检测的基础讲解,直接讲ORB了。这是我的疏忽,让我们先补上FAST角点检测的基础知识。
4. FAST角点检测
一、FAST是什么?
FAST = Features from Accelerated Segment Test
- 提出时间:2006年(Rosten和Drummond)
- 核心思想:快速检测角点,适合实时应用
- 特点:速度极快,是ORB算法的基础
二、FAST算法原理
1. 基本检测规则
检测像素点$p$周围16个像素的亮度变化:
周围像素编号(半径为3的圆):
15 00 01
14 02
13 03
12 p 04
11 05
10 06
09 08 07
2. 检测条件
对于中心像素$p$的亮度$I_p$: - 如果连续N个像素都比$I_p + t$亮 - 或者连续N个像素都比$I_p - t$暗 - 那么$p$就是一个角点
常用设置:$N = 12$(FAST-12),$t$是阈值
三、FAST检测步骤
import cv2
import numpy as np
def fast_corner_detection_simple(image, threshold=20):
"""
简化版FAST角点检测(帮助理解原理)
"""
if len(image.shape) == 3:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
else:
gray = image
height, width = gray.shape
corners = []
# 周围16个像素的偏移量(半径为3的圆)
circle_offsets = [
(0, -3), (1, -3), (2, -2), (3, -1),
(3, 0), (3, 1), (2, 2), (1, 3),
(0, 3), (-1, 3), (-2, 2), (-3, 1),
(-3, 0), (-3, -1), (-2, -2), (-1, -3)
]
# 遍历图像(忽略边缘)
for y in range(3, height - 3):
for x in range(3, width - 3):
center_intensity = gray[y, x]
# 检查16个像素
brighter_count = 0
darker_count = 0
for dx, dy in circle_offsets:
neighbor_intensity = gray[y + dy, x + dx]
if neighbor_intensity > center_intensity + threshold:
brighter_count += 1
elif neighbor_intensity < center_intensity - threshold:
darker_count += 1
# FAST-12检测:连续12个像素满足条件
# 简化:只要总数≥12就认为是角点
if brighter_count >= 12 or darker_count >= 12:
corners.append((x, y))
return corners
四、OpenCV中的FAST使用
def fast_with_opencv(image, threshold=20):
"""
使用OpenCV的FAST检测器
"""
# 创建FAST检测器
fast = cv2.FastFeatureDetector_create(
threshold=threshold, # 阈值
nonmaxSuppression=True, # 非极大值抑制
type=cv2.FAST_FEATURE_DETECTOR_TYPE_9_16 # 检测类型
)
# 检测关键点
keypoints = fast.detect(image, None)
# 绘制关键点
img_with_keypoints = cv2.drawKeypoints(
image,
keypoints,
None,
color=(0, 255, 0), # 绿色
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS
)
return img_with_keypoints, keypoints
五、FAST算法优化
1. 快速拒绝策略
先检查1、5、9、13号像素(上下左右): - 如果这4个像素中至少有3个不满足条件 - 那么$p$肯定不是角点,直接跳过
def fast_with_quick_reject(image, threshold=20):
"""带快速拒绝的FAST检测"""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
height, width = gray.shape
corners = []
# 快速检查的4个像素(1, 5, 9, 13号)
quick_offsets = [(0, -3), (3, 0), (0, 3), (-3, 0)] # 上、右、下、左
for y in range(3, height - 3):
for x in range(3, width - 3):
center_intensity = gray[y, x]
# 快速拒绝:检查4个关键像素
quick_count = 0
for dx, dy in quick_offsets:
neighbor = gray[y + dy, x + dx]
if (neighbor > center_intensity + threshold or
neighbor < center_intensity - threshold):
quick_count += 1
# 如果少于3个,肯定不是角点
if quick_count < 3:
continue
# 完整检查16个像素
# ...(完整检测代码)
return corners
2. 非极大值抑制
防止在角点附近检测到多个响应:
def non_maximum_suppression(corners, responses, min_distance=10):
"""
非极大值抑制
corners: 角点坐标列表 [(x1, y1), (x2, y2), ...]
responses: 角点响应值列表
min_distance: 最小距离
"""
if not corners:
return []
# 按响应值降序排序
sorted_indices = np.argsort(responses)[::-1]
suppressed_corners = []
for idx in sorted_indices:
x, y = corners[idx]
keep = True
# 检查是否与已保留的角点太近
for cx, cy in suppressed_corners:
dist = np.sqrt((x - cx)**2 + (y - cy)**2)
if dist < min_distance:
keep = False
break
if keep:
suppressed_corners.append((x, y))
return suppressed_corners
六、FAST参数详解
# OpenCV FAST参数
fast = cv2.FastFeatureDetector_create(
threshold=20, # 关键参数:亮度差阈值
# 值越小,检测越敏感(更多角点)
# 值越大,检测越严格(更少但更稳定的角点)
nonmaxSuppression=True, # 非极大值抑制
# True: 移除邻近的弱角点
# False: 保留所有检测到的角点
type=cv2.FAST_FEATURE_DETECTOR_TYPE_9_16
# TYPE_9_16: 使用16个像素,需要连续9个满足条件
# TYPE_7_12: 使用12个像素,需要连续7个
# TYPE_5_8: 使用8个像素,需要连续5个
)
七、FAST vs Harris/Shi-Tomasi对比
| 特性 | FAST | Harris | Shi-Tomasi | 适合嵌入式 |
|---|---|---|---|---|
| 速度 | ⚡⚡⚡ 极快 | ⚡ 慢 | ⚡⚡ 中等 | ✅ FAST |
| 原理 | 亮度比较 | 梯度分析 | 特征值分析 | ✅ FAST简单 |
| 内存 | 很小 | 中等 | 中等 | ✅ FAST |
| 参数 | 阈值t | 多个参数 | 质量等级 | ✅ FAST简单 |
| 准确性 | 中等 | 高 | 高 | Harris/Shi-Tomasi |
| 实时性 | ✅ 优秀 | ❌ 差 | ⚡ 一般 | ✅ FAST |
八、FAST的优缺点
优点:
- 速度极快:比Harris快几十倍
- 实现简单:只有亮度比较
- 内存占用小:不需要计算梯度
- 适合实时应用:视频处理、SLAM、AR
缺点:
- 没有方向信息:旋转图像会失效
- 没有尺度不变性:不能处理尺度变化
- 对噪声敏感:直接比较像素值
- 特征点质量一般:不如Harris稳定
九、FAST在ORB中的作用
# ORB使用FAST作为角点检测器,然后:
# 1. 添加方向计算(灰度质心法)
# 2. 添加尺度不变性(图像金字塔)
# 3. 添加描述符(旋转BRIEF)
# 所以:ORB = FAST + 方向 + 金字塔 + BRIEF
5. 从特征点到特征描述子
仅仅找到一个点是不够的,我们还需要描述它,就像给人拍照留下“指纹”一样。
特征描述子 (Feature Descriptor) 就是一个向量,它量化了特征点周围邻域的图像信息(例如,梯度方向、强度分布)。
- 目的: 确保即使特征点在不同图像中发生旋转、缩放或光照变化,它的描述子仍然保持相似,从而实现准确匹配。
- 经典算法: SIFT (Scale-Invariant Feature Transform) 和 SURF (Speeded Up Robust Features) 是最著名的特征点检测和描述一体化算法,它们是 Harris 算法在尺度和旋转不变性上的巨大进步。
我来详细讲解图像金字塔的原理和应用。
6. 图像金字塔(Image Pyramid)
1. 基本概念
图像金字塔是一系列以金字塔形状排列的图像集合: - 底层:原始图像(最大尺寸) - 上层:逐步缩小的图像(尺寸递减) - 形状像金字塔,因此得名
2. 为什么需要金字塔?
2.1 尺度问题
- 同一个物体在不同距离下,在图像中的尺寸不同
- 固定尺寸的检测器只能检测特定尺度的特征
- 需要多尺度分析才能全面检测
2.2 计算效率
- 在小图像上检测更快
- 可以由粗到精地处理
3. 金字塔的两种主要类型
3.1 高斯金字塔(Gaussian Pyramid)
构建过程: 1. 高斯模糊:用高斯滤波器平滑图像 2. 下采样:删除偶数行和列,图像尺寸减半
import cv2
import numpy as np
# 构建高斯金字塔
def build_gaussian_pyramid(image, levels):
pyramid = [image]
for i in range(1, levels):
# 高斯模糊
blurred = cv2.GaussianBlur(pyramid[-1], (5, 5), 0)
# 下采样(尺寸减半)
downsampled = cv2.pyrDown(blurred)
pyramid.append(downsampled)
return pyramid
# 示例
image = cv2.imread('test.jpg', 0) # 灰度图
pyramid = build_gaussian_pyramid(image, 4)
# 显示金字塔
for i, level_img in enumerate(pyramid):
print(f"Level {i}: {level_img.shape}")
尺度关系: - Level 0: 原始尺寸 (W×H) - Level 1: (W/2 × H/2) - Level 2: (W/4 × H/4) - Level n: (W/2ⁿ × H/2ⁿ)
3.2 拉普拉斯金字塔(Laplacian Pyramid)
用途:图像重建、压缩
联系:拉普拉斯金字塔的每一层近似于对该层图像应用拉普拉斯算子后的结果。
数学解释:
- 高斯模糊可以看作是与高斯核卷积:G = I * g
- 拉普拉斯算子可以近似为:∇²I ≈ I - (I * g)
- 这正是拉普拉斯金字塔的计算:L = Gᵢ - UP(Gᵢ₊₁)
直观理解:
- 高斯金字塔的每一层是低频信息(模糊后的图像)
- 拉普拉斯金字塔的每一层是高频信息(细节、边缘)
构建过程:
- 构建高斯金字塔
- 每层进行上采样并相减
def build_laplacian_pyramid(gaussian_pyramid):
laplacian = []
for i in range(len(gaussian_pyramid)-1):
# 上采样
expanded = cv2.pyrUp(gaussian_pyramid[i+1])
# 调整尺寸(可能因奇数尺寸需要)
if expanded.shape != gaussian_pyramid[i].shape:
expanded = cv2.resize(expanded, gaussian_pyramid[i].shape[::-1])
# 相减得到拉普拉斯层
laplacian.append(gaussian_pyramid[i] - expanded)
laplacian.append(gaussian_pyramid[-1]) # 最后一层直接使用
return laplacian
4. ORB中的金字塔(尺度因子≠2)
ORB不使用标准的2倍下采样,而是用尺度因子(scaleFactor):
4.1 尺度因子原理
# ORB金字塔构建(简化版)
def build_orb_pyramid(image, scaleFactor=1.2, nlevels=8):
pyramid = []
current_scale = 1.0
for level in range(nlevels):
if level == 0:
pyramid.append(image)
else:
# 计算当前层的缩放比例
scale = 1.0 / (scaleFactor ** level)
new_size = (int(image.shape[1] * scale),
int(image.shape[0] * scale))
# 缩放图像
resized = cv2.resize(image, new_size)
pyramid.append(resized)
current_scale /= scaleFactor
return pyramid
4.2 为什么用1.2而不是2?
- 更精细的尺度覆盖:
- 2倍下采样:尺度变化太大,可能漏掉中间尺度的特征
-
1.2倍:尺度变化平缓,覆盖更全面
-
特征连续性:
- 相邻尺度间特征更稳定
- 便于尺度空间极值检测
5. 金字塔在特征检测中的应用
5.1 多尺度特征检测流程
def detect_features_multiscale(image, detector, scaleFactor=1.2, nlevels=8):
all_keypoints = []
# 构建金字塔
pyramid = build_orb_pyramid(image, scaleFactor, nlevels)
# 在各层检测特征
for level, level_img in enumerate(pyramid):
# 在当前层检测特征
keypoints = detector.detect(level_img, None)
# 坐标转换:将当前层坐标转换回原始图像坐标
for kp in keypoints:
# 计算尺度因子
scale = scaleFactor ** level
# 调整坐标
kp.pt = (kp.pt[0] * scale, kp.pt[1] * scale)
# 调整特征尺度
kp.size = kp.size * scale if hasattr(kp, 'size') else kp.size
all_keypoints.extend(keypoints)
return all_keypoints
5.2 尺度空间极值检测
在SIFT算法中,金字塔还用于: 1. 构建高斯差分金字塔(DoG) 2. 在三维空间(x, y, σ)寻找极值点 3. 实现真正的尺度不变性
6. 调参要点(针对嵌入式设备)
6.1 金字塔层数(nlevels)
- 值越大:尺度覆盖越广,但计算量越大
- 推荐值:对于K230,4-6层通常足够
- 经验公式:
nlevels = log(min(width, height)) / log(scaleFactor)
6.2 尺度因子(scaleFactor)
- 常用值:1.1-1.3
- 1.1:尺度覆盖最密,计算量最大
- 1.3:尺度覆盖较疏,计算量较小
- 1.2:平衡选择(ORB默认)
6.3 第一层索引(firstLevel)
- 默认0:从原始图像开始
- 设为1:跳过原始图像,从缩小图像开始
- 减少计算量
- 适合处理大图像
7. OpenCV中的金字塔函数
import cv2
# 1. 高斯金字塔函数
img = cv2.imread('test.jpg')
# 下采样(高斯模糊+尺寸减半)
lower = cv2.pyrDown(img)
# 上采样(尺寸加倍,但信息不会增加)
higher = cv2.pyrUp(lower)
# 2. 构建自定义金字塔
def custom_pyramid(img, scale_factor=1.2, levels=5):
pyramid = [img]
for i in range(1, levels):
# 计算新尺寸
h, w = img.shape[:2]
new_w = int(w / (scale_factor ** i))
new_h = int(h / (scale_factor ** i))
# 缩放
resized = cv2.resize(img, (new_w, new_h))
pyramid.append(resized)
return pyramid
7. ORB特征检测算法
一、ORB算法概述
ORB = Oriented FAST + Rotated BRIEF
- 提出时间:2011年(Ethan Rublee等)
- 核心特点:专利免费 + 实时性好 + 旋转不变性
- 设计目标:替代SIFT/SURF,适合实时应用和嵌入式设备
二、ORB的三大部分
ORB算法流程:
FAST角点检测
↓
计算方向(灰度质心法)
↓
BRIEF描述符(旋转后)
三、第一部分:FAST角点检测
1. FAST原理
检测像素点周围16个像素的亮度变化:
# FAST检测逻辑(简化)
def is_fast_corner(pixel, circle_pixels, threshold=20):
"""
pixel: 中心像素亮度
circle_pixels: 周围16个像素的亮度
threshold: 亮度差阈值
"""
# 连续N个像素比中心亮或暗
brighter_count = sum(1 for p in circle_pixels if p > pixel + threshold)
darker_count = sum(1 for p in circle_pixels if p < pixel - threshold)
# 连续12个像素满足条件(FAST-12)
return brighter_count >= 12 or darker_count >= 12
2. FAST在ORB中的优化
# ORB对FAST的改进:
# 1. 多尺度检测(图像金字塔)
# 2. 非极大值抑制(保留最强响应)
# 3. 限制特征点数量
# OpenCV实现
fast = cv2.FastFeatureDetector_create(
threshold=20, # 亮度差阈值
nonmaxSuppression=True # 非极大值抑制
)
keypoints_fast = fast.detect(gray, None)
四、第二部分:计算方向(灰度质心法)
1. 为什么需要方向?
- 原始BRIEF描述符没有方向信息
- 图像旋转时,描述符会变化
- 需要旋转不变性
2. 灰度质心法原理
def compute_orientation(patch):
"""
计算特征点的方向
patch: 特征点周围的图像块(如31×31)
"""
# 1. 计算矩(moments)
m00 = np.sum(patch) # 零阶矩(总亮度)
# 一阶矩
m01 = 0 # 对y的矩
m10 = 0 # 对x的矩
height, width = patch.shape
for y in range(height):
for x in range(width):
intensity = patch[y, x]
m10 += x * intensity # x方向矩
m01 += y * intensity # y方向矩
# 2. 计算质心
centroid_x = m10 / m00
centroid_y = m01 / m00
# 3. 计算方向角(从特征点到质心)
direction = np.arctan2(centroid_y, centroid_x) # 弧度
return direction
3. 几何解释
图像块:
↑ y
│
│ 质心 (cx, cy)
│ ↗
│ /
│ / 方向角θ
│ /
│ /
│特征点 (0, 0)
└─────────→ x
方向:从特征点指向质心的角度
五、第三部分:BRIEF描述符
1. BRIEF的基本思想
BRIEF是一种二进制描述符,它的核心思想是:
- 不描述特征点的外观(不像SIFT那样计算梯度直方图)
- 只描述特征点周围像素的亮度比较关系
- 结果是一个二进制字符串(0和1组成),而不是浮点数向量
2. BRIEF的工作原理
步骤1:定义比较模式
在特征点周围的31×31像素的区域内,预先定义256个像素对(p, q):
- p和q是区域内的两个像素坐标
- 这些坐标是随机生成的,但在训练阶段会优化
步骤2:进行亮度比较
对于每个像素对(p, q),比较它们的亮度值:
PYTHON1# 伪代码表示
2if I(p) < I(q): # I(p)表示p点的像素亮度
3 bit = 1
4else:
5 bit = 0
6
步骤3:生成二进制描述符
将256次比较的结果拼接起来,得到一个256位的二进制字符串:
TEXT1描述符 = [bit1, bit2, bit3, ..., bit256]
2
例如:10110011...(共256位)
3. BRIEF的优势(为什么适合嵌入式)
- 计算速度快:
- 只需要比较像素值,不需要复杂的梯度计算
- 比较操作在嵌入式设备上非常高效
- 内存占用小:
- 256位 = 32字节(每个特征点)
- SIFT描述符:128维×4字节 = 512字节
- 内存节省16倍!
- 匹配速度快:
- 二进制字符串可以用汉明距离(Hamming Distance)快速比较
- 汉明距离:计算两个二进制串中不同位的数量
- CPU有专门的指令(POPCNT)可以快速计算
4. BRIEF的局限性
- 没有旋转不变性:
- 像素对的位置是固定的
- 图像旋转后,相同的特征点会得到完全不同的描述符
- 没有尺度不变性:
- 只在固定大小的区域内采样
1. 原始BRIEF(Binary Robust Independent Elementary Features)
def brief_descriptor_original(patch, test_pairs):
"""
原始BRIEF描述符
patch: 图像块(已平滑)
test_pairs: 预定义的测试点对列表
"""
descriptor = 0
for i, (x1, y1, x2, y2) in enumerate(test_pairs):
# 比较两个像素的亮度
if patch[y1, x1] < patch[y2, x2]:
descriptor |= (1 << i) # 设置第i位为1
return descriptor # 二进制串
2. BRIEF的问题
- 没有旋转不变性:图像旋转后,测试点对位置不变,但内容变了
- 对噪声敏感:直接比较单个像素值
3. ORB的改进:rBRIEF(旋转BRIEF)
def rbrief_descriptor(patch, orientation, test_pairs):
"""
旋转BRIEF描述符
orientation: 特征点方向(弧度)
"""
# 1. 构建旋转矩阵
cos_theta = np.cos(orientation)
sin_theta = np.sin(orientation)
rotation_matrix = np.array([[cos_theta, -sin_theta],
[sin_theta, cos_theta]])
descriptor = 0
for i, (x1, y1, x2, y2) in enumerate(test_pairs):
# 2. 旋转测试点对
p1_rotated = np.dot(rotation_matrix, [x1, y1])
p2_rotated = np.dot(rotation_matrix, [x2, y2])
# 3. 双线性插值获取像素值
intensity1 = bilinear_interpolation(patch, p1_rotated[0], p1_rotated[1])
intensity2 = bilinear_interpolation(patch, p2_rotated[0], p2_rotated[1])
# 4. 比较
if intensity1 < intensity2:
descriptor |= (1 << i)
return descriptor
4. 几点说明
坐标的显式性和预定义性
在 BREIF 中,用于比较的像素点对 (p_i, q_i) 的坐标是事先确定好并存储在一个查找表中的。这个表是在算法设计或训练阶段就固定下来的。
- 对于标准 BREIF:点对坐标通常是按照某种概率分布(例如高斯分布)在图像块内随机生成一次,然后就固定下来,成为算法参数的一部分。
- 对于 ORB 中的 rBRIEF:点对坐标是通过离线学习从大量随机点对中筛选出来的最优集合,同样被固定存储下来。
点对坐标的细节:
- 在使用 OpenCV、OpenVINO、PyTorch 等主流框架时,rBRIEF 的点对查找表已经内置在实现中。
- 你不需要关心具体的坐标是多少,也不需要自己生成或管理这个表。
- 这是 ORB 算法的核心“黑盒”部分,由框架作者根据原始论文实现并优化。
旋转校正的数学计算:
- 框架会自动为每个 FAST 关键点计算方向(Orientation),并在生成描述符时自动进行坐标旋转。
- 你只需要在创建 ORB 对象时通过参数(如
WTA_K、scoreType等)选择变体,不需要手动实现旋转。
描述符的二进制结构:
- 框架会直接返回一个描述符数组(如 N x 32 字节),你不需要关心哪一位对应哪个点对比较。
七、OpenCV ORB参数详解
# OpenCV ORB完整参数
orb = cv2.ORB_create(
nfeatures=500, # 保留的最大特征点数量
scaleFactor=1.2, # 金字塔缩放因子
nlevels=8, # 金字塔层数
edgeThreshold=31, # 边缘阈值(不检测边缘像素)
firstLevel=0, # 第一层金字塔
WTA_K=2, # 产生每个BRIEF描述符的测试点数
scoreType=cv2.ORB_HARRIS_SCORE, # 角点评分类型
patchSize=31, # 图像块大小,生成BRIEF描述符
fastThreshold=20 # FAST阈值
)
# 关键参数调优指南:
# 1. nfeatures: 嵌入式设备建议100-200
# 2. nlevels: 嵌入式设备建议4-6层
# 3. patchSize: 嵌入式设备建议15-21
# 4. fastThreshold: 值越大,角点越少但更稳定
八、ORB vs SIFT vs SURF对比
| 特性 | ORB | SIFT | SURF | 适合嵌入式 |
|---|---|---|---|---|
| 专利 | 免费 | 专利 | 专利 | ✅ ORB |
| 速度 | 非常快 | 慢 | 中等 | ✅ ORB |
| 内存 | 小(二进制) | 大(浮点) | 中等 | ✅ ORB |
| 旋转不变 | ✅ | ✅ | ✅ | 都支持 |
| 尺度不变 | ✅(金字塔) | ✅ | ✅ | 都支持 |
| 描述符大小 | 32字节 | 128×4字节 | 64×4字节 | ✅ ORB |
| 匹配速度 | 快(汉明距离) | 慢(欧氏距离) | 中等 | ✅ ORB |
图像形态学处理与分割基础
1. 图像形态学处理 (Morphological Image Processing)
形态学处理是一组非线性操作,主要用于二值图像(前景为 1 或 255,背景为 0)或灰度图像(在灰度图像上进行形态学操作,背景和前景的概念确实不再是简单的“黑”和“白”,而是基于像素值的高低和最大/最小操作来定义的),通过利用图像的形状和结构来简化图像、去除噪声、隔离独立元素,并连接相邻元素。
形态学操作的核心是使用一个小的结构元素 (Structuring Element, SE)(类似于我们滤波时用的核),在图像上进行扫描和逻辑运算。
A. 腐蚀 (Erosion)
- 原理: 结构元素 $B$ 与图像 $A$ 进行卷积,只有当结构元素完全包含在前景像素区域时,中心像素才保留为前景(白色或 1)。形象来说,就是腐蚀掉边缘
-
只要结构元素覆盖区域内有一个像素是背景(黑色),那么输出图像中 $(x, y)$ 位置的像素就被设置为背景(黑色)。
-
效果:
-
缩小图像中前景物体的尺寸。
-
消除小的孤立白点(噪声)。
-
断开细小的连接线。
-
B. 膨胀 (Dilation)
- 原理: 只要结构元素 $B$ 触碰到前景像素区域,中心像素就变为前景(白色或 1)。形象来说,就是把边缘膨胀。
-
只要结构元素覆盖区域内有一个像素是前景(白色),那么输出图像中 $(x, y)$ 位置的像素就被设置为前景(白色)。
-
效果:
-
增大图像中前景物体的尺寸。
-
填充物体内部的小孔或凹陷。
-
连接相近的物体。
-
C. 开运算 (Opening) 和 闭运算 (Closing)
形态学操作通常不是单独使用的,而是组合使用来达到特定的目的:
-
开运算 (Opening): 先腐蚀,后膨胀。
- 目的: 用于平滑物体轮廓,并去除比结构元素小的毛刺或孤立小点(噪声)。它能有效地打开物体轮廓上的狭窄连接。是形态学中的低通滤波器。
-
闭运算 (Closing): 先膨胀,后腐蚀。
- 目的: 用于平滑物体轮廓,并连接物体轮廓上的断裂点或填充物体内部的小孔。
D. 礼帽运算 (Top-Hat Transform)
- 定义
礼帽运算是通过原图减去对原图进行开运算(先腐蚀,后膨胀)的结果。
$$\text{TopHat}(I) = I - \text{Opening}(I)$$ - $\text{Opening}(I) = \text{Dilation}(\text{Erosion}(I))$
-
作用与目的
-
开运算的作用: 开运算会移除图像中比结构元素小的前景目标(亮区域的噪声)和突出的亮结构。它会平滑图像的背景。
-
礼帽运算的作用: 当原图 $I$ 减去被平滑的背景 $\text{Opening}(I)$ 时,剩下的就是原图中比周围亮的小区域。
核心应用:提取亮细节和校正背景
礼帽运算可以有效地:
-
提取亮目标: 提取出图像中尺寸较小、亮度较高的物体或细节(如白色小点、细线)。
-
校正不均匀光照(小物体前景): 如果图像的背景是缓慢变化的(例如,光照从亮到暗),礼帽运算可以移除这种缓慢变化的背景,得到一个亮度更均匀的图像。(用礼帽删去小物品得到背景)
E. 黑帽运算 (Black-Hat Transform)
- 定义
黑帽运算是通过对原图进行闭运算(先膨胀,后腐蚀)的结果减去原图。
$$\text{BlackHat}(I) = \text{Closing}(I) - I$$
-
$\text{Closing}(I) = \text{Erosion}(\text{Dilation}(I))$
-
作用与目的
-
闭运算的作用: 闭运算会填充图像中尺寸较小、比周围暗的空洞或裂缝(暗区域的噪声)。
-
黑帽运算的作用: 当被填充后的图像 $\text{Closing}(I)$ 减去原图 $I$ 时,剩下的就是原图中比周围暗的小区域。
核心应用:提取暗细节和缺陷
黑帽运算可以有效地:
- 提取暗目标: 提取出图像中尺寸较小、亮度较低的物体或细节(如黑色小孔、裂纹、暗斑)。
2. 检测缺陷: 在工业检测中,常用于检测产品表面上的凹陷或划痕。
2. 图像分割基础 (Image Segmentation)
图像分割是将图像划分成具有相似特性的互不重叠区域的过程。这是许多高层计算机视觉任务(如目标识别、场景理解)的基础。图像分割是一个复杂的像素级分类过程,其结果是一个标签图,将图像数据抽象成了具有特定意义(如颜色、纹理、语义)的层次结构,并不是简单地在图像上画几条线来分割区域。
A. 基于阈值的分割 (Thresholding)
这是最简单、最常用的分割方法。
-
原理: 通过选择一个或多个阈值 $T$,将图像的灰度值范围分成几个子区域。
-
二值化 (Binarization): 仅用一个阈值将图像分成前景(高于 $T$)和背景(低于 $T$)两个区域。
-
Otsu 大津法: 一种自动选择最佳全局阈值的方法,其核心是最大化前景和背景区域的类间方差,从而确保最佳的分割效果。
B. 基于区域的分割 (Region-Based Segmentation)
这类方法侧重于找到具有相似属性的连通区域。
-
区域生长 (Region Growing): 从一个或多个种子点 (Seed Point) 开始,不断合并相邻的、满足预定义相似性准则(如灰度值相似、颜色相似)的像素点,直到所有满足条件的点都被包含进来。
-
区域分裂与合并 (Region Splitting and Merging): 首先将图像分成许多小块,然后根据相似性将相邻的小块合并;如果某一块不均匀,则将其分裂成更小的块,直到满足所有准则。(联想归并排序)
特征匹配(Feature Matching)
1. 特征匹配的基本概念
1.1 什么是特征匹配?
- 目标:在两幅或多幅图像中找到对应的特征点
- 输入:特征点 + 描述符
- 输出:特征点之间的对应关系(匹配对)
1.2 匹配的核心问题
图像1:特征点A₁, A₂, A₃, ... 描述符D₁, D₂, D₃, ...
图像2:特征点B₁, B₂, B₃, ... 描述符E₁, E₂, E₃, ...
问题:对于每个Dᵢ,在{Eⱼ}中找到最相似的描述符
2. 距离度量
2.1 欧氏距离(用于SIFT、SURF等浮点描述符)
def euclidean_distance(desc1, desc2):
"""计算两个描述符的欧氏距离"""
return np.sqrt(np.sum((desc1 - desc2) ** 2))
# 对于128维的SIFT描述符
# dist = √[(d₁₁-e₁₁)² + (d₁₂-e₁₂)² + ... + (d₁₂₈-e₁₂₈)²]
2.2 汉明距离(用于ORB、BRIEF等二进制描述符)
def hamming_distance(desc1, desc2):
"""计算两个二进制描述符的汉明距离"""
# 方法1:使用异或和位计数
xor_result = desc1 ^ desc2
return np.sum(np.unpackbits(xor_result))
# 方法2:OpenCV内置函数
# return cv2.norm(desc1, desc2, cv2.NORM_HAMMING)
为什么用汉明距离?
- 二进制字符串:
10110011vs10111011 - 不同位的数量 = 汉明距离
- 计算速度快,适合嵌入式设备
3. 匹配算法
3.1 暴力匹配(Brute-Force Matcher)
原理:为图像1的每个特征点,遍历图像2的所有特征点,找到最佳匹配
import cv2
import numpy as np
def brute_force_matching(desc1, desc2, norm_type=cv2.NORM_HAMMING):
"""暴力匹配"""
# 创建暴力匹配器
bf = cv2.BFMatcher(norm_type, crossCheck=True)
# 进行匹配(crossCheck=True确保双向一致)
matches = bf.match(desc1, desc2)
# 按距离排序(距离越小,匹配越好)
matches = sorted(matches, key=lambda x: x.distance)
return matches
# 使用示例
orb = cv2.ORB_create()
kp1, des1 = orb.detectAndCompute(img1, None)
kp2, des2 = orb.detectAndCompute(img2, None)
matches = brute_force_matching(des1, des2)
3.2 KNN匹配(k-Nearest Neighbors)
原理:为每个特征点找到k个最近邻,然后进行筛选
def knn_matching(desc1, desc2, k=2, norm_type=cv2.NORM_HAMMING):
"""KNN匹配"""
bf = cv2.BFMatcher(norm_type)
# 为每个特征点找到k个最佳匹配
knn_matches = bf.knnMatch(desc1, desc2, k=k)
return knn_matches
# 使用示例
knn_matches = knn_matching(des1, des2, k=2)
3.3 FLANN匹配(Fast Library for Approximate Nearest Neighbors)
原理:使用近似算法加速匹配,适合大数据集
def flann_matching(desc1, desc2):
"""FLANN匹配(适合浮点描述符如SIFT)"""
# FLANN参数
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)
search_params = dict(checks=50) # 搜索精度
# 创建FLANN匹配器
flann = cv2.FlannBasedMatcher(index_params, search_params)
# 进行匹配
matches = flann.knnMatch(desc1, desc2, k=2)
return matches
4. 匹配过滤策略
4.1 比率测试(Lowe's Ratio Test)
原理:最佳匹配与次佳匹配的距离比值应小于阈值
def ratio_test(knn_matches, ratio_thresh=0.75):
"""应用比率测试过滤误匹配"""
good_matches = []
for matches in knn_matches:
if len(matches) >= 2:
# 最佳匹配和次佳匹配
best_match, second_best_match = matches[0], matches[1]
# 应用比率测试
if best_match.distance < ratio_thresh * second_best_match.distance:
good_matches.append(best_match)
return good_matches
# 使用示例
knn_matches = knn_matching(des1, des2, k=2)
good_matches = ratio_test(knn_matches, ratio_thresh=0.75)
4.2 交叉验证(Cross-Check)
原理:双向匹配一致才接受
def cross_check_matching(desc1, desc2, norm_type=cv2.NORM_HAMMING):
"""交叉验证匹配"""
bf = cv2.BFMatcher(norm_type)
# 正向匹配:img1→img2
matches_12 = bf.match(desc1, desc2)
# 反向匹配:img2→img1
matches_21 = bf.match(desc2, desc1)
# 只保留双向一致的匹配
good_matches = []
for m12 in matches_12:
for m21 in matches_21:
if m12.queryIdx == m21.trainIdx and m12.trainIdx == m21.queryIdx:
good_matches.append(m12)
break
return good_matches
4.3 距离阈值
def distance_threshold(matches, max_distance=50):
"""基于距离阈值过滤"""
good_matches = [m for m in matches if m.distance < max_distance]
return good_matches
5. 完整特征匹配流程
import cv2
import numpy as np
import matplotlib.pyplot as plt
def feature_matching_pipeline(img1_path, img2_path):
"""完整的特征匹配流程"""
# 1. 读取图像
img1 = cv2.imread(img1_path)
img2 = cv2.imread(img2_path)
gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
# 2. 特征检测与描述符计算
orb = cv2.ORB_create(nfeatures=500)
kp1, des1 = orb.detectAndCompute(gray1, None)
kp2, des2 = orb.detectAndCompute(gray2, None)
print(f"图像1特征点: {len(kp1)}")
print(f"图像2特征点: {len(kp2)}")
# 3. 特征匹配(使用KNN+比率测试)
bf = cv2.BFMatcher(cv2.NORM_HAMMING)
knn_matches = bf.knnMatch(des1, des2, k=2)
# 4. 应用比率测试
good_matches = []
for m, n in knn_matches:
if m.distance < 0.75 * n.distance:
good_matches.append(m)
print(f"原始匹配数: {len(knn_matches)}")
print(f"过滤后匹配数: {len(good_matches)}")
# 5. 可视化匹配结果
img_matches = cv2.drawMatches(
img1, kp1, img2, kp2, good_matches[:50], # 只显示前50个匹配
None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS
)
# 6. 显示结果
plt.figure(figsize=(16, 8))
plt.imshow(cv2.cvtColor(img_matches, cv2.COLOR_BGR2RGB))
plt.title(f"特征匹配结果 (匹配数: {len(good_matches)})")
plt.axis('off')
plt.show()
return {
'keypoints1': kp1,
'keypoints2': kp2,
'descriptors1': des1,
'descriptors2': des2,
'matches': good_matches
}
# 使用示例
# results = feature_matching_pipeline('img1.jpg', 'img2.jpg')
6. 匹配质量评估
6.1 评估指标
def evaluate_matching_quality(matches, total_keypoints1, total_keypoints2):
"""评估匹配质量"""
# 1. 匹配率
match_rate1 = len(matches) / total_keypoints1 * 100
match_rate2 = len(matches) / total_keypoints2 * 100
# 2. 平均匹配距离
avg_distance = np.mean([m.distance for m in matches])
# 3. 距离标准差
std_distance = np.std([m.distance for m in matches])
print("=== 匹配质量评估 ===")
print(f"匹配数量: {len(matches)}")
print(f"匹配率 (相对于图像1): {match_rate1:.1f}%")
print(f"匹配率 (相对于图像2): {match_rate2:.1f}%")
print(f"平均匹配距离: {avg_distance:.2f}")
print(f"距离标准差: {std_distance:.2f}")
return {
'match_count': len(matches),
'match_rate1': match_rate1,
'match_rate2': match_rate2,
'avg_distance': avg_distance,
'std_distance': std_distance
}
6.2 可视化匹配分布
def visualize_match_distribution(kp1, kp2, matches):
"""可视化匹配点的分布"""
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
# 图像1的匹配点
img1_points = np.array([kp1[m.queryIdx].pt for m in matches])
axes[0].scatter(img1_points[:, 0], img1_points[:, 1], s=10, alpha=0.6)
axes[0].set_title('图像1匹配点分布')
axes[0].set_xlabel('X')
axes[0].set_ylabel('Y')
axes[0].invert_yaxis() # 图像坐标y轴向下
# 图像2的匹配点
img2_points = np.array([kp2[m.trainIdx].pt for m in matches])
axes[1].scatter(img2_points[:, 0], img2_points[:, 1], s=10, alpha=0.6, color='red')
axes[1].set_title('图像2匹配点分布')
axes[1].set_xlabel('X')
axes[1].set_ylabel('Y')
axes[1].invert_yaxis()
plt.tight_layout()
plt.show()
7. 实践任务
任务1:基础特征匹配
# 使用两幅相关图像(如同一场景的不同视角)
# 实现完整的特征匹配流程
# 可视化匹配结果
# 评估匹配质量
任务2:参数调优实验
# 实验不同参数对匹配结果的影响:
# 1. ORB的nfeatures参数(100, 300, 500)
# 2. 比率测试的阈值(0.5, 0.75, 0.9)
# 3. 距离阈值(30, 50, 70)
# 记录:匹配数量、正确率、处理时间
任务3:鲁棒性测试
# 测试在不同条件下的匹配性能:
# 1. 旋转变化(0°, 30°, 60°)
# 2. 尺度变化(100%, 75%, 50%)
# 3. 光照变化(亮度调整)
# 4. 添加噪声(高斯噪声)
9. 常见问题与解决方案
问题1:匹配数量太少
可能原因:
- 特征点检测不足
- 描述符区分性差
- 图像差异太大
解决方案:
# 1. 调整ORB参数
orb = cv2.ORB_create(
nfeatures=1000, # 增加特征点数量
fastThreshold=10, # 降低FAST阈值
scaleFactor=1.1 # 更密的尺度采样
)
# 2. 使用更宽松的匹配阈值
good_matches = ratio_test(knn_matches, ratio_thresh=0.8) # 从0.75提高到0.8
问题2:误匹配太多
解决方案:
# 1. 使用更严格的过滤
good_matches = ratio_test(knn_matches, ratio_thresh=0.6) # 从0.75降低到0.6
# 2. 结合多种过滤方法
matches = distance_threshold(matches, max_distance=40)
matches = cross_check_matching(desc1, desc2)
# 3. 使用RANSAC进行几何验证(后续讲解)
结束任务
- 加载图像并转换为灰度图。
- 应用中值滤波(去椒盐噪声)或高斯滤波(去高斯噪声)。
- 应用直方图均衡化或简单的亮度/对比度调整。
- 使用 Canny 边缘检测器提取轮廓。
- 对 Canny 结果进行膨胀和腐蚀,观察对边缘连通性的影响。
实践总结
- 中值滤波有很好的鲁棒性,对于高斯噪声和中值噪声效果很好
- 高斯滤波效果不如中值滤波,虽然其在高斯噪声上的效果也还行。适当增加高斯核大小有助于消噪,但是会使图像失真。
- 在OpenCV中最好不要进行原地操作,可能会有奇怪的bug(但OpenMV是通常为原地操作)