本节摘要:运动分析回答"画面里什么在动、动了多少、往哪动"。三套方案各有地盘:帧间差分最简单,适合触发告警;背景减除(MOG2、KNN)建背景模型分离前景,是运动目标提取的主力;光流给出逐像素/逐点的运动方向。本节跑通三者的最小实现,讲清背景模型的更新机制与阴影抑制,并给出选型路线。
阅读完本节,你应当能够:
帧间差分——两帧相减,动的区域亮,静的区域黑:
import cv2 prev = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY) curr = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY) diff = cv2.absdiff(prev, curr) _, mask = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)
mask 里白色的地方就是两帧之间发生变化的区域。25 这个阈值过滤掉轻微的传感器噪声(第 2 章第 4 节的固定阈值在此复用)。
背景减除——建一个"背景模型",每帧与模型比对:
backsub = cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=16, detectShadows=True) mask = backsub.apply(frame) # 每帧调用,模型自动更新 mask = cv2.threshold(mask, 200, 255, cv2.THRESH_BINARY)[1] # 阴影127前景255
光流——给一组点,看它们下一帧跑到哪:
p0 = cv2.goodFeaturesToTrack(prev_gray, 100, 0.1, 7) p1, st, err = cv2.calcOpticalFlowPyrLK(prev_gray, curr_gray, p0, None) good = p1[st == 1] # st=1 表示成功跟踪
三段代码对应三个层次的问题:差分答"变了吗"、背景减除答"哪些像素属于运动物体"、光流答"往哪个方向、多快"。
相邻两帧相减,逻辑简单到不需要模型。优点:零训练、零状态、计算极便宜,对光照突变的鲁棒性意外地好(变化是相对的)。缺点有三个:目标运动慢时与背景几乎重合,差分"看不见"慢慢挪动的目标;提取的掩码是"运动扫过的区域",目标内部纹理均匀处是空洞,轮廓不完整;只知道"变了",不知道是目标进来还是背景被遮挡。
定位就是轻量触发器:门口有人经过触发抓拍、画面异常触发告警,够用且几乎不占算力。
思路:统计每个像素在时间轴上的取值分布,多数时间是"背景态";新帧里明显偏离档案的像素判为前景。均值背景是最朴素的版本(本章开头的思路),但树叶晃动、水面波光这类"背景本身在抖"的场景,单一均值无法描述。
MOG2 的解法是混合高斯模型:每个像素的档案不是一条数,而是 3 到 5 个高斯分布的混合——多数时间匹配到的分布算背景,偶尔出现的另一套分布(比如摇晃的树叶)也纳入档案。新帧来了,匹配任何背景分布则判背景,全不匹配则判前景。KNN 是近年加入的方案,用最近邻思路做同样的事,对复杂纹理背景常更稳。
两个关键参数:history 是"档案统计多长时间",值越大背景越保守(适应光照渐变慢),500 是常规起点;varThreshold 是"偏离多少算可疑",噪声大调大,远处小目标检测不出时调小。detectShadows 开启后阴影被标成 127(前景是 255),需要再用阈值把阴影滤掉——光照强的室外场景这个功能价值巨大,阴影跟着目标走、极易被误当前景的一部分。
背景模型还有个"冷启动"问题:视频开头模型未收敛,前几秒全是前景。对策是给 apply 传学习率参数:初始化阶段用较大学习率快速建模,稳定后转默认自适应更新,或干脆先空跑几百帧不输出。
光流假设"同一像素的亮度在相邻帧间不变",据此为每个像素解一个运动向量(亮度恒常假设在真实世界里近似成立)。稠密光流(Farneback)对全图每像素算向量,信息全但计算重;稀疏光流(金字塔 Lucas-Kanade,calcOpticalFlowPyrLK)只算指定的一组点,配合上一章的 goodFeaturesToTrack 角点(第 3 章第 1 节的 Shi-Tomasi 正是为此设计),快且稳,是视频跟踪的标准搭配。
稀疏光流的输出是每个点的新位置与状态位,画在图上就是一张"运动场"——箭头指示方向与幅度。应用直接而实用:手势识别(运动区域的主方向)、车流方向统计、相机自运动估计。
背景减除的原始掩码必然带碎斑与空洞,标准后处理正是第 2 章的组合:开运算清碎斑、闭运算填空洞、findContours 提取连通域、按面积过滤掉太小的区域,剩下的轮廓就是"运动目标候选框",交给下一节的跟踪器或第 4 章的检测器。
实战里三套方案常常组合:背景减除出候选框(便宜),框内跑检测器确认类别(贵但准),光流在跟踪丢失时提供运动线索。纯帧间差分做触发、检测器做确认,是算力受限设备的经典两段式。选择的第一依据永远是相机是否固定——背景减除的前提是"背景基本不动",云台相机或手持场景直接出局,只能走差分或光流。
MOG2 的调参顺序:先看原始掩码(imshow 在循环里),碎斑多先加大 varThreshold 或在形态学环节清理;目标整体检出但"缺一块",是目标速度慢被背景模型吸收了,减小 history;光照渐变场景(日出日落)里前景越来越多,加大 history 让模型更慢跟随,或接受定期重建模型。
帧率越低,相邻帧间目标位移越大:差分图更"实"(不容易漏慢目标),但空洞也更大;帧率高则相反。改了帧率(比如从 25 降到 10)后差分阈值与形态学核尺寸都要复查,参数不是一次调好终身有效。
⚠️ 常见坑:忘了滤阴影,检测框比目标大一圈还"拖着尾巴",目标走哪框跟哪大——检查 mask 里的 127 值是否被当成前景用了(阈值卡 200 以上即可滤掉)。另一个坑是相机自动曝光(AE)开着,画面亮度突变瞬间整个画面变前景;固定曝光(CAP_PROP_EXPOSURE 关自动)是监控类项目的第一项配置。

