4、图像预处理(下):图像滤波与阈值化
好,咱们接着聊图像预处理的下半部分。上一节我们讲了图像增强和直方图,这一节我们聚焦两个非常实用的操作:图像滤波和图像阈值化。这两个东西,说白了就是给图像“去噪”和“分割”的利器。
我个人做项目时,这两步几乎每次都会用到。尤其是滤波,选对了方法,后面跟踪的准确率能提升一大截。选错了?嗯,那画面就糊成一团了。
4.1 图像滤波:让图像更干净
滤波的本质是什么?就是用一个“小窗口”在图像上滑动,对窗口内的像素做某种数学运算。你想想看,图像里的噪声,通常都是孤立的、突兀的像素点。滤波就是要“抹平”这些突兀的地方。
OpenCV里提供了好几种滤波方法,我挑最常用的四种来讲。
4.1.1 均值滤波
均值滤波是最简单的一种。它把窗口内所有像素的平均值,作为中心像素的新值。
我在项目中遇到过这种情况:用均值滤波处理一张带有高斯噪声的图片,效果还行,但图像边缘会变模糊。为什么呢?因为边缘处的像素值变化剧烈,一平均,边缘就被“抹”掉了。
import cv2
import numpy as np
img = cv2.imread('noisy_image.jpg')
# 均值滤波,核大小为5x5
blurred = cv2.blur(img, (5, 5))
cv2.imshow('Mean Filter', blurred)
cv2.waitKey(0)
注意:核越大,模糊越严重。我建议从3x3开始试,不够再加大。别一上来就用15x15,那画面基本没法看了。
4.1.2 高斯滤波
高斯滤波和均值滤波有点像,但它不是简单平均。它给窗口内的像素分配了不同的权重——离中心越近的像素,权重越大。这个权重分布服从高斯函数。
为什么会这样?因为自然图像中,相邻像素的相关性更强。离得远的像素,对中心的影响应该小一些。高斯滤波更符合这个直觉。
# 高斯滤波,核大小5x5,标准差1.5
gaussian = cv2.GaussianBlur(img, (5, 5), 1.5)
cv2.imshow('Gaussian Filter', gaussian)
小技巧:标准差sigma值越大,图像越模糊。我个人习惯设成1.0到2.0之间,效果比较自然。
4.1.3 中值滤波
中值滤波就更有意思了。它把窗口内的像素排序,取中间那个值作为新值。
这招对付“椒盐噪声”特别管用。椒盐噪声就是图像上随机出现的白点和黑点,像撒了盐和胡椒一样。中值滤波能完美去除这种噪声,因为它直接跳过了那些极端的像素值。
我曾经用中值滤波处理过一张工业相机拍的零件图,上面全是椒盐噪声。均值滤波试了,高斯滤波也试了,效果都不理想。换成中值滤波,一次搞定。
# 中值滤波,核大小为5
median = cv2.medianBlur(img, 5)
cv2.imshow('Median Filter', median)
注意:中值滤波的核大小必须是奇数。3、5、7都行。核太大,图像细节会丢失。
4.1.4 双边滤波
双边滤波是这四种里最“聪明”的。它既考虑了像素的空间距离,又考虑了像素值的差异。
什么意思呢?就是它能在去噪的同时,尽量保留边缘。比如你有一张人脸照片,脸上有痘痘(噪声),但眼睛和嘴巴的轮廓(边缘)要保留。双边滤波就能做到这一点。
# 双边滤波,d=9,sigmaColor=75,sigmaSpace=75
bilateral = cv2.bilateralFilter(img, 9, 75, 75)
cv2.imshow('Bilateral Filter', bilateral)
经验之谈:双边滤波的参数调起来比较费劲。d是直径,sigmaColor控制颜色相似度,sigmaSpace控制空间距离。我一般先固定d=9,然后调sigmaColor和sigmaSpace,从50开始试。
4.2 四种滤波方法对比
我把这四种方法整理成了一个表格,方便你对比选择。
| 滤波方法 | 原理 | 适用场景 | 缺点 |
|---|---|---|---|
| 均值滤波 | 窗口内像素平均 | 简单噪声,实时性要求高 | 边缘模糊严重 |
| 高斯滤波 | 加权平均,权重服从高斯分布 | 高斯噪声,自然图像 | 边缘仍会模糊 |
| 中值滤波 | 取窗口内像素的中值 | 椒盐噪声 | 处理时间长,细节可能丢失 |
| 双边滤波 | 空间距离+像素值差异加权 | 需要保留边缘的去噪 | 计算量大,参数难调 |
4.3 图像阈值化:把图像变成黑白分明
阈值化,说白了就是“一刀切”。设定一个阈值,像素值大于它的变成白色,小于它的变成黑色。这样图像就变成了二值图像,只有0和255两种值。
为什么要做阈值化?因为很多后续操作,比如轮廓检测、目标跟踪,都需要二值图像作为输入。你想想看,一张彩色图里有几百种颜色,计算机怎么知道哪个是目标?变成黑白两色,目标就清晰了。
4.3.1 全局阈值
全局阈值就是整张图像用一个固定的阈值。比如设定阈值为127,所有像素值大于127的变成255,小于127的变成0。
# 先转成灰度图
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 全局阈值,阈值为127
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
cv2.imshow('Global Threshold', thresh)
注意:全局阈值对光照不均匀的图像效果很差。比如一张图片左边亮右边暗,用同一个阈值,左边可能全白,右边全黑。这时候就需要自适应阈值了。
4.3.2 自适应阈值
自适应阈值会根据图像不同区域的亮度,自动计算不同的阈值。它把图像分成一个个小区域,每个区域单独计算阈值。
我在项目中遇到过这种情况:用全局阈值处理一张文档扫描图,结果文字断断续续的。换成自适应阈值,文字立马清晰了。嗯,这就是自适应阈值的威力。
# 自适应阈值,blockSize=11,C=2
adaptive = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 11, 2)
cv2.imshow('Adaptive Threshold', adaptive)
参数说明:blockSize是计算阈值的小区域大小,必须是奇数。C是一个常数,从计算出的均值中减去这个值。我一般设blockSize=11,C=2,效果比较稳定。
4.3.3 Otsu阈值
Otsu阈值是一种自动寻找最佳阈值的方法。它假设图像由前景和背景两部分组成,然后找到一个阈值,使得这两部分的类内方差最小,类间方差最大。
说白了,就是让前景和背景的区分度最大。你不需要手动设阈值,Otsu会自动帮你算出来。
# Otsu阈值,自动计算最佳阈值
ret, otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f'Otsu自动计算的阈值: {ret}')
cv2.imshow('Otsu Threshold', otsu)
核心要点:Otsu最适合双峰直方图的图像。如果图像的直方图只有一个峰,或者多个峰,Otsu的效果就不太好了。我曾经用Otsu处理过一张直方图单峰的图片,结果阈值偏得离谱。所以,用之前最好先看看直方图。
4.4 阈值化方法对比
| 方法 | 原理 | 适用场景 | 注意事项 |
|---|---|---|---|
| 全局阈值 | 固定阈值 | 光照均匀的图像 | 光照不均时效果差 |
| 自适应阈值 | 局部计算阈值 | 光照不均、文档扫描 | blockSize和C需要调参 |
| Otsu阈值 | 自动寻找最佳阈值 | 双峰直方图的图像 | 单峰直方图效果差 |
4.5 实战建议
好了,理论讲完了,我给大家一个实战中的选择思路。
如果你拿到一张图,先看有没有噪声。有噪声?先滤波。椒盐噪声用中值滤波,高斯噪声用高斯滤波。想保留边缘?用双边滤波。
滤波完了,再做阈值化。光照均匀用全局阈值,光照不均用自适应阈值。如果懒得调参,试试Otsu,但记得先看直方图。
我曾经在一个项目中,先用了高斯滤波去噪,然后用自适应阈值做二值化,最后用轮廓检测找到了目标。整个过程一气呵成,效果非常好。你想想看,如果跳过滤波直接阈值化,噪声会被当成目标,那结果就全乱了。
嗯,这一节的内容就到这里。下一节我们讲形态学操作,那是二值图像处理的又一大利器。