2. 运动物体检测基础:背景建模、帧差法、光流法、运动物体检测的数学基础
各位同学,欢迎来到第二章。
上一章我们聊了动态环境对SLAM系统的冲击。说白了,就是那些乱跑的行人、晃动的树叶、飞驰的汽车,它们会让你的特征点匹配一塌糊涂,甚至直接导致定位漂移。那怎么把这些“捣乱分子”揪出来呢?
这一章,我们就来啃下运动物体检测这块硬骨头。我会从最经典的背景建模讲起,再到简单粗暴的帧差法,最后聊聊光流法。嗯,这些都是基础,但基础不牢,地动山摇。我个人习惯是,不管做多高级的算法,这些底层的数学直觉必须得有。
2.1 背景建模:给场景拍一张“素颜照”
背景建模的思路很直观。你想想看,如果摄像头固定不动,那背景是不是基本不变的?变化的只有前景物体,比如人、车。那我们就先给场景拍一张“素颜照”,也就是背景模型。然后,每一帧新图像来了,跟这张“素颜照”一对比,不一样的地方就是运动物体。
最简单的做法:均值背景建模
我刚开始做项目时,就用的这个。取前N帧图像,对每个像素位置求个平均值,作为背景。代码大概长这样:
// 假设有N帧灰度图,每帧大小相同
cv::Mat background = cv::Mat::zeros(height, width, CV_32FC1);
for (int i = 0; i < N; ++i) {
cv::Mat frame;
// 读取第i帧到frame
background += frame;
}
background /= N; // 得到均值背景
然后,新帧来了,直接做差分:
cv::Mat diff;
cv::absdiff(currentFrame, background, diff);
cv::threshold(diff, foregroundMask, 30, 255, cv::THRESH_BINARY);
阈值30是我凭经验给的。为什么是30?因为传感器有噪声,光照会微变,留点余量。但这个方法有个致命伤——如果场景里一直有车停着,它会被“学习”进背景里。我曾经在停车场测试,一辆车停了5分钟,再开走,背景里就多了一个“车洞”。
2.2 帧差法:简单粗暴,但有效
帧差法就更直接了。我不建背景模型了,我直接用相邻两帧或三帧做差分。运动物体在相邻帧之间位置变了,像素值自然就变了。
两帧差分:
cv::Mat diff;
cv::absdiff(frame_t, frame_t_1, diff);
cv::threshold(diff, motionMask, 25, 255, cv::THRESH_BINARY);
你看,代码比背景建模还短。但问题也很明显:
- 只能检测到物体的边缘轮廓,内部是空洞。因为物体内部颜色变化不大。
- 如果物体运动太慢,两帧之间位移小于一个像素,就检测不到了。
- 如果物体运动太快,会出现“鬼影”。
三帧差分: 稍微改进一点。取连续三帧,做两次差分,再取交集。这样可以减少“鬼影”和空洞。
cv::Mat diff1, diff2, motionMask;
cv::absdiff(frame_t, frame_t_1, diff1);
cv::absdiff(frame_t_1, frame_t_2, diff2);
cv::bitwise_and(diff1, diff2, motionMask);
cv::threshold(motionMask, motionMask, 20, 255, cv::THRESH_BINARY);
帧差法在SLAM里有什么用?我个人的经验是,它适合做快速的前景/背景粗筛。比如在ORB-SLAM的跟踪线程里,我可以先用帧差法标记出可能是动态的区域,然后在这些区域里降低特征点提取的阈值,或者干脆不提取。这样能省下不少计算量。
2.3 光流法:追踪像素的“运动轨迹”
光流法,听起来高大上,其实核心思想就一句话:找到同一个像素点在前后两帧图像中的位置变化。这个位置变化量,就是光流向量。
数学基础:亮度恒定假设
光流法有个基本假设:同一个空间点,在相邻两帧图像上的像素亮度是不变的。用数学公式写出来就是:
I(x, y, t) = I(x + dx, y + dy, t + dt)
其中 I 是像素亮度,(x,y) 是像素坐标,t 是时间。把这个公式用泰勒展开,忽略高阶项,就得到了光流约束方程:
Ix * u + Iy * v + It = 0
这里 Ix, Iy 是图像梯度,It 是时间梯度,u = dx/dt, v = dy/dt 就是我们要求的光流向量。
一个方程有两个未知数,解不了。所以需要引入额外的约束条件。最经典的是Lucas-Kanade方法,它假设一个局部窗口内所有像素的运动是一致的。然后解一个超定方程组。
LK光流法实战:
// 使用OpenCV的calcOpticalFlowPyrLK
std::vector<cv::Point2f> prevPts, nextPts;
std::vector<uchar> status;
std::vector<float> err;
// 假设prevPts是上一帧提取的特征点
cv::calcOpticalFlowPyrLK(prevGray, currGray, prevPts, nextPts, status, err);
// 根据status筛选跟踪成功的点
for (size_t i = 0; i < status.size(); ++i) {
if (status[i]) {
// 计算位移向量
cv::Point2f flow = nextPts[i] - prevPts[i];
float magnitude = cv::norm(flow);
// 如果位移过大,可能是动态点
if (magnitude > threshold) {
// 标记为动态
}
}
}
在SLAM里,光流法常用来做特征点跟踪。但要注意,如果场景里有运动物体,那些物体上的特征点光流向量会跟背景上的明显不同。我们可以利用这个差异来检测动态区域。
2.4 三种方法的对比与选择
好了,三种方法都讲完了。我整理了一个表格,方便你对比:
| 方法 | 计算量 | 适用场景 | 主要缺点 | SLAM中的典型用法 |
|---|---|---|---|---|
| 背景建模 | 低 | 固定摄像头 | 无法适应光照变化和背景更新 | 预处理,生成静态地图 |
| 帧差法 | 极低 | 快速运动物体 | 空洞、鬼影、对慢速物体不敏感 | 快速粗筛,降低特征提取区域 |
| 光流法 | 中高 | 相机运动或静止 | 对纹理要求高,计算量大 | 特征点跟踪,动态点剔除 |
我个人建议,在实际的SLAM项目中,不要只用一种方法。我习惯的做法是:先用帧差法做快速粗筛,把明显是动态的区域标记出来;然后在这些区域里,用光流法做精细的跟踪和验证。这样既保证了实时性,又提高了准确率。
嗯,这一章的内容就到这里。运动物体检测是动态SLAM的第一道防线。你想想看,如果连哪些像素是运动的都搞不清楚,后面的处理就无从谈起。所以,这些基础方法虽然简单,但一定要吃透。
公众号:蓝海资料掘金营,微信deep3321