3. 图像处理基础(二):几何变换与滤波、边缘检测

好,咱们接着往下聊。上一章我们把像素操作和直方图这些基本功过了一遍,这一章要讲的东西,在工业缺陷检测里可以说是「天天见」。几何变换、滤波、边缘检测,这三板斧你要是玩熟了,很多缺陷一眼就能看出来。

3.1 图像几何变换:让图像动起来

几何变换说白了就是改变图像的形状和位置。在工业现场,相机装歪了、产品放偏了,都是家常便饭。这时候就得靠几何变换来「校正」一下。

3.1.1 缩放与平移

缩放最简单,cv2.resize() 搞定。但我得提醒你一句:千万别用默认的插值方式做缩小。我踩过这个坑——有一次做PCB板缺陷检测,图像缩小后边缘出现了锯齿,结果把正常的焊盘误判成了缺陷。后来我养成了习惯:缩小用 INTER_AREA,放大用 INTER_CUBICINTER_LINEAR

import cv2
import numpy as np

img = cv2.imread('pcb_defect.jpg')
# 缩小用 INTER_AREA,保留边缘信息
small = cv2.resize(img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_AREA)
# 平移:构建平移矩阵
M = np.float32([[1, 0, 50], [0, 1, 30]])  # 向右50像素,向下30像素
shifted = cv2.warpAffine(img, M, (img.shape[1], img.shape[0]))
我的习惯:做批量检测时,我会先把所有图像缩放到统一尺寸,再用平移把感兴趣区域(ROI)对齐到固定位置。这样后续处理省心很多。

3.1.2 旋转与仿射变换

旋转在工业里太常用了。产品在传送带上角度总会有偏差,你得先把它「摆正」再检测。OpenCV 提供了 cv2.getRotationMatrix2D() 来生成旋转矩阵。

# 旋转:绕图像中心旋转45度
h, w = img.shape[:2]
center = (w // 2, h // 2)
M_rot = cv2.getRotationMatrix2D(center, 45, 1.0)  # 角度,缩放因子
rotated = cv2.warpAffine(img, M_rot, (w, h))

仿射变换更灵活,它用三个点就能确定一个变换关系。我经常用它来做「透视校正」——比如拍到的产品是斜着的,用仿射变换把它拉正。

# 仿射变换:用三个点映射
pts1 = np.float32([[50, 50], [200, 50], [50, 200]])
pts2 = np.float32([[10, 100], [200, 50], [100, 250]])
M_affine = cv2.getAffineTransform(pts1, pts2)
affined = cv2.warpAffine(img, M_affine, (w, h))
注意:仿射变换后图像边缘会出现黑色区域。如果你要做后续的阈值分割,记得先裁剪掉这些黑边,否则会影响统计结果。

3.2 图像滤波:去噪与保边

滤波是缺陷检测的「预处理神器」。工业图像里噪声无处不在——光照不均、传感器热噪声、传送带震动……滤波的目的就是把这些干扰去掉,同时尽量保留缺陷本身的特征。

3.2.1 均值滤波 vs 高斯滤波

均值滤波简单粗暴,取邻域内像素的平均值。但它有个致命缺点:会把边缘也模糊掉。高斯滤波好一些,它给中心像素更高的权重,边缘保留得更好。

# 均值滤波
blur_avg = cv2.blur(img, (5, 5))
# 高斯滤波
blur_gauss = cv2.GaussianBlur(img, (5, 5), 1.5)  # 1.5是标准差

我个人习惯:能用高斯就别用均值。除非你明确知道噪声是均匀分布的,比如某些老式CCD相机产生的固定模式噪声。

3.2.2 中值滤波:椒盐噪声克星

中值滤波用邻域内像素的中位数代替中心像素。它对椒盐噪声(黑白点噪声)效果极好。我记得有一次做金属表面划痕检测,图像上全是传感器坏点产生的白点,用高斯滤波怎么调都去不干净,换成中值滤波,3x3的核,一下就干净了。

# 中值滤波
median = cv2.medianBlur(img, 5)  # 5是核大小,必须是奇数
实战经验:中值滤波的核大小一般取3或5就够了。核太大,图像细节会丢失,小缺陷就看不出来了。

3.2.3 双边滤波:保边去噪的利器

双边滤波是「既要又要」的典型——它既考虑空间距离,又考虑像素值差异。所以它能平滑平坦区域,同时保留边缘。在检测玻璃划痕、塑料表面缺陷时,我经常用它。

# 双边滤波
bilateral = cv2.bilateralFilter(img, 9, 75, 75)
# 参数:直径,颜色标准差,空间标准差

不过双边滤波有个缺点:。在实时检测场景下,如果帧率要求高,我一般不用它,而是用高斯滤波+后续的边缘增强来替代。

3.3 边缘检测:找到缺陷的边界

边缘检测是缺陷检测的核心。你想啊,划痕、裂纹、气泡、脏污,这些缺陷本质上都是图像中灰度发生剧烈变化的地方。找到这些变化,就找到了缺陷。

3.3.1 Sobel算子:一阶导数的魅力

Sobel算子计算图像在x和y方向上的梯度。说白了,就是看像素值变化得有多快。变化大的地方,就是边缘。

# Sobel边缘检测
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)  # x方向梯度
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)  # y方向梯度
# 计算梯度幅值
sobel_mag = np.sqrt(sobel_x**2 + sobel_y**2)
sobel_mag = np.uint8(np.clip(sobel_mag, 0, 255))
避坑指南:我曾经直接用 cv2.CV_8U 做 Sobel,结果负梯度被截断了,边缘信息丢失了一半。后来我学乖了:先用 cv2.CV_64F 计算,再转回8位。

