前言

在计算机视觉的众多技术中,光流法(Optical Flow)可以说是最古老也最具生命力的算法之一。从 1950 年代心理学家 Gibson 首次提出视觉运动感知理论,到 1981 年 Lucas 和 Kanade 发表那篇经典论文,再到今天深度学习时代的 RAFT、GMFlow 等现代光流网络,这项技术已经走过了半个多世纪的历程。

我第一次接触光流法是在大学的计算机视觉课程上。当时教授在黑板上写下那个著名的光流方程 Iₓu + Iᵧv + Iₜ = 0,然后告诉我们:“这个简单的方程,蕴含了理解运动的全部秘密。” 那时候我还不太理解这句话的含义,直到后来在实际项目中用它实现了一个简单的视频目标跟踪系统,才真正体会到光流法的强大之处。

在今天的边缘计算和嵌入式 AI 场景中,光流法依然占据着不可替代的地位。相比于深度学习的目标跟踪算法,传统光流法具有以下优势:

  • 计算量小:不需要复杂的神经网络,可以在资源受限的嵌入式设备上实时运行
  • 无需训练:不需要标注数据,开箱即用
  • 实时性好:很多优化后的实现可以轻松达到 30 FPS 以上
  • 适用范围广:从无人机的视觉导航,到视频防抖,再到动作识别,光流法无处不在

本文将带您从零开始深入理解光流法的原理,从最基本的亮度恒定假设,到经典的 Lucas-Kanade 算法,再到 OpenCV 中的各种光流法实现。我们会通过大量代码示例,让您不仅理解理论,更能在实际项目中应用这项技术。

光流法基本原理

一、什么是光流法?

1.1 光流的定义

简单来说,光流就是空间中运动物体在成像平面上像素运动的瞬时速度。当你盯着窗外行驶的汽车时,视网膜上汽车图像的移动速度就是光流。

更正式的定义是:给定图像序列 I(x, y, t),光流法的目标是为每个像素点 (x, y) 找到一个速度向量 (u, v),使得:

I(x, y, t) = I(x + u·dt, y + v·dt, t + dt)

这个等式表达的就是:经过微小的时间间隔 dt 后,像素点 (x, y) 移动到了 (x + u·dt, y + v·dt),而亮度保持不变。

1.2 光流法的三大假设

所有传统光流算法都建立在三个基本假设之上:

假设一:亮度恒定(Brightness Constancy)

同一物体像素点的亮度在连续帧之间不会发生变化。这是最核心的假设,也是光流约束方程的基础。

I(x, y, t) = I(x + dx, y + dy, t + dt)

这个假设在现实中并不总是成立——光照变化、阴影移动、物体旋转都会导致亮度变化。但在大多数场景下,尤其是帧间时间间隔很短时,这个假设是近似成立的。

假设二:时间连续或小运动(Temporal Persistence)

物体的运动是连续平滑的,不会发生突变。换句话说,相邻两帧之间的位移很小。

这就是为什么我们经常需要对图像进行降采样(构建金字塔)——因为当运动较大时,这个假设就不成立了。

假设三:空间一致性(Spatial Coherence)

相邻像素具有相似的运动。如果一个像素属于某个物体,那么它周围的像素也应该属于同一个物体,因此具有相似的运动速度。

这个假设是 Lucas-Kanade 算法能够求解的关键——因为光流约束方程只有一个,但有两个未知数 uv,是病态问题。利用空间一致性,我们可以在一个小邻域内建立多个方程,从而求解出唯一解。

1.3 光流法的分类

光流算法可以大致分为两类:

稀疏光流(Sparse Optical Flow)

只计算图像中部分特征点的光流(比如角点、边缘点)。计算速度快,但只能得到稀疏的运动向量。

代表算法:Lucas-Kanade

稠密光流(Dense Optical Flow)

计算图像中每一个像素的光流。计算量大,但能得到完整的运动场。

代表算法:Farneback, Horn-Schunck, TV-L1

在实际应用中,稀疏光流通常用于目标跟踪、视觉里程计等任务,而稠密光流则用于视频插帧、动作识别、视频分割等需要完整运动信息的场景。

二、光流约束方程的数学推导

理解光流约束方程的推导过程,是深入理解光流法的关键。让我们从亮度恒定假设开始。

2.1 泰勒展开

我们从亮度恒定假设出发:

I(x, y, t) = I(x + dx, y + dy, t + dt)

对右边进行一阶泰勒展开:

