3. 数据清洗与预处理
数据预处理这件事,我做了快十年了。说实话,很多刚入行的同学总觉得模型结构才是王道,预处理随便搞搞就行。但我在实际项目中踩过的坑告诉我——数据预处理的质量,直接决定了你模型的天花板。
尤其是缺陷检测这种场景。你想想看,一张图片里可能只有几个像素的划痕,如果噪声没去干净,或者对比度没拉起来,模型根本学不到东西。今天我们就把这四个环节掰开揉碎了讲清楚。
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)
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)
3.3 尺寸归一化:统一输入,减少变量
模型训练时,输入尺寸必须固定。但实际采集的图像,分辨率可能千差万别。有的相机 500 万像素,有的 2000 万。不归一化,模型根本没法训练。
尺寸归一化有两个关键点:
- 保持宽高比:直接 resize 会拉伸变形,缺陷形状都变了。我一般用 letterbox 填充。
- 插值方法选择:缩小用 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}
3.5 本章知识体系总览
下面这张图,是我自己总结的预处理流程。每次做新项目,我都会先对着这张图走一遍:
你看,整个流程是串行的。每一步的输出都是下一步的输入。我建议你在项目里也按这个顺序来,不要跳步。曾经有个同事跳过了去噪直接做增强,结果噪声被放大了好几倍,模型训练直接崩了。