3. 数据清洗与预处理:图像去噪、对比度增强、尺寸归一化、数据平衡策略

数据预处理这件事,我做了快十年了。说实话,很多刚入行的同学总觉得模型结构才是王道,预处理随便搞搞就行。但我在实际项目中踩过的坑告诉我——数据预处理的质量,直接决定了你模型的天花板。

尤其是缺陷检测这种场景。你想想看,一张图片里可能只有几个像素的划痕,如果噪声没去干净,或者对比度没拉起来,模型根本学不到东西。今天我们就把这四个环节掰开揉碎了讲清楚。

核心观点:预处理不是“可有可无的体力活”,而是模型性能的“放大器”。同样的模型,预处理做与不做,mAP 可能差 10 个点以上。

3.1 图像去噪:别让噪声干扰模型判断

工业场景下,图像噪声来源太多了。传感器热噪声、光照不均、传输丢包……我见过最夸张的一次,客户给的 PCB 板图像,信噪比不到 20dB,肉眼都快看不清缺陷了。

常用的去噪方法,我按实用程度排个序:

方法 适用场景 我的建议
高斯滤波 高斯噪声,轻度噪声 速度快,但容易模糊边缘,慎用
中值滤波 椒盐噪声,孤立噪点 缺陷检测首选,保边效果好
双边滤波 需要保留细节的场景 计算量大,但效果确实好
非局部均值去噪 纹理丰富的图像 效果好但慢,适合离线预处理

我个人习惯,在缺陷检测项目里,中值滤波是默认选项。为什么?因为缺陷往往是边缘信息,中值滤波在去噪的同时,对边缘的保留能力比高斯滤波强太多了。

import cv2
import numpy as np

# 中值滤波去噪
def denoise_image(img, kernel_size=3):
    """
    img: 输入图像 (H, W, C)
    kernel_size: 核大小,奇数
    """
    # 中值滤波
    denoised = cv2.medianBlur(img, kernel_size)
    return denoised

# 实际项目中我常用 3x3 或 5x5
# 核太大容易把细小缺陷也抹掉
img_clean = denoise_image(img_noisy, kernel_size=3)
我曾经踩过的坑:有一次做手机屏幕划痕检测,用了 7x7 的中值滤波,结果划痕太细直接被抹平了。后来改成 3x3,效果立竿见影。记住:去噪的强度要和缺陷尺度匹配。

3.2 对比度增强:让缺陷“现出原形”

很多缺陷在原始图像里是“隐身”的。比如透明薄膜上的气泡、金属表面的细微划痕,对比度极低。这时候不做增强,模型根本学不到特征。

我常用的几种方法:

  • 直方图均衡化:简单粗暴,全局拉伸对比度。适合光照均匀的场景。
  • 自适应直方图均衡化(CLAHE):分块做均衡化,避免局部过曝。我项目里用得最多。
  • Gamma 校正:手动调节亮度曲线,适合特定材质。
  • 拉普拉斯增强:强化边缘,适合纹理型缺陷。
def enhance_contrast(img, method='clahe'):
    """
    对比度增强
    """
    if method == 'clahe':
        # 我最常用的 CLAHE
        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
        # 如果是彩色图,转 LAB 空间只增强 L 通道
        if len(img.shape) == 3:
            lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
            lab[:,:,0] = clahe.apply(lab[:,:,0])
            enhanced = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
        else:
            enhanced = clahe.apply(img)
        return enhanced
    elif method == 'gamma':
        # Gamma 校正
        gamma = 1.5
        inv_gamma = 1.0 / gamma
        table = np.array([(i/255.0)**inv_gamma * 255 
                          for i in range(256)]).astype('uint8')
        return cv2.LUT(img, table)
小技巧:做 CLAHE 时,clipLimit 参数我一般从 2.0 开始调。太大容易放大噪声,太小效果不明显。tileGridSize 用 8x8 基本够用,除非图像特别大。

3.3 尺寸归一化:统一输入,减少变量

模型训练时,输入尺寸必须固定。但实际采集的图像,分辨率可能千差万别。有的相机 500 万像素,有的 2000 万。不归一化,模型根本没法训练。