I(x + dx, y + dy, t + dt) = I(x, y, t) + (∂I/∂x)dx + (∂I/∂y)dy + (∂I/∂t)dt + ε

其中 ε 是高阶无穷小,可以忽略。

2.2 光流约束方程

将两式相减,两边除以 dt,得到:

(∂I/∂x)(dx/dt) + (∂I/∂y)(dy/dt) + (∂I/∂t) = 0

令:

  • Iₓ = ∂I/∂x,图像在 x 方向的空间梯度
  • Iᵧ = ∂I/∂y,图像在 y 方向的空间梯度
  • Iₜ = ∂I/∂t,图像的时间梯度
  • u = dx/dt,x 方向的光流速度
  • v = dy/dt,y 方向的光流速度

就得到了著名的光流约束方程:

Iₓu + Iᵧv + Iₜ = 0

2.3 孔径问题

现在问题来了:一个方程,两个未知数 uv,怎么解?

这就是光流法中的孔径问题(Aperture Problem)

想象一下,你透过一个小孔看一个运动的物体——你只能看到边缘的运动,却无法确定完整的运动方向。比如一条倾斜的直线向右运动,透过小孔你看到的可能只是直线沿着垂直方向移动。

数学上说,光流约束方程只能约束沿着梯度方向的运动分量,但垂直于梯度方向的运动分量是无法确定的:

∇I · (u, v) = -Iₜ

这说明光流向量 (u, v) 在梯度方向 ∇I 上的投影等于 -Iₜ / |∇I|,但垂直于梯度方向的分量可以是任意值。

如何解决这个问题?这就是 Lucas-Kanade 算法的精髓所在——利用空间一致性假设,在一个小窗口内建立多个方程,从而求解出唯一的光流向量。

三、Lucas-Kanade 算法详解

Lucas-Kanade 算法由 Bruce D. Lucas 和 Takeo Kanade 于 1981 年提出,是最经典的稀疏光流算法,也是今天 OpenCV 中 calcOpticalFlowPyrLK 函数的理论基础。

3.1 核心思想

Lucas-Kanade 的核心思想就是利用空间一致性假设:在一个小的邻域窗口内(比如 3×3 或 5×5),所有像素都具有相同的光流向量 (u, v)

如果窗口内有 n 个像素,我们就可以建立 n 个方程:

Iₓ₁u + Iᵧ₁v = -Iₜ₁
Iₓ₂u + Iᵧ₂v = -Iₜ₂
...
Iₓₙu + Iᵧₙv = -Iₜₙ

写成矩阵形式就是:

[Iₓ₁ Iᵧ₁]   [u]   [-Iₜ₁]
[Iₓ₂ Iᵧ₂]   [v]   [-Iₜ₂]
...    =   ...
[Iₓₙ Iᵧₙ]       [-Iₜₙ]

简记为:

A · v = b

其中:

    [Iₓ₁ Iᵧ₁]
A = [Iₓ₂ Iᵧ₂]
    [...   ...]
    [Iₓₙ Iᵧₙ]

    [u]
v = [v]

    [-Iₜ₁]
b = [-Iₜ₂]
    [...  ]
    [-Iₜₙ]

3.2 最小二乘解

这是一个超定方程组,我们用最小二乘法求解。两边乘以 Aᵀ

AᵀA · v = Aᵀb

得到:

[ΣIₓ²   ΣIₓIᵧ] [u]   [-ΣIₓIₜ]
[ΣIₓIᵧ  ΣIᵧ²] [v] = [-ΣIᵧIₜ]

M = AᵀA,则:

v = M⁻¹ · Aᵀb

只要矩阵 M 可逆,我们就能求出唯一解。什么时候 M 可逆呢?当 M 的两个特征值都比较大的时候——换句话说,当这个窗口内包含至少两个不同方向的边缘时,这通常就是角点的特征。

这就是为什么 Lucas-Kanade 光流通常只对角点进行计算——只有角点区域的 M 矩阵才是良态的,才能得到稳定的光流解。

3.3 算法步骤

Lucas-Kanade 算法的完整步骤如下:

  1. 特征点检测:在第一帧图像中检测角点(通常用 Shi-Tomasi 算法)
  2. 图像梯度计算:计算当前帧和下一帧图像的空间梯度 Iₓ, Iᵧ 和时间梯度 Iₜ
  3. 窗口内方程构建:对每个特征点,在其邻域窗口内构建超定方程组
  4. 最小二乘求解:计算 M = AᵀA,求解光流向量 v = M⁻¹Aᵀb
  5. 迭代优化:使用牛顿法或高斯-牛顿法进行迭代求精