3.3.2 Canny边缘检测:工业界的标准答案

Canny 是边缘检测的「扛把子」。它做了四件事:去噪、算梯度、非极大值抑制、双阈值检测。效果确实好,但参数调起来也让人头疼。

# Canny边缘检测
edges = cv2.Canny(img, 50, 150)  # 低阈值50,高阈值150

双阈值怎么调?我有个笨办法:先设高阈值,把最明显的边缘找出来;再设低阈值,把弱边缘补上。一般高阈值是低阈值的2-3倍。比如检测金属表面的细微划痕,我会用 Canny(img, 30, 90),因为划痕的对比度通常不高。

我的经验:在工业缺陷检测中,Canny 的阈值不是固定的。我会根据图像的对比度动态调整——先计算图像的均值和标准差,然后设置阈值为 均值 ± 标准差 * 系数。这样能适应不同光照条件。

3.4 实战组合拳:滤波+边缘检测

在实际项目中,很少单独用某一个操作。我通常的流程是:

  1. 先滤波去噪:根据噪声类型选择滤波器。椒盐噪声用中值,高斯噪声用高斯,既要保边又要去噪用双边。
  2. 再边缘检测:用 Canny 提取边缘,阈值根据图像动态调整。
  3. 后处理:用形态学操作(下一章会讲)连接断裂的边缘,去除小噪点。
# 一个典型的工业检测预处理流程
def preprocess_for_defect_detection(img):
    # 1. 中值滤波去噪
    denoised = cv2.medianBlur(img, 5)
    # 2. 高斯滤波平滑
    smoothed = cv2.GaussianBlur(denoised, (3, 3), 0)
    # 3. Canny边缘检测(动态阈值)
    mean_val = np.mean(smoothed)
    std_val = np.std(smoothed)
    low_thresh = max(0, int(mean_val - std_val * 0.5))
    high_thresh = min(255, int(mean_val + std_val * 1.5))
    edges = cv2.Canny(smoothed, low_thresh, high_thresh)
    return edges

嗯,这一章的内容就这些。几何变换让你能「摆正」图像,滤波让你「看清」图像,边缘检测让你「找到」缺陷。这三样东西,在后面的章节里会反复用到。下一章我们讲形态学操作,那是处理边缘和区域的利器,到时候见。