运动估计基础:光流法、块匹配法与相位相关法

运动估计,说白了就是让计算机看懂「谁在动、往哪动」。

我刚开始做视频防抖的时候,以为运动估计就是算算像素偏移。后来踩了不少坑才明白——选对方法,比调参重要十倍。今天咱们就把三种主流方法掰开揉碎了讲。

一、光流法:像素级别的运动追踪

光流法的核心思想很简单:假设一个像素点在两帧之间亮度不变,那么它的移动轨迹就是「光流」。嗯,这里要注意——这个假设在真实场景中经常不成立,但足够好用。

1.1 Lucas-Kanade 光流

我个人习惯用 Lucas-Kanade(LK)做稀疏光流。它只追踪图像中「特征明显」的点,比如角点、边缘点。

核心原理

  • 假设一个像素点周围的小窗口内,所有像素的运动一致
  • 用最小二乘法求解超定方程组
  • 得到每个特征点的运动向量 (dx, dy)

避坑指南:我曾经在光照变化的场景下用 LK 光流,结果全崩了。后来加了图像金字塔(多尺度处理),才搞定大运动场景。

代码实现(OpenCV):

import cv2
import numpy as np

# 读取两帧图像
prev = cv2.imread('frame1.jpg', 0)
next = cv2.imread('frame2.jpg', 0)

# 检测 Shi-Tomasi 角点
corners = cv2.goodFeaturesToTrack(prev, 100, 0.01, 10)

# LK 光流计算
lk_params = dict(winSize=(15, 15), maxLevel=2,
                 criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
next_pts, status, err = cv2.calcOpticalFlowPyrLK(prev, next, corners, None, **lk_params)

1.2 Farneback 光流

Farneback 是稠密光流——它给每个像素都算一个运动向量。你想想看,这计算量得多大?

我建议在需要精细运动分析时用 Farneback,比如视频稳像中的全局运动估计。

参数调优经验

参数 作用 我的推荐值
pyr_scale 金字塔缩放比例 0.5
levels 金字塔层数 3(大运动用 5)
winsize 窗口大小 15(噪声大时用 21)
iterations 迭代次数 3

小技巧:Farneback 对噪声敏感。我一般先做高斯模糊再算光流,效果提升明显。

二、块匹配法:简单粗暴但有效

块匹配法(Block Matching)的思路更直接——把图像切成小块,然后在下一帧里找最像的那一块。

说白了就是「找相似」。我在做实时防抖时,块匹配法是我的首选,因为它快、稳定、好理解。

搜索策略对比

  • 全搜索:遍历所有位置,精度最高,但慢得像蜗牛
  • 三步搜索:从粗到细,速度提升 10 倍以上
  • 菱形搜索:我常用的,在精度和速度之间取得平衡

匹配准则:

# SAD(绝对差值和)——最简单,也最常用
SAD = sum(|I1(x,y) - I2(x+dx, y+dy)|)

# SSD(平方差值和)——对噪声更鲁棒
SSD = sum((I1(x,y) - I2(x+dx, y+dy))^2)

# NCC(归一化互相关)——光照变化时用
NCC = sum(I1*I2) / sqrt(sum(I1^2)*sum(I2^2))

注意:块匹配法假设块内所有像素做相同运动。遇到旋转或缩放场景,这个方法会失效。我曾经在无人机航拍数据上吃过这个亏。

三、相位相关法:频域里的优雅解法

相位相关法是我个人觉得最「聪明」的方法。它把运动估计问题从空间域转换到频域,利用傅里叶变换的平移性质。

核心原理

  1. 对两帧图像做 FFT(快速傅里叶变换)
  2. 计算互功率谱的相位差
  3. 做逆 FFT,峰值位置就是运动向量

为什么用相位而不是幅度?因为相位包含位置信息,幅度包含亮度信息。我们只关心位置变化,所以只看相位。

代码实现:

def phase_correlation(img1, img2):
    # FFT 变换
    f1 = np.fft.fft2(img1)
    f2 = np.fft.fft2(img2)
    
    # 互功率谱
    cross_power = (f1 * np.conj(f2)) / np.abs(f1 * np.conj(f2) + 1e-10)
    
    # 逆 FFT
    result = np.fft.ifft2(cross_power)
    
    # 找到峰值位置
    h, w = result.shape
    idx = np.argmax(np.abs(result))
    dy, dx = np.unravel_index(idx, (h, w))
    
    # 处理边界
    if dy > h//2: dy -= h
    if dx > w//2: dx -= w
    
    return dx, dy

实战经验:相位相关法对光照变化完全不敏感,这是它最大的优势。但缺点是只能处理纯平移运动,旋转和缩放需要额外处理。

四、三种方法对比与选型

我整理了一张对比表,方便你快速决策:

方法 精度 速度 适用场景 我的推荐
LK 光流 高(稀疏) 特征点追踪、目标跟踪 ★★★★☆
Farneback 光流 高(稠密) 全局运动分析、稳像 ★★★☆☆
块匹配法 实时防抖、视频编码 ★★★★★
相位相关法 高(平移) 光照变化场景、图像配准 ★★★★☆

我的选择原则

  • 实时系统 → 块匹配法(三步搜索 + SAD)
  • 高精度离线分析 → Farneback 光流 + 后处理
  • 光照剧烈变化 → 相位相关法
  • 需要追踪特定目标 → LK 光流 + 特征点检测

五、知识体系总览

下面这张图是我自己总结的运动估计方法选择流程,帮你快速定位该用哪种方法:

运动估计方法选择流程图 输入两帧图像 需要实时? 块匹配法 三步搜索+SAD 光照变化大? 相位相关法 纯平移运动 需要稠密光流? Farneback光流 全局运动分析 LK光流法 特征点追踪

这张图是我在实际项目中反复验证过的决策路径。你按照这个流程走,基本不会选错方法。


运动估计是整个防抖系统的「眼睛」。眼睛看错了,后面再怎么调也白搭。我建议你先把这三种方法在自己的数据集上跑一遍,感受一下它们的脾气秉性。

下一章咱们会深入讲全局运动估计——如何从这些局部运动向量中,提取出相机的真实运动轨迹。到时候你会发现,运动估计只是开始,真正的挑战在后头。