(第一部分完,约2200字)

四、金字塔光流:解决大位移问题

标准的 Lucas-Kanade 算法有一个严重的局限:它只能处理小运动。还记得小运动假设吗?如果物体在两帧之间移动了超过几个像素,泰勒展开的一阶近似就不再成立,光流计算就会失效。

金字塔光流(Pyramidal Lucas-Kanade)就是为了解决这个问题而提出的。

4.1 图像金字塔

图像金字塔就是对原始图像进行多次降采样得到的一系列图像。最顶层是分辨率最低的图像,最底层是原始图像。

比如一个 4 层金字塔:

  • 第 0 层(原始):640×480
  • 第 1 层:320×240
  • 第 2 层:160×120
  • 第 3 层:80×60

这样做的好处是:在低分辨率图像上,原来 10 个像素的位移可能就变成了 1-2 个像素,满足小运动假设。

4.2 由粗到精(Coarse-to-Fine)策略

金字塔光流采用"由粗到精"的计算策略:

  1. 顶层计算:在最顶层(分辨率最低)计算光流,此时位移很小,计算结果准确
  2. 结果传播:将顶层的光流结果作为下一层的初始估计
  3. 逐层求精:在下一层图像上,以之前的估计为起点进行迭代求精
  4. 直到底层:重复上述过程直到达到原始图像层

这样即使原始图像中有较大的位移,在顶层金字塔中也会被压缩成小位移,从而可以被准确计算。

4.3 数学原理

在金字塔的每一层 L,我们计算光流增量 (dLx, dLy),然后将累计的光流传递到下一层:

gL-1 = 2 × (gL + (dLx, dLy))

其中 gL 是第 L 层的累计光流估计。

这个过程一直进行到第 0 层(原始图像),最终得到完整的光流向量。

在 OpenCV 中,calcOpticalFlowPyrLK 函数默认使用 3-4 层金字塔,这也是实践中的最佳配置。

五、OpenCV 稀疏光流实战

现在让我们进入实战环节,用 OpenCV 实现 Lucas-Kanade 光流跟踪。

5.1 环境准备

首先确保安装了正确版本的 OpenCV:

pip install opencv-python numpy matplotlib

5.2 基本代码框架

这是一个最基本的 Lucas-Kanade 光流跟踪示例:

import cv2
import numpy as np

# 视频捕获
cap = cv2.VideoCapture(0)  # 0 表示默认摄像头

# Shi-Tomasi 角点检测参数
feature_params = dict(
    maxCorners=100,       # 最多检测多少个角点
    qualityLevel=0.3,     # 角点质量阈值
    minDistance=7,        # 角点之间的最小距离
    blockSize=7           # 计算协方差矩阵的窗口大小
)

# Lucas-Kanade 光流参数
lk_params = dict(
    winSize=(15, 15),     # 搜索窗口大小
    maxLevel=4,           # 金字塔层数
    criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)
)

# 随机颜色(用于绘制轨迹)
color = np.random.randint(0, 255, (100, 3))

# 读取第一帧并检测角点
ret, old_frame = cap.read()
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)

# 创建一个 mask 用于绘制轨迹
mask = np.zeros_like(old_frame)

while True:
    ret, frame = cap.read()
    if not ret:
        break
    
    frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
    # 计算光流
    p1, st, err = cv2.calcOpticalFlowPyrLK(
        old_gray, frame_gray, p0, None, **lk_params
    )
    
    # 筛选出跟踪成功的点
    good_new = p1[st == 1]
    good_old = p0[st == 1]
    
    # 绘制轨迹
    for i, (new, old) in enumerate(zip(good_new, good_old)):
        a, b = new.ravel()
        c, d = old.ravel()
        mask = cv2.line(mask, (int(a), int(b)), (int(c), int(d)), color[i].tolist(), 2)
        frame = cv2.circle(frame, (int(a), int(b)), 5, color[i].tolist(), -1)
    
    img = cv2.add(frame, mask)
    
    cv2.imshow('Optical Flow', img)
    
    if cv2.waitKey(30) & 0xFF == 27:  # ESC 退出
        break
    
    # 更新上一帧和特征点
    old_gray = frame_gray.copy()
    p0 = good_new.reshape(-1, 1, 2)

cap.release()
cv2.destroyAllWindows()

5.3 参数详解

让我们详细解释一下关键参数:

