一、基本原理
帧差法是运动检测中最简单、最直观的方法之一,其核心思想是相邻帧相减,差值即为运动区域。简单来说就是连续的两个帧图像之间,背景区域基本不变,而运动目标区域的像素值会发生明显变化,因此,将当前帧与前一帧像素相减,取绝对值,得到”差分图像“,差分图像中值大的像素点就属于运动目标。
从基本原理中也可以看出对于帧差法来说更适合目标运动明显、背景相对静止的场景。
二、关键步骤
1.灰度化去噪:将色彩帧转化为灰度图,再用高斯滤波消除噪声。
2.帧差计算:用前一帧减去后一帧得到差分图。
3.二值化阈值:对差分图做阈值处理,大于阈值的像素视为前景,小于阈值的视为背景。
4.形态学处理:开运算去除噪点,闭运算填补空洞。
5.轮廓提取:从二值图中寻找联通区域作为候选目标。
6.候选筛选:通过面积、宽高比、位置距离等条件筛选出最可能的目标。
优点:计算极快,实时性好;实现简单无需训练;对光照条件有一定的鲁棒性。
缺点:只能检测运动目标的边缘轮廓;对目标运动速度过于敏感;对于静止目标接近失效;需要连续帧进行分析处理。
三、代码实现
1.帧差计算
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (5, 5), 0) diff = cv2.absdiff(self.prev_frame, gray) _, diff = cv2.threshold(diff, 20, 255, cv2.THRESH_BINARY)将当前帧同样转为灰度并高斯滤波,然后与上一帧做减法。阈值为20(经验值),大于20的像素变为255(前景),其余为0(背景)。阈值的选取直接影响检测灵敏度:太小则噪声多,太大则弱运动被忽略。
2.形态学处理
kernel = np.ones((3, 3), np.uint8) diff = cv2.morphologyEx(diff, cv2.MORPH_OPEN, kernel) # 开运算 diff = cv2.morphologyEx(diff, cv2.MORPH_CLOSE, kernel) # 闭运算开运算(先腐蚀后膨胀):去除孤立噪点。
闭运算(先膨胀后腐蚀):填补目标区域内部细小空洞。
3.ROI区域约束
x, y, w, h = self.prev_bbox margin = int(max(w, h) * 1.0) x1 = max(0, x - margin) y1 = max(0, y - margin) x2 = min(frame.shape[1], x + w + margin) y2 = min(frame.shape[0], y + h + margin) roi_diff = np.zeros_like(diff) roi_diff[y1:y2, x1:x2] = diff[y1:y2, x1:x2]以上一帧目标框为中心,向外扩展一个 margin(大小等于目标框的长边),裁剪出一个ROI区域。不在ROI内的差分像素全部置0。这种做法基于"运动目标在相邻帧间位移不会太大"的假设,有效减少背景中其他运动的干扰。
4.候选框评估
def _motion_bbox(self, diff, frame_shape): contours, _ = cv2.findContours(diff, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) best = None best_score = -1 prev_center = self._center(self.prev_bbox) for c in contours: x, y, w, h = cv2.boundingRect(c) area = w * h # 筛选条件1:面积在目标面积的0.1倍到5倍之间 if area < self.target_area * 0.1 or area > self.target_area * 5: continue # 筛选条件2:宽高比变化不超过2 aspect = w / (h + 1e-6) if abs(aspect - self.target_aspect) > 2: continue # 筛选条件3:位移不超过目标最大尺寸的2倍 center = self._center((x, y, w, h)) dist = np.linalg.norm(center - prev_center) max_dim = max(self.prev_bbox[2], self.prev_bbox[3]) if dist > max_dim * 2: continue # 综合评分:距离权重0.5 + 面积权重0.3 + 宽高比权重0.2 distance_score = 1.0 / (1.0 + dist / max_dim) area_score = min(area / self.target_area, self.target_area / area) aspect_score = 1.0 / (1.0 + abs(aspect - self.target_aspect)) score = 0.5 * distance_score + 0.3 * area_score + 0.2 * aspect_score if score > best_score: best_score = score best = (x, y, w, h) return best三个筛选条件:一是面积约束,排除过大/过小的噪声区域;二是宽高比约束,保持目标的形状一致性;三是位移约束,限制搜索范围,防止跳变。综合评分函数用加权和的方式,将距离、面积相似度、宽高比相似度归一化到 [0,1] 区间。权重分配距离最重要(0.5),因为运动目标在帧间通常是连续平缓移动的。
5.状态更新
if cand is None: self.lost_count += 1 cand = self.prev_bbox # 跟丢时保持上一帧位置 else: self.lost_count = 0 self.prev_bbox = cand self.prev_frame = gray # 更新为下一帧差做准备 self.trajectory.append(self.prev_bbox) return self.prev_bbox当找不到合适候选框时,lost_count 递增,当前位置保持不动(prev_bbox不变)。找到候选则重置丢失计数,并更新 prev_bbox。最后将当前灰度图保存为 prev_frame 供下一帧使用。
四、整体流程
第一帧:
frame → 灰度化 + 高斯滤波 → 保存为 prev_frame;
标注框 → 提取area、aspect → 保存为 prev_bbox。
后续帧:
当前帧 → 灰度化 + 高斯滤波 → gray;
帧差:diff = |gray - prev_frame|
二值化:阈值20
形态学:开运算 + 闭运算
ROI裁剪:以上一帧框为中心
找轮廓 → 候选筛选(面积/宽高比/位移)
评分排序 → 选出最优框
跟丢处理或不跟丢
prev_frame = gray, prev_bbox = cand
返回 (x, y, w, h)。
五、局限性
1.空洞问题:帧差法只会检测到目标前后两帧位置变化的区域,目标重叠部分会被抵消,所以找到的轮廓可能是目标边缘的两块区域而非完整目标.
2.目标静止则丢失:当目标停止运动时,两帧之间无差异,没有任何轮廓极易造成目标丢失。
3.单一阈值:阈值 20 是硬编码经验值,在不同光照、不同对比度场景下不一定最优。
4.缺乏尺度自适应:目标靠近/远离摄像机时面积会变化,代码虽然允许面积在 0.1~5 倍范围,但是没有在线更新机制。