4. 图像预处理实战:高斯滤波、中值滤波、直方图均衡化、ROI提取、图像缩放与仿射变换

各位同学,欢迎来到第四讲。上一章我们聊了相机标定和图像采集,那都是「把图拿到手」的功夫。这一章,咱们要干点更实在的——把图收拾干净,把感兴趣的区域拎出来

说实话,我在分拣机项目上踩过最大的坑,不是算法选型不对,而是预处理没做好。你想想看,一张满是噪点的图,你拿什么深度学习模型去识别都白搭。所以这一章,我把自己这些年积累的预处理实战经验,全盘托出。

4.1 高斯滤波:去噪但不模糊边缘

高斯滤波,说白了就是用周围像素的加权平均来替换中心像素。权重由高斯函数决定——离中心越近,权重越大。

为什么用高斯?因为自然噪声(比如传感器热噪声)通常服从高斯分布。用高斯滤波去噪,数学上最匹配

我在做食品分拣机时,遇到过一种情况:传送带震动导致图像出现高频抖动噪声。用均值滤波,边缘糊成一团;用中值滤波,处理速度跟不上。最后是高斯滤波+适当核大小搞定的。

核心参数:核大小 (ksize) 和标准差 (sigmaX)。核越大,去噪越强,但细节丢失也越多。我一般从 3x3 开始试,不行再上 5x5。
import cv2
import numpy as np

# 读取图像
img = cv2.imread('part_on_belt.jpg', cv2.IMREAD_GRAYSCALE)

# 高斯滤波
gauss_blur = cv2.GaussianBlur(img, (5, 5), 1.5)

# 显示对比
cv2.imshow('Original', img)
cv2.imshow('Gaussian Blur', gauss_blur)
cv2.waitKey(0)
我的小技巧:sigmaX 设为 0 时,OpenCV 会根据核大小自动计算。我习惯手动设一个 1.0~2.0 的值,效果更可控。

4.2 中值滤波:对付椒盐噪声的利器

中值滤波,就是用邻域内所有像素的中位数代替中心像素。它对椒盐噪声(黑白点噪声)有奇效。

为什么?因为椒盐噪声的像素值要么极大(255),要么极小(0),取中位数时会被正常像素「挤掉」。你想想看,一个 3x3 区域里,8个正常像素+1个白点,中位数肯定是正常值。

我曾经在电子元件分拣项目中,遇到一批 PCB 板图像全是白点——那是反光造成的。用高斯滤波去不掉,用中值滤波,核大小选 3x3,一遍就干净了。

注意:中值滤波的核大小必须是奇数。而且核越大,图像越「油画感」。我一般不超过 5x5,除非噪声特别严重。
# 中值滤波
median_blur = cv2.medianBlur(img, 5)  # 核大小5x5

cv2.imshow('Median Blur', median_blur)

4.3 直方图均衡化:让对比度「活」过来

直方图均衡化,就是把图像的灰度分布从「挤在一起」拉伸到「均匀分布」。说白了,让暗的地方更暗,亮的地方更亮,中间细节全出来。

我在做物流包裹分拣时,仓库光线不均匀,有的地方暗得看不清条码。直方图均衡化一上,条码立刻清晰可辨。

但要注意:全局均衡化有时会过度增强噪声。所以我更推荐 CLAHE(对比度受限自适应直方图均衡化)。

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

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

cv2.imshow('Global Equalization', equ)
cv2.imshow('CLAHE', clahe_img)
参数调优:clipLimit 控制对比度限制,我一般设 2.0~3.0。tileGridSize 是分块大小,8x8 是常用值。如果图像纹理细腻,可以改小到 4x4。

4.4 ROI提取:只关心该关心的

ROI(Region of Interest)提取,就是从大图中切出你真正要处理的那一小块。在分拣机上,物料位置是固定的,我们只需要处理物料所在区域。

这样做的好处:

  • 减少计算量:只处理 ROI,速度翻倍
  • 排除干扰:背景中的传送带、螺丝孔不会影响识别
  • 简化算法:在固定 ROI 内做阈值分割,参数不用频繁调整
# 假设物料在图像中心区域
x, y, w, h = 200, 150, 300, 250
roi = img[y:y+h, x:x+w]

cv2.imshow('ROI', roi)
我曾经踩过的坑:ROI 坐标写死了,结果传送带一偏移,切出来的全是背景。后来我加了动态 ROI——先做边缘检测找到物料轮廓,再根据轮廓外接矩形动态切图。嗯,这个后面章节会细讲。