Shi-Tomasi 角点参数:

  • maxCorners:检测的最大角点数。数量越多跟踪越稳定,但计算越慢。通常 50-200 是合理范围。
  • qualityLevel:角点质量阈值,0-1 之间。值越高,检测到的角点越少但质量越好。0.3 是经验值。
  • minDistance:角点之间的最小像素距离。避免在同一区域检测到大量重叠的角点。
  • blockSize:计算角点响应时的窗口大小。

Lucas-Kanade 光流参数:

  • winSize:搜索窗口大小。窗口越大,对噪声的鲁棒性越好,但计算量也越大。(15, 15) 或 (21, 21) 是常用值。
  • maxLevel:金字塔层数。层数越多能处理的运动越大,但计算时间也越长。通常 3-4 层就足够了。
  • criteria:迭代停止条件。这里设置最多迭代 10 次,或者当光流变化小于 0.03 时停止。

5.4 返回值解析

calcOpticalFlowPyrLK 返回三个值:

  1. p1:下一帧中对应特征点的位置
  2. st:状态向量,1 表示该点跟踪成功,0 表示跟踪失败
  3. err:每个点的跟踪误差

st 向量非常重要——我们可以通过它筛选出跟踪成功的点,避免绘制错误的轨迹。在实际应用中,每隔一段时间就应该重新检测一次特征点,因为随着时间推移,很多点会逐渐丢失。

六、稠密光流:Farneback 算法

稀疏光流只跟踪少数特征点,但很多应用场景需要知道每一个像素的运动——这就是稠密光流。

6.1 Farneback 算法简介

Farneback 算法由 Gunnar Farneback 于 2003 年提出,是基于多项式展开的稠密光流算法。它的核心思想是:

  1. 对每个像素周围的邻域进行二次多项式拟合
  2. 在邻域变换下,观察多项式系数如何变化
  3. 通过这些变化来估计光流

相比于其他稠密光流算法,Farneback 的优势在于:

  • 计算速度相对较快
  • 可以得到稠密的光流场
  • 对光照变化有一定的鲁棒性

6.2 Farneback 基本原理

Farneback 算法假设图像在局部可以近似为二次多项式:

f(x) = xᵀAx + bᵀx + c

其中 A 是 2×2 对称矩阵,b 是 2 维向量,c 是标量。

当发生全局位移 d 时,多项式变换为:

f₂(x) = f₁(x - d) = (x - d)ᵀA(x - d) + bᵀ(x - d) + c

通过比较变换前后的多项式系数,我们可以解出位移 d

在实际实现中,Farneback 算法同样使用金字塔策略,由粗到精地计算光流。

(第二部分完,约2100字)

6.3 Farneback 光流代码实现

下面是使用 Farneback 算法计算稠密光流并可视化的完整代码:

import cv2
import numpy as np

cap = cv2.VideoCapture(0)
ret, frame1 = cap.read()
prvs = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)

# 创建 HSV 图像用于可视化光流
hsv = np.zeros_like(frame1)
hsv[..., 1] = 255  # 饱和度设为最大值

while True:
    ret, frame2 = cap.read()
    if not ret:
        break
    
    next = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY)
    
    # 计算 Farneback 稠密光流
    flow = cv2.calcOpticalFlowFarneback(
        prvs, next, None,
        pyr_scale=0.5,    # 金字塔缩放比例
        levels=3,         # 金字塔层数
        winsize=15,       # 平均窗口大小
        iterations=3,     # 每层迭代次数
        poly_n=5,         # 多项式邻域大小
        poly_sigma=1.2,   # 多项式标准差
        flags=0
    )
    
    # 将光流转换为极坐标 (幅度, 角度)
    mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1])
    
    # 角度对应 Hue 通道,幅度对应 Value 通道
    hsv[..., 0] = ang * 180 / np.pi / 2
    hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
    
    # HSV 转 BGR 显示
    bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
    
    cv2.imshow('Dense Optical Flow', bgr)
    
    if cv2.waitKey(30) & 0xFF == 27:
        break
    
    prvs = next

cap.release()
cv2.destroyAllWindows()

6.4 Farneback 参数调优

Farneback 算法的参数对结果影响很大:

  • pyr_scale=0.5:经典值,每层金字塔分辨率减半。设为 0.8 可以保留更多细节但计算量增加。
  • levels=3:金字塔层数,越多能处理越大的运动。
  • winsize=15:平均窗口大小,越大越平滑但会丢失细小运动。
  • iterations=3:每层迭代次数,越多越精确但越慢。
  • poly_n=5:多项式邻域大小,通常 5 或 7。
  • poly_sigma=1.2:高斯平滑标准差,对应 poly_n=5 时是 1.1,poly_n=7 时是 1.5。

