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_tframe_{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 帧差法的优缺点总结

帧差法不是万能的,但它有自己的一席之地。我总结一下:

优点:

  • 计算速度快,实时性高
  • 实现简单,代码量少
  • 对光照变化有一定鲁棒性(相比背景减除法)

缺点:

  • 只能检测运动边界,不能提取完整运动物体
  • 对慢速运动不敏感
  • 阈值选择依赖经验

嗯,帧差法就像一把小刀——简单、锋利,但干不了重活。在后续章节中,我们会讲到更高级的方法,比如光流法、背景建模。但帧差法作为入门,绝对值得你花时间吃透。

最后说一句:别小看这个基础方法。我在一个工业检测项目里,就是用三帧差分加形态学处理,搞定了传送带上零件计数的问题。有时候,最简单的工具反而最管用。