4.5 图像缩放:保持比例还是牺牲精度?

图像缩放,最常用的就是 cv2.resize。但这里有个坑:插值方法的选择

场景 推荐插值方法 原因
缩小图像 INTER_AREA 避免摩尔纹,保留整体信息
放大图像 INTER_CUBIC 或 INTER_LINEAR 平滑,减少锯齿
实时处理 INTER_LINEAR 速度快,效果可接受
# 缩小到一半
small = cv2.resize(img, (0,0), fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)

# 放大到2倍
large = cv2.resize(img, (0,0), fx=2.0, fy=2.0, interpolation=cv2.INTER_CUBIC)
我的习惯:如果只是预览,用 INTER_LINEAR。如果是最终输出给识别算法,用 INTER_AREA 缩小或 INTER_CUBIC 放大。别问为什么,问就是试出来的。

4.6 仿射变换:旋转、平移、缩放一把抓

仿射变换,就是用矩阵乘法对图像做线性变换。旋转、平移、缩放、剪切,全都能做。

在分拣机上,物料可能歪着放。我们需要先做仿射变换把它「摆正」,再识别。否则 OCR 识别歪着的字符,准确率直接腰斩。

# 获取图像尺寸
h, w = img.shape[:2]

# 定义变换矩阵:旋转30度,中心点缩放
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, 30, 1.0)  # 旋转30度,不缩放

# 执行仿射变换
rotated = cv2.warpAffine(img, M, (w, h))

cv2.imshow('Rotated 30 deg', rotated)

更复杂的场景——比如物料在传送带上位置不固定——我们需要先定位,再变换。定位可以用模板匹配或特征点匹配,找到物料上的几个关键点,然后计算变换矩阵。

实战经验:仿射变换后的图像边缘会有黑边。如果黑边影响后续处理,可以用 cv2.BORDER_REPLICATE 或 cv2.BORDER_REFLECT 填充。我一般用 BORDER_REPLICATE,复制边缘像素,看起来自然。
# 带边界填充的仿射变换
rotated_filled = cv2.warpAffine(img, M, (w, h), 
                                borderMode=cv2.BORDER_REPLICATE)

4.7 综合实战:分拣机物料预处理流水线

好了,知识点都讲完了。咱们来串一个完整的预处理流水线。这是我在实际项目中用过的流程:

  1. 读图:从相机获取灰度图
  2. ROI提取:根据传送带位置,切出物料区域
  3. 高斯滤波:去除传感器噪声(核大小 5x5)
  4. CLAHE:增强对比度,让物料细节出来
  5. 仿射变换:根据物料角度,旋转摆正
  6. 缩放:统一缩放到 224x224(给神经网络用)
def preprocess_pipeline(img, roi_rect, angle):
    # 1. ROI提取
    x, y, w, h = roi_rect
    roi = img[y:y+h, x:x+w]
    
    # 2. 高斯滤波
    blurred = cv2.GaussianBlur(roi, (5,5), 1.5)
    
    # 3. CLAHE
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
    enhanced = clahe.apply(blurred)
    
    # 4. 仿射变换(旋转摆正)
    h_roi, w_roi = enhanced.shape[:2]
    center = (w_roi // 2, h_roi // 2)
    M = cv2.getRotationMatrix2D(center, angle, 1.0)
    aligned = cv2.warpAffine(enhanced, M, (w_roi, h_roi),
                             borderMode=cv2.BORDER_REPLICATE)
    
    # 5. 缩放到固定尺寸
    final = cv2.resize(aligned, (224, 224), 
                       interpolation=cv2.INTER_AREA)
    
    return final

# 使用示例
processed = preprocess_pipeline(img, (100, 80, 400, 300), -15.0)
cv2.imshow('Final Preprocessed', processed)
性能优化:如果处理速度跟不上传送带速度,可以把 CLAHE 换成全局直方图均衡化,或者把高斯滤波核从 5x5 降到 3x3。每省 1 毫秒,都是真金白银。

好了,这一章的内容就到这里。预处理是图像处理的「地基」,地基不牢,后面再好的算法也白搭。下一章,咱们要聊边缘检测和轮廓提取——那是从「像素」到「特征」的关键一步。

有什么问题,欢迎在课程群里讨论。咱们下章见。