运动估计基础:光流法、块匹配法与相位相关法
运动估计,说白了就是让计算机看懂「谁在动、往哪动」。
我刚开始做视频防抖的时候,以为运动估计就是算算像素偏移。后来踩了不少坑才明白——选对方法,比调参重要十倍。今天咱们就把三种主流方法掰开揉碎了讲。
一、光流法:像素级别的运动追踪
光流法的核心思想很简单:假设一个像素点在两帧之间亮度不变,那么它的移动轨迹就是「光流」。嗯,这里要注意——这个假设在真实场景中经常不成立,但足够好用。
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))
注意:块匹配法假设块内所有像素做相同运动。遇到旋转或缩放场景,这个方法会失效。我曾经在无人机航拍数据上吃过这个亏。
三、相位相关法:频域里的优雅解法
相位相关法是我个人觉得最「聪明」的方法。它把运动估计问题从空间域转换到频域,利用傅里叶变换的平移性质。
核心原理:
- 对两帧图像做 FFT(快速傅里叶变换)
- 计算互功率谱的相位差
- 做逆 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 光流 + 特征点检测
五、知识体系总览
下面这张图是我自己总结的运动估计方法选择流程,帮你快速定位该用哪种方法:
这张图是我在实际项目中反复验证过的决策路径。你按照这个流程走,基本不会选错方法。
运动估计是整个防抖系统的「眼睛」。眼睛看错了,后面再怎么调也白搭。我建议你先把这三种方法在自己的数据集上跑一遍,感受一下它们的脾气秉性。
下一章咱们会深入讲全局运动估计——如何从这些局部运动向量中,提取出相机的真实运动轨迹。到时候你会发现,运动估计只是开始,真正的挑战在后头。