4、帧差法运动检测:原理讲解、两帧差分与三帧差分实现、阈值处理
帧差法,说白了就是运动检测里最朴实、最直接的方法。我刚开始接触视频分析时,第一个上手的就是它。为什么?因为它快,而且原理简单到让人一眼就能看明白。
你想想看,视频里如果有个物体在动,那相邻两帧之间肯定有变化。帧差法就是抓住这个「变化」来做文章。今天咱们就把它的原理、两种实现方式,还有阈值处理这块硬骨头啃下来。
4.1 帧差法的核心原理
帧差法的数学表达其实就一句话:D(x,y) = |I_t(x,y) - I_{t-1}(x,y)|。其中 I_t 是当前帧,I_{t-1} 是前一帧。两帧相减取绝对值,得到的就是差分图像。
嗯,这里要注意:差分图像里,像素值大的地方,就是运动区域。像素值接近0的地方,就是背景。就这么简单。
但实际项目中,我遇到过一个问题——如果背景本身在轻微抖动(比如树叶被风吹动),差分图像里也会出现很多噪声。所以光做差分还不够,还得配合阈值处理。
核心思想:帧差法假设背景是静止的,只有前景在运动。两帧相减后,运动区域会留下明显的「残影」。
4.2 两帧差分法实现
两帧差分是最基础的版本。我习惯用OpenCV的 absdiff 函数来做,比手动逐像素相减快得多。
import cv2
import numpy as np
cap = cv2.VideoCapture('test_video.mp4')
# 读取第一帧作为背景
ret, frame1 = cap.read()
gray1 = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)
while True:
ret, frame2 = cap.read()
if not ret:
break
gray2 = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY)
# 两帧差分
diff = cv2.absdiff(gray1, gray2)
# 阈值处理
_, thresh = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY)
cv2.imshow('Frame Diff', diff)
cv2.imshow('Threshold', thresh)
# 更新前一帧
gray1 = gray2.copy()
if cv2.waitKey(30) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
这段代码里,我用了 gray1 = gray2.copy() 来更新帧。为什么要用 copy()?因为如果不复制,直接赋值的话,两个变量会指向同一块内存,后面就乱套了。我曾经在这个坑里摔过一次,调试了半天才发现是引用问题。
个人经验:两帧差分对快速运动检测效果不错,但有个致命弱点——如果物体运动速度很慢,两帧之间变化太小,差分图像几乎看不到东西。这时候就得考虑三帧差分或者光流法了。
4.3 三帧差分法实现
三帧差分,说白了就是两帧差分的升级版。它用三帧图像:frame_{t-1}、frame_t、frame_{t+1}。先做两次两帧差分,再把结果做「与」操作。
为什么要这样做?因为两帧差分会留下「鬼影」——物体移动后,原来位置和当前位置都会留下残差。三帧差分通过逻辑与,能把这些鬼影去掉大部分。
import cv2
import numpy as np
cap = cv2.VideoCapture('test_video.mp4')
# 读取前三帧
ret, frame1 = cap.read()
ret, frame2 = cap.read()
ret, frame3 = cap.read()
gray1 = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)
gray2 = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY)
gray3 = cv2.cvtColor(frame3, cv2.COLOR_BGR2GRAY)
while True:
# 两次差分
diff1 = cv2.absdiff(gray2, gray1)
diff2 = cv2.absdiff(gray3, gray2)
# 阈值处理
_, thresh1 = cv2.threshold(diff1, 30, 255, cv2.THRESH_BINARY)
_, thresh2 = cv2.threshold(diff2, 30, 255, cv2.THRESH_BINARY)
# 逻辑与操作
motion = cv2.bitwise_and(thresh1, thresh2)
cv2.imshow('Motion Detection', motion)
# 滑动窗口:丢弃最旧帧,加入新帧
gray1 = gray2.copy()
gray2 = gray3.copy()
ret, frame3 = cap.read()
if not ret:
break
gray3 = cv2.cvtColor(frame3, cv2.COLOR_BGR2GRAY)
if cv2.waitKey(30) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
你看,三帧差分的核心就是那个 cv2.bitwise_and。它把两个差分结果中「同时有变化」的区域保留下来。这样,物体移动时留下的拖影就被过滤掉了。
避坑指南:我曾经在一个项目中用三帧差分检测行人,发现阈值设得太低时,行人走过的地方会留下一条长长的「尾巴」。后来把阈值从20调到40,效果好了很多。阈值这东西,真得根据场景反复调。
4.4 阈值处理的艺术
阈值处理,说白了就是决定「多大的变化才算运动」。这个值选得好不好,直接决定了检测效果。
| 阈值大小 | 效果 | 适用场景 |
|---|---|---|
| 10-20 | 非常敏感,噪声多 | 室内、光照稳定 |
| 30-50 | 适中,平衡噪声与检测 | 大多数场景 |
| 60-100 | 不敏感,只检测明显运动 | 室外、有风吹草动 |
我个人习惯的做法是:先用一个中等阈值(比如30)跑一遍,看看效果。如果噪声太多,就往上调;如果漏检太多,就往下调。没有万能公式,全靠经验。
另外,OpenCV的 threshold 函数有几种模式:
THRESH_BINARY:大于阈值的设为255,其余为0。最常用。THRESH_OTSU:自动计算阈值。适合光照变化大的场景。THRESH_TRIANGLE:另一种自动阈值方法,适合直方图有单峰的情况。
我的建议:如果场景光照稳定,用手动阈值就够了。如果光照变化大(比如室外从早到晚),试试 THRESH_OTSU。它虽然慢一点,但省去了手动调参的麻烦。
4.5 帧差法的优缺点总结
帧差法不是万能的,但它有自己的一席之地。我总结一下:
优点:
- 计算速度快,实时性高
- 实现简单,代码量少
- 对光照变化有一定鲁棒性(相比背景减除法)
缺点:
- 只能检测运动边界,不能提取完整运动物体
- 对慢速运动不敏感
- 阈值选择依赖经验
嗯,帧差法就像一把小刀——简单、锋利,但干不了重活。在后续章节中,我们会讲到更高级的方法,比如光流法、背景建模。但帧差法作为入门,绝对值得你花时间吃透。
最后说一句:别小看这个基础方法。我在一个工业检测项目里,就是用三帧差分加形态学处理,搞定了传送带上零件计数的问题。有时候,最简单的工具反而最管用。