雨雪是"背景在动"的极端案例,粒子噪声会让掩码布满碎斑。应对层次:提高 varThreshold 容忍粒子级波动;形态学开运算清残斑;时间维度的持续性过滤(真目标会连续出现多帧,雨点帧间无连续性,用一个"连续 N 帧命中才上报"的计数器即可过滤)。极端天气下宁可漏报攒着,也别误报刷屏。
几像素级的周期性晃动,背景模型能部分吸收(它本来就是统计容忍),配合形态学清理基本无感;晃到十像素以上,边缘与背景反复"进出"模型,掩码沿高对比边缘出现持续噪声带,此时要么物理加固,要么上电子稳像(配准加 warp,计算量上一个台阶)——先怀疑支架螺丝,再怀疑算法。
正常现象。目标转出视野、被遮挡、纹理区滑出,点就会失跟(状态位为 0)。设计上要"边跟边补":每隔几帧重新 goodFeaturesToTrack 补充新点,维持点的总数与覆盖;全靠一茬点跟到底,几十帧后就所剩无几了。
用一个高频真实需求收束本节:统计画面中越过指定线的人数(单向)。固定机位、俯视或半俯视视角。
第一步建背景模型。MOG2 挂在脚手架的处理层,前两百帧只更新不输出(冷启动,本节 3.3);阴影检测开着,掩码阈值卡 200 滤影。
第二步净化与目标提取。开闭运算清补(第 2 章第 3 节配方),findContours 加面积过滤出人员候选框。到这一步为止全是已有配方,无新知识。
第三步越线判定。在画面里画一条检测线(两个端点定义),对每个目标框的底边中点做"上一帧与这一帧是否分居线两侧"的判断:是,且方向与设定一致,计一次数。用底边中点而不是框中心,因为人过线时脚先过、框中心滞后,中点判定在行人半身重叠时更稳。
第四步防重与身份。同一人来回跨线会重复计数,两种解法:计数后给该目标加冷却期(几秒内不再计),或用下一节的跟踪器给目标编号、同号只计一次。冷却期方案十行代码,跟踪方案更严谨——按精度需求选,先用冷却期跑起来,不够再升级。
第五步校验。人工数一段视频的真实过线数对比系统计数,偏差超过百分之五回来调面积过滤与冷却期。这个"对账"步骤上线前必做——计数控不住的系统,算法再漂亮也没有意义。
那是学习率与场景变化在拉锯。树叶晃动、光照缓变会持续污染背景模型,模型一边学一边忘,掩码边缘就跟着闪。对策分两层:检测端提高 varThreshold 让小幅度扰动过不了门槛;模型端给 learningRate 一个小而非零的值,让背景缓慢更新跟上演化,又不至于把停住的行人学进背景。两参数联调的标准流程是:固定一个、扫另一个,用一段包含"静止目标"的录像做回归测试。
有了运动目标候选框,下一节给它们编号并持续跟随——MeanShift、CSRT 与八种跟踪器的选型一网打尽。