4. 运动一致性检测基础:基于光流的运动场分析

各位同学好,我是老张。今天咱们聊一个在动态SLAM里绕不开的话题——运动一致性检测

说白了,就是怎么区分「相机自己在动」和「场景里的东西自己在动」。这个问题我当年刚入行时踩过不少坑,今天把经验掰开揉碎了讲给你听。

4.1 光流法:运动场的「眼睛」

先问个问题:你盯着一个移动的物体看,眼睛会怎么反应?

对,眼球会追着它转。但SLAM里的相机没这么智能,它只能老老实实拍下每一帧。这时候,光流就派上用场了。

光流,说白了就是图像中像素点的运动矢量。每个像素点从第t帧跑到第t+1帧,位移了多少、方向如何,这就是光流。

核心思想:如果场景是静态的,光流应该完全由相机运动引起,呈现一种「全局一致性」的模式。如果某个区域的光流模式跟周围不一样,那大概率是独立运动的物体。

我在项目里遇到过这种情况:一个行人从镜头前走过,他的光流方向跟背景完全相反。嗯,这就是典型的运动不一致。

4.2 Lucas-Kanade光流:稀疏但精准

Lucas-Kanade(LK)光流,我习惯叫它「点追踪器」。它不关心整张图,只盯着几个关键点。

它的假设很简单:

  • 亮度恒定:同一个点,亮度不变
  • 小运动:相邻帧之间,运动量很小
  • 空间一致性:邻域内的点,运动一致

你想想看,这三个假设其实挺强的。尤其是「小运动」这条,如果物体跑得飞快,LK就抓瞎了。所以实际使用时,我们通常会配合图像金字塔——从粗到细,逐层追踪。

我的经验:LK适合追踪角点、边缘等特征明显的点。如果你用ORB特征点,LK光流是绝配。我在一个室内机器人项目里,用LK追踪天花板上的纹理点,效果出奇的好。

代码实现也不复杂,OpenCV里直接调:

import cv2
import numpy as np

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

# 检测特征点(比如Shi-Tomasi角点)
prev_pts = cv2.goodFeaturesToTrack(prev, maxCorners=100, 
                                   qualityLevel=0.01, minDistance=10)

# LK光流追踪
next_pts, status, err = cv2.calcOpticalFlowPyrLK(prev, next, 
                                                  prev_pts, None)

# 筛选有效点
good_pts = prev_pts[status == 1]
good_next = next_pts[status == 1]

# 计算运动矢量
flow_vectors = good_next - good_pts

这段代码跑完后,你就得到了每个特征点的运动矢量。接下来,就是判断这些矢量是否「一致」了。

4.3 Farneback光流:稠密但计算量大

LK只能给你稀疏的几个点,但有时候我们需要全图的运动信息。比如,你想知道整张图里哪些区域在动,哪些是静止的。

这时候,Farneback光流就登场了。它计算每个像素点的光流,输出一张稠密的光流场。

Farneback的原理稍微复杂一些,它用多项式展开来近似每个像素邻域的亮度变化。我简单说下:

  • 对每个像素,用一个二次多项式拟合它的邻域
  • 通过比较两帧的多项式系数,算出位移
  • 再通过全局优化,得到平滑的光流场

注意:Farneback的计算量比LK大得多。我在一个嵌入式项目里试过,640x480的图像,跑一次要几十毫秒。如果你用实时系统,得掂量掂量。

OpenCV调用同样简单:

import cv2

prev = cv2.imread('frame1.jpg', cv2.IMREAD_GRAYSCALE)
next = cv2.imread('frame2.jpg', cv2.IMREAD_GRAYSCALE)

# Farneback稠密光流
flow = cv2.calcOpticalFlowFarneback(prev, next, None, 
                                     pyr_scale=0.5, levels=3, 
                                     winsize=15, iterations=3, 
                                     poly_n=5, poly_sigma=1.2, 
                                     flags=0)

# flow是一个两通道的矩阵,分别表示x和y方向的位移
fx, fy = flow[..., 0], flow[..., 1]

输出的是一个跟原图一样大的矩阵,每个像素点都有一个(x, y)位移量。有了这个,你就可以做很多事了。

4.4 如何区分相机运动与物体运动?

好,现在我们有光流了。怎么判断哪些是相机运动引起的,哪些是物体独立运动?

我总结了一套三步走的方法:

  1. 全局运动估计:用所有光流点,拟合一个全局运动模型(比如仿射变换或单应性矩阵)。这个模型代表「相机自身的运动」。
  2. 残差计算:每个点的实际光流,减去全局模型预测的光流,得到残差。
  3. 一致性判断:残差大的点,就是运动不一致的点,很可能是独立运动的物体。

关键点:全局模型拟合时,要用RANSAC剔除异常值。因为独立运动的物体本身就是异常值,如果不剔除,模型会被带偏。

举个例子。假设相机在向右平移,那么背景上的点都应该向左移动(相对运动)。但一个行人从右向左走,他的光流方向可能跟背景相反。用RANSAC拟合时,行人的点会被当作外点剔除,剩下的点拟合出的模型就是纯相机运动。

我曾经在一个自动驾驶项目里用过这个方法。当时车辆在行驶,路边的行人、自行车都在动。我们用Farneback光流加RANSAC,成功把动态物体标记出来,然后丢给跟踪模块做避障。效果还不错,就是计算量有点大。

4.5 实战中的坑与技巧

讲几个我踩过的坑:

  • 光照变化:光流假设亮度恒定,但实际场景中光照会变。比如太阳被云遮住,整张图变暗,光流就会出错。我建议在预处理时做直方图均衡化。
  • 大运动:如果物体运动太快,LK和Farneback都会失效。解决办法是用图像金字塔,或者降低帧率。
  • 纹理缺失:白墙、天空这种区域,光流算不出来。我一般会加一个置信度阈值,低于阈值的点直接丢弃。

我的习惯:在实际项目中,我会把LK和Farneback结合起来用。先用LK追踪特征点,快速判断哪些区域可能有动态物体。然后在这些区域用Farneback做稠密光流,精确定位。这样既省计算量,又保证了精度。

4.6 本章小结

运动一致性检测,说白了就是找「不听话」的像素点。光流法给了我们一个直观的工具:

  • LK光流:稀疏、快速、适合特征点追踪
  • Farneback光流:稠密、全面、适合区域分析
  • 全局模型+残差:区分相机运动与物体运动的核心思路

下一章,我们会深入讲怎么用这些光流信息做动态物体的检测与剔除。嗯,到时候会涉及一些数学推导,但我会尽量讲得通俗些。


公众号:蓝海资料掘金营,微信deep3321