3. 图像预处理:灰度化、直方图均衡化、滤波与增强

图像预处理,说白了就是给原始图像“洗把脸”。

相机采集到的数据,往往带着噪声、光照不均、对比度差等问题。如果直接扔给后面的检测算法,结果大概率会翻车。我做了这么多年视觉项目,可以负责任地告诉你:预处理做得好,后续算法事半功倍;预处理糊弄事,后面调参调到怀疑人生。

核心观点: 预处理不是可选项,而是机器视觉流水线的“地基”。地基不稳,上层建筑全是危楼。
图像预处理核心知识体系 图像预处理 灰度化 直方图均衡化 图像滤波 图像增强 加权平均法 最大值法 全局均衡 CLAHE 均值滤波 高斯滤波 中值滤波 锐化/伽马 预处理的目标:消除无关信息,增强有用特征,为后续检测/识别创造最佳条件

3.1 图像灰度化:降维打击

彩色图像每个像素有 R、G、B 三个通道,处理起来计算量太大。灰度化就是把三通道压缩成单通道,保留亮度信息,丢掉颜色信息。

常用的方法有三种:

方法 公式 特点 适用场景
加权平均法 Gray = 0.299R + 0.587G + 0.114B 符合人眼视觉特性 通用场景,最推荐
平均值法 Gray = (R + G + B) / 3 计算简单,但丢失细节 对精度要求不高的场合
最大值法 Gray = max(R, G, B) 保留高亮区域 高光检测场景
我的习惯: 除非有特殊需求,否则一律用加权平均法。OpenCV 的 cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 底层就是用的这个权重,稳定可靠。
import cv2
import numpy as np

# 读取彩色图像
img = cv2.imread('input.jpg')

# 方法1:OpenCV 内置函数(推荐)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 方法2:手动实现加权平均
b, g, r = cv2.split(img)
gray_manual = (0.299 * r + 0.587 * g + 0.114 * b).astype(np.uint8)

cv2.imwrite('gray.jpg', gray)

3.2 直方图均衡化:让对比度“支棱”起来

你想想看,一张图像如果整体偏暗或者偏亮,细节根本看不清楚。直方图均衡化就是通过拉伸像素值的分布范围,让暗的地方变亮,亮的地方变暗,整体对比度拉满。

我遇到过最典型的案例:一个工厂的零件检测项目,车间灯光不均匀,图像半边亮半边暗。直接做阈值分割根本没法用。后来用 CLAHE(限制对比度自适应直方图均衡化)处理了一下,效果立竿见影。

注意: 全局直方图均衡化容易放大噪声,尤其是在图像背景比较平坦的区域。我曾经在一个医疗影像项目里吃过这个亏——均衡化之后,原本干净的背景上全是噪点,差点把病灶给淹没了。

两种均衡化的对比:

类型 原理 优点 缺点
全局均衡化 (HE) 基于整张图像的直方图 实现简单,效果明显 噪声放大,过度增强
CLAHE 分块均衡 + 对比度限制 抑制噪声,局部细节好 参数需要调(clip limit、网格大小)
import cv2

img = cv2.imread('dark.jpg', 0)  # 以灰度图读取

# 全局直方图均衡化
equ = cv2.equalizeHist(img)

# CLAHE(推荐)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
clahe_img = clahe.apply(img)

cv2.imwrite('clahe_result.jpg', clahe_img)
参数经验: clipLimit 一般设在 1.0~3.0 之间,tileGridSize 用 8x8 起步。如果图像细节多,可以适当减小网格尺寸。

3.3 图像滤波:去噪与保边的博弈

滤波的本质是“卷积”。用一个小的窗口(核)在图像上滑动,对窗口内的像素做加权平均。不同的核,干不同的事。

三种最常用的滤波,我帮你拆开讲:

3.3.1 均值滤波

最简单的线性滤波。核内所有像素取平均。优点是快,缺点是——它不分青红皂白,边缘和噪声一起模糊。

blur = cv2.blur(img, (5, 5))  # 5x5 的核

3.3.2 高斯滤波

比均值滤波聪明一点。它根据像素离中心点的距离分配权重——越近权重越大,越远权重越小。这样能更好地保留边缘。

gaussian = cv2.GaussianBlur(img, (5, 5), sigmaX=1.0)
关键参数: sigmaX 控制高斯函数的“胖瘦”。sigma 越大,图像越模糊。我一般从 1.0 开始试,不够再加。

3.3.3 中值滤波

非线性的。把窗口内的像素排序,取中间值代替中心像素。它对椒盐噪声(黑白点噪声)有奇效。

我记得有一次做 PCB 焊点检测,图像上全是随机出现的白点噪声。均值滤波和高斯滤波都试了,效果很差。换成中值滤波,核大小设成 3,噪声直接消失,焊点边缘还清清楚楚。

median = cv2.medianBlur(img, 3)  # 核大小必须是奇数

三种滤波的对比总结:

滤波类型 噪声抑制 边缘保留 计算速度 最佳应用
均值滤波 一般 差 快 轻度噪声,实时性要求高
高斯滤波 较好 中等 中等 通用去噪,预处理标准步骤
中值滤波 优秀(椒盐噪声) 好 较慢 椒盐噪声、需要保边的场景

3.4 图像增强技术:让特征“跳”出来

增强不是去噪,而是主动提升图像的某些特征,让后续算法更容易抓到关键信息。

常用的增强手段:

  • 伽马校正: 调整图像的亮度曲线。gamma < 1 提亮暗部,gamma > 1 压暗亮部。我常用 gamma=0.5 来处理过曝的图像。
  • 拉普拉斯锐化: 增强边缘和细节。说白了就是让图像更“锋利”。
  • USM 锐化(Unsharp Masking): 先模糊,再与原图做差,把差值加回去。效果比直接拉普拉斯更自然。
import cv2
import numpy as np

img = cv2.imread('blurry.jpg')

# 伽马校正
gamma = 0.5
gamma_corrected = np.power(img / 255.0, gamma) * 255.0
gamma_corrected = gamma_corrected.astype(np.uint8)

# 拉普拉斯锐化
laplacian = cv2.Laplacian(img, cv2.CV_64F)
sharpened = cv2.addWeighted(img, 1.5, laplacian, -0.5, 0)

# USM 锐化
blurred = cv2.GaussianBlur(img, (0, 0), sigmaX=3.0)
usm = cv2.addWeighted(img, 1.5, blurred, -0.5, 0)

cv2.imwrite('enhanced.jpg', usm)
避坑指南: 我曾经在一个字符识别项目里过度锐化,结果字符边缘出现了“振铃效应”——一圈白一圈黑,OCR 直接识别失败。锐化要适度,过犹不及。

3.5 预处理流程设计思路

实际项目中,预处理不是把上面所有方法都跑一遍。而是根据问题来选。我一般这样思考:

  1. 先看噪声类型: 椒盐噪声?用中值滤波。高斯噪声?用高斯滤波。
  2. 再看光照条件: 不均匀?用 CLAHE。整体偏暗?用伽马校正。
  3. 最后看检测目标: 要边缘?做锐化。要区域?做平滑。
我的经验: 预处理流程越短越好。每多一步,就多一分信息损失。能用一步解决的,绝不用两步。