简介:基于OpenCV 2.4.9与Visual Studio 2010实现的光流法运动目标检测示例包,面向计算机视觉初学者及视频分析开发者,帮助理解光流原理与运动检测实现,适用于课程设计或项目预研。包内完整包含C++源码、VS2010工程配置、已编译exe及bike.avi测试视频,共40个文件、12.36MB,工程中带有tlog日志、pdb调试符号和ipch缓存等构建文件,可直接打开工程复现检测流程并单步调试。代码实现了视频灰度化与去噪预处理、Lucas-Kanade与Farneback双光流算法、角点特征检测、目标轨迹追踪及可视化绘制,后处理过滤噪声并连接断点,提升检测稳定性。已有172人学习下载,可作为视频监控、自动驾驶、无人机导航等场景的入门参考,也可为二次开发和算法对比提供基座。
1. 用 OpenCV 2.4.9 的 opflow 工程把视频运动目标检测从原理落到代码
很多人一提到视频里的运动目标检测,第一反应就是背景差分或者帧间差分,但这两种方法对摄像头抖动、光照突变和背景缓慢变化都太敏感。而光流法走的是另一条路子:它不关心背景长什么样,只关心像素在相邻帧之间的运动矢量,所以对场景结构变化不敏感,适合做运动目标检测的补充甚至主方案。早年间 OpenCV 2.4.9 时代,opflow 这类工程就是干这个的典型代码骨架,配合 VS2010 编译环境,能直接跑出运动目标的轮廓框。
这篇文章不会假装见过某个现成的 opflow.zip 源码包,而是按我实际做运动检测的路线,把光流特征提取、Lucas-Kanade 稀疏光流计算、目标框聚集与跟踪的完整流程拆开讲。你可以照着本文在 VS2010 里新建工程、配置 OpenCV 2.4.9,然后一步步把运动目标检测跑起来。整个方案的核心代码量不大,真正花时间的往往是参数调优和编译环境配置,这两块我会重点展开。
适用人群很明确:手头有老项目要维护、需要在 XP 或 Win7 老机器上部署、或者作业里明确规定用 OpenCV 2.4.9 + VS2010 的工程。如果你是刚接触光流,本文也会先把稠密光流和稀疏光流的区别讲清楚,避免一上来就调错 API。
2. 光流特征的理论基础与 OpenCV 2.4.9 中的 API 选型
2.1 光流法的基本假设:亮度恒定与邻域速度一致
光流法要成立,靠两个假设。第一个是亮度恒定,意思是同一个空间点在两帧图像中的灰度值不变,数学上写成 I(x, y, t) = I(x + dx, y + dy, t + dt)。第二个是邻域速度一致,即一个像素周围的邻域点具有相同的运动矢量,这样才能解出唯一的速度值。基于这两个假设,对亮度方程做一阶泰勒展开并忽略高阶项,就得到光流约束方程:
Ix * u + Iy * v + It = 0
这里 Ix、Iy 是图像在 x 和 y 方向的梯度,It 是灰度随时间的变化率,u 和 v 是待求的光流矢量。这个方程只有一个约束,却有两个未知数,所以必须引入邻域约束才能闭合。Lucas-Kanade 方法假设一个窗口内的像素运动一致,用最小二乘法解超定方程组,从而得到每个特征点的光流矢量。
理解了这一步,你就能明白为什么在做光流之前通常要先做灰度化、有时还要做高斯平滑:梯度计算对噪声非常敏感,平滑能显著降低 Ix、Iy、It 的估计误差。在 OpenCV 2.4.9 里,如果直接用灰度视频流计算光流,结果往往比用彩色图转换后再算要稳定,因为三通道的梯度会互相干扰。
2.2 稀疏光流与稠密光流:为什么运动目标检测常用稀疏光流
光流按计算方式分成稀疏光流和稠密光流。稠密光流对图像中每个像素都计算运动矢量,OpenCV 2.4.9 里对应 calcOpticalFlowFarneback 函数,输出是一张和原图同尺寸的矢量图,视觉效果很直观,但计算量极大,320x240 的分辨率在 CPU 上跑实时都吃力。稀疏光流则只跟踪预先选定的角点,典型实现是 calcOpticalFlowPyrLK,也叫 Lucas-Kanade 金字塔光流。
做运动目标检测时我会优先选稀疏光流,原因有两点。第一,运动目标的轮廓边缘通常具有明显的角点特征,稀疏跟踪这些点就足以覆盖目标的运动趋势;第二,稀疏光流配合金字塔多尺度策略,能处理较大位移,同时每帧只算几十个点的位置,CPU 占用率可以控制在单核 30% 以内。如果你用的环境是 VS2010 配老机器,稠密光流基本跑不动实时视频,所以 opflow 这类工程选稀疏光流是务实的做法。
需要注意的是,稀疏光流只告诉你“这些特征点去哪了”,并没有告诉你“哪些点是运动目标”。所以完整流程必须是:先用 goodFeaturesToTrack 提取角点,再用 calcOpticalFlowPyrLK 跟踪这些点,最后根据位移大小和方向做聚类,把位移显著且方向一致的点归到同一个运动目标上。
2.3 OpenCV 2.4.9 与 VS2010 的版本匹配关系
OpenCV 2.4.9 是 2014 年发布的版本,配套的预编译库里有 vc10 和 vc11 两个目录,vc10 对应 Visual Studio 2010,vc11 对应 VS2012。你用 VS2010 编译工程,就必须链接 vc10 目录下的 opencv_core249.lib、opencv_imgproc249.lib、opencv_video249.lib、opencv_features2d249.lib 这几个静态库。如果用 VS2015 去链接 vc10 的 lib,虽然有些情况下能通过,但运行时大概率会出现内存崩溃或符号解析失败。
另一个容易踩的坑是运行库类型。OpenCV 2.4.9 预编译库默认是 /MD 方式编译的,也就是动态链接到 MSVCR100.dll。所以你在 VS2010 里必须把工程的“代码生成 > 运行库”设置为“多线程调试 DLL (/MDd)”或“多线程 DLL (/MD)”,不能选 /MT。不然编译能过,运行时一调用 opencv 函数就报“应用程序无法正常启动”。
除了这些,还要把 OpenCV 的 bin 目录加到系统 PATH 环境变量里。在 Win7 上安装 VS2010 后,我一般会把 D:\opencv\opencv249\build\x86\vc10\bin 加到用户变量 PATH,并把 D:\opencv\opencv249\build\include 加入工程的附加包含目录,D:\opencv\opencv249\build\x86\vc10\lib 加入附加库目录。
3. 在 VS2010 里搭建 opflow 工程并配置 OpenCV 2.4.9 环境
3.1 新建工程与属性表配置实例
用 VS2010 新建一个 Win32 控制台应用程序,工程名可以叫 opflow_test,勾选“空项目”。然后打开项目属性,按下面的表格逐项配置。这里我建议不要直接在项目属性里改,而是新建一个属性表文件保存下来,因为后续可能要建多个测试工程,属性表可以直接复用。
| 配置项 | 路径 | 值 |
|---|---|---|
| 附加包含目录 | 配置属性 > C/C++ > 常规 | D:\opencv\opencv249\build\include;D:\opencv\opencv249\build\include\opencv;D:\opencv\opencv249\build\include\opencv2 |
| 附加库目录 | 配置属性 > 链接器 > 常规 | D:\opencv\opencv249\build\x86\vc10\lib |
| 附加依赖项 | 配置属性 > 链接器 > 输入 | opencv_core249d.lib;opencv_imgproc249d.lib;opencv_video249d.lib;opencv_features2d249d.lib;opencv_highgui249d.lib |
| 运行库 | 配置属性 > C/C++ > 代码生成 | 多线程调试 DLL (/MDd) |
| 字符集 | 配置属性 > 常规 | 使用多字节字符集 |
注意 Debug 配置下要链接带 d 后缀的 lib,Release 配置下去掉 d。很多人在 Debug 下链接了 release 的 lib,结果一调试就报错。这个错误在 VS2010 下特别隐蔽,因为它不像缺少符号那样直接报链接失败,而是运行时才崩溃。
配置完成后写一段最简单的代码验证环境:读入摄像头或者视频文件,显示原始帧。如果画面能正常显示,说明环境没问题,然后在此基础上加光流计算。千万不要一上来就写全部功能,那样出了问题很难定位是环境问题还是算法问题。
3.2 视频读取与灰度化预处理代码
#include "opencv2/opencv.hpp" #include <iostream> using namespace cv; using namespace std; int main() { VideoCapture cap("test.avi"); if (!cap.isOpened()) { cout << "cannot open video file" << endl; return -1; } Mat frame, gray, prevGray; cap >> frame; if (frame.empty()) return -1; cvtColor(frame, gray, COLOR_BGR2GRAY); prevGray = gray.clone(); while (true) { cap >> frame; if (frame.empty()) break; cvtColor(frame, gray, COLOR_BGR2GRAY); imshow("opflow_video", frame); if (waitKey(30) >= 0) break; gray.copyTo(prevGray); // 仅用于测试,后续会被光流结果替换 } return 0; }这段代码的作用是验证视频读取和灰度转换链路是否通畅。cvtColor 函数的第三个参数是 COLOR_BGR2GRAY,在 OpenCV 2.4.9 里也可以写 CV_BGR2GRAY,两者等价,但新版写法在后续升级时不用改。对于视频文件,要注意 OpenCV 2.4.9 在 VS2010 下默认不支持所有格式的编码解码,一般是 avi 用 mjpg 或 xvid 编码没问题,mp4 经常打不开,建议测试时先用 avi 文件。如果打不开视频,优先看 bin 目录下有没有 opencv_ffmpeg249.dll,没有的话补上基本就能解决。
预处理阶段我会再加上高斯模糊,因为光流法对图像噪声敏感。在 goodFeaturesToTrack 之前对 gray 做一次 GaussianBlur(gray, gray, Size(3,3), 0),能减少误跟踪。但要注意,模糊核不能太大,否则角点会被抹平,导致特征点数量骤减。
3.3 静态库依赖缺失的典型报错与排查顺序
VS2010 编译 OpenCV 工程最常见的报错有两种。第一种是 fatal error LNK1104: cannot open file 'opencv_core249d.lib',这种基本就是附加库目录路径配错了。第二种是 error LNK2019: unresolved external symbol,这类符号无法解析的报错,多半是 lib 文件名拼错、链接了 release lib 到 debug 工程,或者缺少某个依赖库。
排查顺序我习惯这样走:先检查配置管理器里当前是 Debug 还是 Release,确认 lib 后缀匹配;然后确认 lib 目录下确实存在对应文件,OpenCV 2.4.9 的 lib 文件只存在于 build\x86\vc10\lib 下,不要指向 build\x86\vc12 或 x64 目录。最后检查附加依赖项里是不是少了 opencv_features2d249d.lib,因为 goodFeaturesToTrack 在 2.4.9 里归在 features2d 模块,漏掉它就会出现 LNK2019。
4. 核心实现:用 calcOpticalFlowPyrLK 计算光流特征并检测运动目标
4.1 特征点提取:goodFeaturesToTrack 的参数选择
在计算光流之前,必须先选定要跟踪的特征点。OpenCV 2.4.9 里用 goodFeaturesToTrack 函数提取 Shi-Tomasi 角点,它的核心参数不多,但对效果影响很大:
vector<Point2f> corners; int maxCorners = 200; double qualityLevel = 0.01; double minDistance = 10; int blockSize = 3; bool useHarrisDetector = false; double k = 0.04; goodFeaturesToTrack(prevGray, corners, maxCorners, qualityLevel, minDistance, Mat(), blockSize, useHarrisDetector, k);参数说明如下。maxCorners 是最大角点数量,运动目标检测场景我一般设 150 到 200,太少会导致目标边缘覆盖不足,太多则增加光流计算耗时;qualityLevel 是角点质量等级,低于最大响应值乘以该系数的角点会被丢弃,0.01 是一个比较宽松的阈值,适合目标纹理不够丰富的场景,如果视频画面里噪点很多,可以提高到 0.02 或 0.03;minDistance 是角点之间的最小像素距离,设 10 可以避免角点扎堆,让特征点更均匀地分布在目标上。
这里有一个很容易踩的坑:goodFeaturesToTrack 的输入图像必须是单通道 8 位图,如果传入灰度图,直接可用;如果传入的是原彩色帧,OpenCV 内部虽然不会报错,但结果会与预期不符,因为函数只处理第一通道。所以在调用前一定要确保 prevGray 是 cvtColor 后的灰度图。另外,blockSize 参数代表角点检测的邻域尺寸,一般取 3 或 5。视频分辨率小、运动目标占画面比例小时,blockSize 取 3 能保留更多的局部细节;如果目标是车辆、行人这类大目标,blockSize 取 5 能减少背景纹理干扰。
4.2 稀疏光流跟踪主循环与状态位处理
特征点提取完之后,进入视频循环,每帧调用 calcOpticalFlowPyrLK 计算光流:
vector<Point2f> prevPts, nextPts; vector<uchar> status; vector<float> err; Size winSize(21, 21); int maxLevel = 3; TermCriteria criteria(TermCriteria::COUNT | TermCriteria::EPS, 30, 0.01); goodFeaturesToTrack(prevGray, prevPts, 200, 0.01, 10, Mat(), 3, false, 0.04); while (runnable) { cap >> frame; cvtColor(frame, gray, COLOR_BGR2GRAY); calcOpticalFlowPyrLK(prevGray, gray, prevPts, nextPts, status, err, winSize, maxLevel, criteria); vector<Point2f> validPrev, validNext; for (size_t i = 0; i < status.size(); i++) { if (status[i] == 1) { validPrev.push_back(prevPts[i]); validNext.push_back(nextPts[i]); } } // 计算每个点的位移 vector<float> dx(validPrev.size()), dy(validPrev.size()); for (size_t i = 0; i < validPrev.size(); i++) { dx[i] = validNext[i].x - validPrev[i].x; dy[i] = validNext[i].y - validPrev[i].y; } imshow("opflow_result", frame); // 更新上一帧数据 gray.copyTo(prevGray); validNext.copyTo(prevPts); waitKey(30); }calcOpticalFlowPyrLK 的输入输出有几点需要注意。第一个参数 prevGray 是上一帧的灰度图,第二个 gray 是当前帧灰度图;第三个 prevPts 是上一帧的特征点,第四个 nextPts 输出当前帧对应的点位置。status 数组是关键:status[i] 为 1 表示第 i 个点跟踪成功,为 0 表示失败。失败的原因通常是目标移出画面、被遮挡、或者灰度变化不满足亮度恒定假设。
winSize 参数是搜索窗口大小,窗口越大越能跟踪大位移目标,但误匹配率也越高。对视频中快速运动的目标,比如行驶的车辆,winSize 要设大一些,比如 31x31;对慢速运动的人体姿势变化,21x21 足够。maxLevel 是金字塔层数,取 3 表示使用 3 层金字塔,可以处理约 2^3 = 8 倍于 base 分辨率的位移。如果目标运动速度极快,maxLevel 可以调到 5,但计算量会明显上升。
err 参数输出每个点的误差,通常配合 status 使用。在目标被部分遮挡时,status 可能仍然为 1,但 err 值会异常增大。所以更稳健的做法是再加一个条件:err[i] 小于某个阈值才认为跟踪有效。这个阈值取决于视频噪声程度,我一般先打印出 err 的分布,再取中位数的 2 到 3 倍作为阈值,避免拍脑袋。
4.3 位移阈值与聚类:把光流点归成运动目标框
得到每个特征点的位移矢量之后,下一步是把位移显著的点筛选出来并聚成目标框。判断一个点是否属于运动目标,核心逻辑是位移幅值超过阈值:
float movementThreshold = 2.0f; vector<Point2f> movingPoints; for (size_t i = 0; i < validPrev.size(); i++) { float dist = sqrt(dx[i] * dx[i] + dy[i] * dy[i]); if (dist > movementThreshold) { movingPoints.push_back(validNext[i]); } } if (movingPoints.size() >= 3) { Rect boundRect = boundingRect(movingPoints); rectangle(frame, boundRect, Scalar(0, 0, 255), 2); }movementThreshold 按像素每帧来设置。30 帧每秒的视频里,2 像素每帧对应每秒 60 像素的运动速度。在 640x480 分辨率下,这个速度对应的物理运动已经比较明显。如果阈值设 1,很容易把光照变化引起的噪声点误判为运动目标;设 5 的话,慢速目标可能漏检。实际调试时,先用上述距离过滤观察输出,再来回调整这个值。
boundingRect 是对所有满足条件的点做最小外接矩形,只在 movingPoints 数量大于等于 3 时触发,否则单点噪声可能画出一个极小的框。这种简单聚类假设一个运动目标对应的光流点在空间上是连续的,而且彼此距离不会太远。如果画面里同时出现多个运动目标,需要对 movingPoints 做进一步聚类,比如先把点按空间位置做 KMeans 再分别求外接矩形。
4.4 运动目标检测的完整代码整合
把前面的步骤整合成一个完整的主循环,代码如下。这个版本可以直接在 VS2010 里编译运行,包含了特征点提取、光流计算、位移筛选和矩形绘制四个阶段。
#include "opencv2/opencv.hpp" #include <iostream> #include <vector> using namespace cv; using namespace std; int main() { VideoCapture cap("test.avi"); if (!cap.isOpened()) { cout << "cannot open video" << endl; return -1; } Mat frame, gray, prevGray; cap >> frame; cvtColor(frame, prevGray, COLOR_BGR2GRAY); vector<Point2f> prevPts; goodFeaturesToTrack(prevGray, prevPts, 200, 0.01, 10, Mat(), 3, false, 0.04); vector<Point2f> nextPts; vector<uchar> status; vector<float> err; while (true) { cap >> frame; if (frame.empty()) break; cvtColor(frame, gray, COLOR_BGR2GRAY); calcOpticalFlowPyrLK(prevGray, gray, prevPts, nextPts, status, err, Size(21,21), 3, TermCriteria(TermCriteria::COUNT | TermCriteria::EPS, 30, 0.01)); vector<Point2f> movingPts; for (size_t i = 0; i < status.size(); i++) { if (status[i] == 1) { float dx = nextPts[i].x - prevPts[i].x; float dy = nextPts[i].y - prevPts[i].y; float dist = sqrt(dx * dx + dy * dy); if (dist > 2.0f && err[i] < 100.0f) { movingPts.push_back(nextPts[i]); circle(frame, nextPts[i], 2, Scalar(0, 255, 0), -1); } } } if (movingPts.size() >= 3) { Rect r = boundingRect(movingPts); rectangle(frame, r, Scalar(0, 0, 255), 2); } imshow("opflow_detection", frame); if (waitKey(30) >= 0) break; swap(prevGray, gray); prevPts.assign(nextPts.begin(), nextPts.end()); int missingCount = 0; for (size_t i = 0; i < status.size(); i++) if (status[i] == 0) missingCount++; if (prevPts.size() < 50 || missingCount > 100) { goodFeaturesToTrack(prevGray, prevPts, 200, 0.01, 10, Mat(), 3, false, 0.04); } } return 0; }代码里有两个容易被忽略的细节。第一个是 prevGray 和 gray 的交换用了 swap 函数,而不是 copyTo。swap 只是交换两个 Mat 的数据头,不复制像素数据,速度快很多。第二个是特征点不足时的重新检测机制:当跟踪点数量低于 50 时,说明大量点已经丢失,此时在当前的 prevGray 上重新执行 goodFeaturesToTrack,确保后续帧仍然有足够的点参与光流计算。
err[i] < 100 这个条件会过滤掉跟踪质量极差的点。err 的单位和图像灰度梯度有关,OpenCV 默认输出的 LK 误差在正常场景下小于 50,如果画面快速旋转或目标剧烈变形,err 会超过 200。你可以把 err 的阈值设置得保守一些,比如 150,然后观察误检框的数量来微调。
5. 光流运动检测的 3 个必调参数与两个常见失败模式
5.1 movementThreshold、winSize 与 maxLevel 的联动调节
运动目标检测的效果好坏,很大程度上取决于三个参数的联动。movementThreshold 控制敏感度,winSize 控制搜索能力,maxLevel 控制大位移处理能力,三者不是独立的。当 winSize 增大时,光流匹配的可靠性会下降,因为搜索窗口内可能有多个相似纹理区域,此时 err 会变大,movementThreshold 就需要相应调大才能过滤掉误匹配。反过来,如果把 maxLevel 从 3 降到 1,可处理的位移范围变小,快速运动目标的位移会被算小,movementThreshold 原本能滤掉的静止点可能因为数值波动而被当成运动点。
我通常会按视频里运动目标的速度来定参数组合。目标是行人,速度约每秒 30 到 80 像素,movementThreshold 设 1.5,winSize 设 21,maxLevel 设 3。目标是高速公路上的车辆,速度可达每秒 200 像素以上,movementThreshold 设 5,winSize 设 31,maxLevel 设 4。目标在画面里几乎不动,比如监控下的徘徊人员,阈值必须降到 0.8 到 1.0,但这时候噪声误检概率很高,建议先对连续多帧位移取中位数再做判断。
调试时不要同时改多个参数。我一般只改 movementThreshold,观察矩形框数量是否收敛;如果快速目标的框时有时无,把 winSize 调大一档并观察是否改善;如果目标速度跨度大,再动 maxLevel。每次只改一个参数,记录效果,比随机组合参数要快得多。
5.2 特征点大量丢失与背景误检的排查思路
特征点丢失是稀疏光流最常见的失败模式,典型表现是跟踪框持续几帧后消失,然后重新出现。这通常有三个原因。第一是目标被遮挡时间超过几帧,L-K 光流在遮挡区域无法找到匹配点,status 置 0。第二是目标运动速度超出金字塔能处理的位移范围,对应解决方法就是把 maxLevel 提高或把视频帧率降采样后计算。第三是目标纹理过于单一,比如行人衣服是纯色的,窗口内没有足够梯度,光流方程病态,解出的矢量不可靠。
背景误检是另一个常见问题,表现形式是静止的物体被标成运动目标。最常见的诱因是光照变化导致背景区域的灰度值整体偏移,比如云遮住太阳后又移开。这种灰度偏移满足亮度恒定假设吗?不满足,但 L-K 光流只能从梯度上搜索匹配,当整幅图的灰度同时变化时,背景区域的 It 分量变大,解出的 u、v 会出现整体漂移。解决的思路是引入前后两帧的全局位移估计:先用所有特征点的位移计算一个全局中位数,再把每个点的位移减去这个中位数,再做阈值判断。这样相机全局抖动和光照漂移可以被有效消除。
5.3 用调试窗口可视化光流状态
为了快速定位参数问题,我会在调试视频里同时显示三种信息:运动点的光流矢量线、跟踪失败的点的位置、以及当前帧的光流点数量。光流矢量线很简单:
for (size_t i = 0; i < nextPts.size(); i++) { if (status[i] == 1) { line(frame, prevPts[i], nextPts[i], Scalar(255, 0, 0), 1); } }这样就能直观看到背景点的光流矢量长度和方向。如果背景点的矢量长度和运动目标点几乎一样,说明全局抖动没消除;如果运动目标点的方向杂乱无章,说明 winSize 太大导致匹配错误。把 err 值也显示在控制台上,观察 err 的分布区间,作为调整阈值的依据。靠这组可视化,通常五分钟之内就能定位到问题参数。
6. 进阶应用:把光流检测结果接入多目标跟踪与调试技巧
6.1 多目标分离:按空间距离聚类光流点
single boundingRect 只能处理单个目标,画面里同时出现两个行人就会画出一个大框包住两人。更合理的做法是把光流点做空间聚类。OpenCV 里没有直接的空间聚类 API,但可以用 kmeans 或者自己写一个简单的距离聚类。下面是一种轻量做法:按帧间点距离做最近邻聚合。
vector<vector<Point2f>> clusters; for (size_t i = 0; i < movingPts.size(); i++) { bool placed = false; for (size_t j = 0; j < clusters.size(); j++) { Point2f center = clusters[j][0]; float dist = norm(movingPts[i] - center); if (dist < 40.0f) { clusters[j].push_back(movingPts[i]); placed = true; break; } } if (!placed) { vector<Point2f> newCluster; newCluster.push_back(movingPts[i]); clusters.push_back(newCluster); } }这个聚类通过动态判断点到簇首点的距离,距离小于 40 像素就归入同一个簇,否则新建簇。40 这个阈值和目标的实际像素尺寸相关,如果视频分辨率是 1920x1080,目标间的像素距离会变大,阈值要相应提高到 80 或 100。聚类完成后,对每个簇单独求 boundingRect,再剔除面积小于 100 平方像素的噪声框。
6.2 光流结果与背景差分结合的混合方案
稀疏光流在目标静止或缓慢运动时效果不好,因为位移低于阈值就被过滤掉。此时把光流法和背景差分结合是工业界常见做法。背景差分负责检测“出现过变化”的区域,光流负责确认“变化区域里哪些是真实运动”。具体实现是先用 MOG2 背景分割得到前景掩码,然后只在掩码为前景的区域做光流点筛选。这样既避免了光流对静止目标的漏检,又抑制了背景差分对光照变化的误检。
在 OpenCV 2.4.9 里 MOG2 的类名是 BackgroundSubtractorMOG2,用法是实例化后调用 apply(frame, fgMask),其中的 history 参数控制背景建模的帧数。history 太小会让运动慢的目标被并进背景,太大则光照突变后很久才能恢复,一般设为 500。
6.3 性能调优:降分辨率与 ROI 限制的收益
在 VS2010 配 OpenCV 2.4.9 的老环境里,性能瓶颈通常在 calcOpticalFlowPyrLK 的搜索窗口遍历上。最直接的优化是把视频帧缩小一半再计算光流。640x480 的视频缩到 320x240 后,光流计算量约降为原来的四分之一,而目标检测框则从缩小后的坐标乘 2 映射回原图即可。实测结果是在普通双核 CPU 上,原始分辨率下每帧耗时约 35 毫秒,缩小后降到 12 毫秒左右。
另一个有效手段是限定检测 ROI。监控场景里运动目标通常只出现在一条带状区域,可以提前框定 ROI,只在该区域提取特征点和计算光流。实现上就是在调用 goodFeaturesToTrack 和 calcOpticalFlowPyrLK 之前用 mask 参数限制范围。goodFeaturesToTrack 支持传入 mask 参数,mask 中非零区域才是特征点选取范围,calcOpticalFlowPyrLK 本身不支持 mask,但可以通过先裁剪图像再送入函数来达到同样的效果。裁剪图像用 Mat roi = frame(rect) 取子矩阵,光流计算完毕后把 nextPts 坐标加上 rect.x 和 rect.y 的偏移量。
如果 ROI 内特征点数量不足,说明这个区域缺乏纹理,可以考虑在 ROI 内用均匀网格点替代角点。网格点提取不需要 goodFeaturesToTrack,直接两层 for 循环生成坐标即可,配合 L-K 光流同样有效,而且网格点分布更加均匀。缺点是网格点数量固定,无法自适应目标纹理丰富度,所以通常只作为角点不足时的备用方案。
本文还有配套的精品资源,点击获取