尺寸归一化有两个关键点:

  1. 保持宽高比:直接 resize 会拉伸变形,缺陷形状都变了。我一般用 letterbox 填充。
  2. 插值方法选择:缩小用 INTER_AREA,放大用 INTER_CUBIC 或 INTER_LINEAR。
def resize_with_padding(img, target_size=(640, 640)):
    """
    Letterbox 填充,保持宽高比
    """
    h, w = img.shape[:2]
    target_h, target_w = target_size
    
    # 计算缩放比例
    scale = min(target_w / w, target_h / h)
    new_w = int(w * scale)
    new_h = int(h * scale)
    
    # resize
    resized = cv2.resize(img, (new_w, new_h), 
                         interpolation=cv2.INTER_LINEAR)
    
    # 填充到目标尺寸
    delta_w = target_w - new_w
    delta_h = target_h - new_h
    top, bottom = delta_h // 2, delta_h - delta_h // 2
    left, right = delta_w // 2, delta_w - delta_w // 2
    
    padded = cv2.copyMakeBorder(resized, top, bottom, left, right,
                                cv2.BORDER_CONSTANT, value=(114, 114, 114))
    return padded

嗯,这里要注意:填充的颜色值,我习惯用 114(灰度中值),而不是 0 或 255。因为纯黑或纯白填充,会在图像边缘产生强烈的对比度跳变,模型可能会学到这种伪特征。

3.4 数据平衡策略:别让模型变成“偏科生”

缺陷检测里最头疼的问题——类别不平衡。正常样本可能占 99%,缺陷样本只有 1%。模型学到最后,干脆全部预测为正常,准确率 99% 但毫无卵用。

我常用的平衡策略,按效果排序:

策略 原理 适用场景
过采样(Oversampling) 复制少数类样本 缺陷样本太少时
欠采样(Undersampling) 随机丢弃多数类样本 正常样本太多时
SMOTE 插值生成新样本 特征空间连续时
类别权重 损失函数加权 不想动数据时
Focal Loss 聚焦难分类样本 目标检测场景

我个人习惯的组合拳:

  • 先做 过采样,把缺陷样本复制到正常样本的 10%~20%
  • 再用 类别权重,给缺陷类更高的 loss 权重
  • 如果还是不平衡,上 Focal Loss
from sklearn.utils import class_weight
import numpy as np

# 计算类别权重
def compute_class_weights(labels):
    """
    labels: 所有样本的标签列表
    """
    classes = np.unique(labels)
    weights = class_weight.compute_class_weight(
        'balanced',
        classes=classes,
        y=labels
    )
    return dict(zip(classes, weights))

# 举个例子
labels = [0]*900 + [1]*100  # 9:1 不平衡
weights = compute_class_weight(labels)
print(weights)  # 输出类似 {0: 0.5, 1: 4.5}
注意:过采样不要无脑复制。我见过有人把 100 张缺陷图复制到 900 张,结果模型过拟合严重,把复制出来的噪声也学进去了。建议配合数据增强一起用,每复制一次就做一次随机变换。

3.5 本章知识体系总览

下面这张图,是我自己总结的预处理流程。每次做新项目,我都会先对着这张图走一遍:

数据清洗与预处理流程总览 数据清洗与预处理流程总览 原始图像数据 第一步:图像去噪 中值滤波 / 高斯滤波 / 双边滤波 第二步:对比度增强 CLAHE / Gamma校正 / 直方图均衡化 第三步:尺寸归一化 Letterbox填充 / 保持宽高比 / 插值选择 第四步:数据平衡 过采样 / 类别权重 / Focal Loss 预处理完成数据 💡 核心原则 • 去噪强度匹配缺陷尺度 • 增强时避免放大噪声 • 归一化保持宽高比 • 平衡策略组合使用

你看,整个流程是串行的。每一步的输出都是下一步的输入。我建议你在项目里也按这个顺序来,不要跳步。曾经有个同事跳过了去噪直接做增强,结果噪声被放大了好几倍,模型训练直接崩了。

我的经验:预处理流程写成一个 pipeline 脚本,每次跑数据自动执行。不要手动一张张调参,效率太低。用 OpenCV 的 pipeline 或者自己写个类,把四个步骤串起来,参数用配置文件管理。