七、其他光流算法对比

OpenCV 中还实现了多种其他光流算法,让我们对比一下它们的特点:

7.1 Horn-Schunck 算法

Horn-Schunck 是最早的稠密光流算法之一(1981年),引入了全局平滑约束:

E = ∫∫(Iₓu + Iᵧv + Iₜ)² dxdy + α ∫∫[(∇u)² + (∇v)²] dxdy

优点是理论优美,缺点是对噪声敏感,计算量大,且容易过度平滑。

7.2 TV-L1 算法

TV-L1 使用总变分(Total Variation)正则化,是目前质量最好的传统光流算法之一:

E = ||Iₓu + Iᵧv + Iₜ||₁ + λ(||∇u||₁ + ||∇v||₁)

L1 范数使得算法对异常值和噪声更加鲁棒。OpenCV 中通过 createOptFlow_DualTVL1() 创建。

7.3 SimpleFlow 算法

SimpleFlow 是 2012 年提出的算法,核心思想是非局部滤波,计算效率高且效果不错。OpenCV 中通过 calcOpticalFlowSF() 调用。

7.4 算法性能对比

算法 速度 精度 适用场景
Lucas-Kanade ⭐⭐⭐⭐⭐ ⭐⭐⭐ 稀疏特征点跟踪
Farneback ⭐⭐⭐ ⭐⭐⭐ 一般稠密光流
TV-L1 ⭐⭐⭐⭐ 高精度稠密光流
SimpleFlow ⭐⭐⭐⭐ ⭐⭐⭐ 实时稠密光流

八、实战项目:基于光流的运动目标检测

现在让我们实现一个更实用的项目:使用光流法进行运动目标检测和跟踪。

import cv2
import numpy as np

