3、图像预处理技术:高斯滤波、中值滤波、形态学操作(腐蚀与膨胀)

各位同学,欢迎来到第三章。上一章我们聊了怎么把视频帧抓出来,但说实话,摄像头直接拍出来的画面,往往「脏」得很。噪点、光线不均、小抖动……这些乱七八糟的东西,会直接搞崩你的运动检测算法。

所以,在正式做运动分析之前,我们必须先做一件事——图像预处理。说白了,就是把图像「洗干净」,让后面的算法能看清重点。

我个人习惯把预处理分成两大块:滤波去噪形态学修形。今天我们就一个一个来啃。

3.1 为什么要滤波?

你想想看,摄像头传感器在弱光下,每个像素点都会有点随机波动。这些波动在画面上就是一个个小亮点或小暗点,我们叫它「椒盐噪声」或「高斯噪声」。

如果不处理,运动检测会把每个噪点都当成「运动目标」。结果就是——满屏都在闪,根本没法用。

所以,滤波的目的很简单:保留图像的主要结构,去掉那些没用的随机波动

3.2 高斯滤波:温柔的模糊

高斯滤波,是我在项目中用得最多的滤波器。它的原理其实很直观:用一个「权重模板」去扫过每个像素,中心点权重最大,越往边缘权重越小。最后取加权平均。

为什么用高斯?因为自然界很多噪声都服从高斯分布。说白了,用高斯滤波去对付高斯噪声,就是「用魔法打败魔法」。

核心参数:核大小(ksize)和标准差(sigma)。核越大,图像越模糊;sigma越大,边缘保留越差。

我在项目中遇到过一个问题:用5x5的高斯核去处理一个运动模糊的视频,结果运动目标边缘全糊了,检测框飘得厉害。后来我把核缩小到3x3,sigma设成0.5,效果立马好了很多。

嗯,这里要注意:高斯滤波不是越大越好。要根据你的目标大小来调。目标小,核就得小。

import cv2

# 读取图像
img = cv2.imread('frame.jpg')

# 高斯滤波,核大小5x5,标准差1.5
blurred = cv2.GaussianBlur(img, (5, 5), 1.5)

# 显示结果
cv2.imshow('Gaussian Blur', blurred)
cv2.waitKey(0)

3.3 中值滤波:专治「椒盐噪声」

中值滤波的思路更暴力——它把窗口内的所有像素排序,取中间那个值作为输出。

为什么会有这种操作?因为椒盐噪声(就是那种黑白点)是极端值。排序后,极端值要么在最前面,要么在最后面,中间值大概率是正常像素。所以中值滤波去椒盐噪声,效果一绝。

我曾经在一个工厂质检项目里,摄像头旁边有电机干扰,画面全是黑白闪烁点。高斯滤波怎么调都去不干净,换成中值滤波,3x3的核,一次搞定。

小技巧:中值滤波的核大小必须是奇数。3x3、5x5都行。核越大,去噪越强,但图像细节丢失也越严重。
# 中值滤波,核大小5
median = cv2.medianBlur(img, 5)

cv2.imshow('Median Filter', median)
cv2.waitKey(0)

3.4 高斯 vs 中值:怎么选?

很多新手会问:到底用哪个?我一般这样判断:

  • 噪声是均匀分布的细小颗粒(比如弱光下的彩色噪点)→ 用高斯滤波
  • 噪声是黑白点、孤立亮点(比如传感器坏点、电磁干扰)→ 用中值滤波
  • 既要降噪又要保留边缘(比如运动目标边缘清晰)→ 优先考虑中值滤波

当然,实际项目中我经常两个都用:先中值去椒盐,再高斯平滑一下。效果往往比单一滤波好。

3.5 形态学操作:腐蚀与膨胀

滤波做完,图像干净了。但运动检测出来的前景区域,往往还是「破破烂烂」的——有空洞、有毛刺、有孤立的小点。

这时候就需要形态学操作上场了。说白了,就是用数学形态学的方法,去修整二值图像的形状

3.5.1 腐蚀:让白色区域「瘦一圈」

腐蚀的原理很简单:用一个结构元素(比如3x3的矩形)去扫图像。只有结构元素完全覆盖在白色区域时,中心点才保留为白色。否则变成黑色。

结果就是:白色区域缩小一圈,细小的白色噪点被消除,两个靠得很近的白色区域会被分开。

我记得有一次做行人检测,背景里有树叶晃动,腐蚀操作直接把那些小树叶噪点全干掉了,效果立竿见影。

import cv2
import numpy as np

# 假设我们有一个二值图像
binary = cv2.imread('mask.jpg', 0)

# 定义结构元素:3x3矩形
kernel = np.ones((3, 3), np.uint8)

# 腐蚀
eroded = cv2.erode(binary, kernel, iterations=1)

cv2.imshow('Eroded', eroded)
cv2.waitKey(0)

3.5.2 膨胀:让白色区域「胖一圈」

膨胀和腐蚀正好相反。只要结构元素覆盖的区域里有一个白色像素,中心点就变成白色。

结果:白色区域扩大一圈,空洞被填充,断裂的区域被连接起来。

我一般把膨胀用在腐蚀之后。先腐蚀去掉小噪点,再膨胀把目标区域恢复回来。这个组合操作叫「开运算」。反过来,先膨胀再腐蚀叫「闭运算」,用来填充空洞。

# 膨胀
dilated = cv2.dilate(binary, kernel, iterations=1)

cv2.imshow('Dilated', dilated)
cv2.waitKey(0)

3.6 开运算与闭运算:实战组合拳

在实际的运动检测流程中,我很少单独用腐蚀或膨胀。一般都是组合使用:

操作 顺序 效果 适用场景
开运算 先腐蚀 → 后膨胀 去除小噪点,断开粘连 背景有细小干扰(树叶、灯光闪烁)
闭运算 先膨胀 → 后腐蚀 填充小空洞,连接断裂 目标内部有空洞(如行人身体区域)
# 开运算
opening = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)

# 闭运算
closing = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
避坑指南:我曾经在一个人脸检测项目里,闭运算的核设得太大(15x15),结果把两个人的脸连在了一起,检测框直接框了两个人。后来改成7x7,问题解决。核的大小一定要根据目标尺寸来调,别贪心。

3.7 预处理流程总结

好了,我们把今天的内容串起来。一个典型的运动检测预处理流程,我一般是这么做的:

  1. 读取帧 → 转灰度(减少计算量)
  2. 中值滤波(3x3或5x5)→ 去椒盐噪声
  3. 高斯滤波(3x3,sigma=1.0)→ 平滑均匀噪声
  4. 二值化(比如背景减除法得到前景掩码)
  5. 开运算(3x3核,1-2次)→ 去掉小噪点
  6. 闭运算(5x5核,1次)→ 填充目标空洞

这个流程我用了很多年,在大多数室内外场景下都表现稳定。当然,具体参数你得根据你的摄像头和场景微调。

下一章,我们会正式进入运动检测的核心——背景减除法。到时候你会发现,预处理做得好,后面的算法就像开了挂一样顺畅。

今天就到这里。去试试代码,看看你的摄像头画面经过预处理后,是不是干净了很多?