class OpticalFlowTracker:
    def __init__(self, max_corners=200):
        self.max_corners = max_corners
        self.feature_params = dict(
            maxCorners=max_corners,
            qualityLevel=0.01,
            minDistance=10,
            blockSize=7
        )
        self.lk_params = dict(
            winSize=(15, 15),
            maxLevel=2,
            criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)
        )
        self.tracks = []
        self.track_len = 10
        self.detect_interval = 5
        self.frame_idx = 0
        
    def process_frame(self, frame):
        frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        vis = frame.copy()
        
        # 计算光流
        if len(self.tracks) > 0:
            img0, img1 = self.prev_gray, frame_gray
            p0 = np.float32([tr[-1] for tr in self.tracks]).reshape(-1, 1, 2)
            p1, st, err = cv2.calcOpticalFlowPyrLK(img0, img1, p0, None, **self.lk_params)
            p0r, st, err = cv2.calcOpticalFlowPyrLK(img1, img0, p1, None, **self.lk_params)
            
            # 双向验证:正反向计算的误差应该很小
            d = abs(p0 - p0r).reshape(-1, 2).max(-1)
            good = d < 1.0
            
            new_tracks = []
            for tr, (x, y), good_flag in zip(self.tracks, p1.reshape(-1, 2), good):
                if not good_flag:
                    continue
                tr.append((x, y))
                if len(tr) > self.track_len:
                    del tr[0]
                new_tracks.append(tr)
                cv2.circle(vis, (int(x), int(y)), 2, (0, 255, 0), -1)
            
            self.tracks = new_tracks
            
            # 绘制轨迹
            cv2.polylines(vis, [np.int32(tr) for tr in self.tracks], False, (0, 255, 0))
            
            # 计算运动统计
            if len(self.tracks) > 5:
                motions = np.array([tr[-1][0] - tr[0][0] for tr in self.tracks])
                mean_motion = np.mean(motions)
                cv2.putText(vis, f'Mean motion: {mean_motion:.2f}', 
                           (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
        
        # 每隔几帧重新检测特征点
        if self.frame_idx % self.detect_interval == 0:
            mask = np.zeros_like(frame_gray)
            mask[:] = 255
            for x, y in [np.int32(tr[-1]) for tr in self.tracks]:
                cv2.circle(mask, (x, y), 5, 0, -1)
            
            p = cv2.goodFeaturesToTrack(frame_gray, mask=mask, **self.feature_params)
            if p is not None:
                for x, y in np.float32(p).reshape(-1, 2):
                    self.tracks.append([(x, y)])
        
        self.frame_idx += 1
        self.prev_gray = frame_gray
        
        cv2.putText(vis, f'Track count: {len(self.tracks)}', 
                   (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
        
        return vis

# 主程序
if __name__ == '__main__':
    cap = cv2.VideoCapture(0)
    tracker = OpticalFlowTracker()
    
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        
        vis = tracker.process_frame(frame)
        cv2.imshow('Optical Flow Tracking', vis)
        
        if cv2.waitKey(1) & 0xFF == 27:
            break
    
    cap.release()
    cv2.destroyAllWindows()

这个改进版的跟踪器加入了双向验证(正反向计算光流,误差小的点才保留),大大提高了跟踪的稳定性。

九、参数调优指南

9.1 提高跟踪稳定性

问题:跟踪点经常丢失,轨迹断断续续

解决方案

  • 增大 winSize 到 (21, 21) 或 (31, 31)
  • 增加 maxLevel 金字塔层数到 4-5
  • 降低 qualityLevel 到 0.01,保留更多特征点
  • 增加 minDistance 避免特征点过于密集

9.2 提高实时性能

问题:在嵌入式设备上帧率太低

解决方案

  • 减小 maxCorners 到 50-100
  • 减小 winSize 到 (11, 11)
  • 减小 maxLevel 到 2-3
  • 对输入图像进行降采样(如 640×480 → 320×240)

9.3 处理快速运动

问题:快速移动时跟踪失效

解决方案

  • 增加 maxLevel 到 4-5
  • 增大 winSize
  • 提高摄像头帧率(如果支持)
  • 使用更高的相机快门速度减少运动模糊

十、常见问题与解决方案

10.1 为什么跟踪点会漂移?

原因

  • 小的计算误差会累积
  • 亮度恒定假设不成立
  • 物体旋转或尺度变化

解决方案

  • 定期重新检测特征点
  • 使用更鲁棒的特征描述子(如 ORB)
  • 加入双向验证机制

10.2 为什么光照变化时光流失效?

原因:亮度恒定假设被破坏

解决方案

  • 使用图像归一化(直方图均衡化)
  • 改用基于梯度的特征(如 HOG)
  • 使用对光照更鲁棒的光流变种(如 Census 变换)

10.3 如何处理大位移运动?

原因:小运动假设不成立

解决方案

  • 增加金字塔层数
  • 使用由粗到精的策略
  • 考虑使用深度学习光流算法(如 RAFT)

十一、进阶方向:深度学习光流

传统光流算法虽然经典,但在复杂场景下的精度已经被深度学习方法超越。

11.1 现代深度学习光流网络

  • FlowNet / FlowNet2(2015/2016):端到端的光流网络鼻祖
  • PWC-Net(2018):基于金字塔、变形、代价体的轻量网络
  • RAFT(2020):基于循环优化的光流网络,目前的 SOTA
  • GMFlow / GMFlowNet(2022/2023):基于全局匹配的高速高精度光流

11.2 在边缘设备上部署

很多轻量型的光流网络已经可以在边缘设备上实时运行:

  • PWC-Net 在 Jetson Nano 上可以达到 10-15 FPS
  • 量化后的小型网络在 RK3588 上可以达到 30+ FPS

如果你对精度要求很高,且硬件资源充足,深度学习光流是更好的选择。

总结

本文从零开始深入讲解了光流法的原理和实战应用。我们从最基本的亮度恒定假设出发,推导了光流约束方程,理解了孔径问题的本质,然后详细介绍了 Lucas-Kanade 算法和金字塔策略。

在实战部分,我们不仅给出了稀疏光流和稠密光流的基本代码,还实现了一个带有双向验证的稳定目标跟踪器。我们对比了各种光流算法的优缺点,给出了详细的参数调优指南和常见问题的解决方案。

光流法作为计算机视觉中最古老也最基础的技术之一,至今仍然在边缘计算、嵌入式 AI 等领域发挥着不可替代的作用。理解光流法的原理,不仅能帮助你解决实际问题,更能为你打开理解视觉运动的大门。

从最早的 Gibson 视觉运动理论,到 Lucas-Kanade 算法,再到今天的深度学习光流网络,人类对视觉运动的理解已经走过了半个多世纪。但光流法的核心思想——从亮度变化中推断运动——始终没有改变。这也许就是计算机视觉最迷人的地方:简单的原理,却蕴含着理解世界运动规律的钥匙。

(全文完,约7000字)