4、经典缺陷特征工程:纹理特征提取(LBP/GLCM)、形状特征描述(Hu矩/面积/周长)、颜色特征分析

好,咱们进入正题。缺陷检测里,特征工程是承上启下的关键一步。你想想看,图像预处理做完了,模型要学什么?总不能把几百万个像素直接扔给分类器吧?那太傻了。我们需要把图像里的信息,提炼成几个有意义的数字——这就是特征。

我个人习惯把特征分成三大类:纹理、形状、颜色。这三板斧用好了,大部分缺陷都能搞定。今天我就把压箱底的经验掏出来,跟你聊聊怎么用。

4.1 纹理特征提取:LBP与GLCM

纹理是什么?说白了就是图像局部的粗糙度、方向性、规律性。比如布料的编织纹路、金属表面的拉丝、木材的年轮。缺陷往往破坏了这种规律,所以纹理特征特别管用。

4.1.1 LBP(局部二值模式)

LBP是我个人最常用的纹理特征之一。它的核心思想很简单:比较中心像素和周围邻居的大小关系。

基本原理

  • 取一个3x3的窗口
  • 中心像素作为阈值
  • 周围8个像素跟它比,大于等于的标1,小于的标0
  • 得到一个8位的二进制数,转成十进制就是LBP值

嗯,这里要注意:原始的LBP对旋转很敏感。我在项目中遇到过,同一个纹理旋转了45度,LBP值就完全不一样了。所以后来我基本都用旋转不变LBP均匀模式LBP

我的经验: 均匀模式LBP(Uniform LBP)能大大降低特征维度。原始LBP有256种模式,均匀模式只有58种。我做过对比,在布匹缺陷检测中,均匀模式不仅速度快了4倍,准确率反而提升了2%。因为那些非均匀模式往往是噪声。
import cv2
import numpy as np
from skimage.feature import local_binary_pattern

# 读取灰度图
img = cv2.imread('fabric_defect.jpg', 0)

# 计算LBP特征
# radius=1, n_points=8, method='uniform'
radius = 1
n_points = 8 * radius
lbp = local_binary_pattern(img, n_points, radius, method='uniform')

# 统计直方图作为特征向量
hist, _ = np.histogram(lbp.ravel(), bins=np.arange(0, 59), range=(0, 58))
hist = hist.astype('float32')
hist /= (hist.sum() + 1e-6)  # 归一化

print(f'LBP特征向量维度: {len(hist)}')  # 输出: 59维

4.1.2 GLCM(灰度共生矩阵)

GLCM是另一种经典纹理特征。它统计的是图像中特定距离和方向上,两个像素灰度值同时出现的概率。

我曾经在金属表面划痕检测中吃过亏。一开始用LBP,划痕和背景的纹理差异不大,效果一般。后来换成GLCM,提取了对比度和相关性特征,划痕一下就凸显出来了。

常用的GLCM统计量

特征名 物理含义 缺陷场景举例
对比度(Contrast) 纹理的清晰度和沟纹深浅 划痕越深,对比度越大
相关性(Correlation) 纹理的方向一致性 拉丝纹理被破坏,相关性下降
能量(Energy) 纹理的均匀性和规则性 污渍区域能量值偏低
同质性(Homogeneity) 纹理局部变化的剧烈程度 气泡边缘同质性低
避坑指南: 我曾经把GLCM的窗口设得太大(64x64),结果把微小的针孔缺陷给平滑掉了。后来我总结了一个经验:窗口大小应该是缺陷尺寸的2-3倍。对于小缺陷,用9x9或11x11就够了。
from skimage.feature import graycomatrix, graycoprops

# 先量化灰度级,一般用16或32级
img_quant = (img / 16).astype(np.uint8)

# 计算GLCM:距离=1,方向=0°, 45°, 90°, 135°
glcm = graycomatrix(img_quant, distances=[1], 
                    angles=[0, np.pi/4, np.pi/2, 3*np.pi/4],
                    levels=16, symmetric=True, normed=True)

# 提取特征
contrast = graycoprops(glcm, 'contrast').ravel()
correlation = graycoprops(glcm, 'correlation').ravel()
energy = graycoprops(glcm, 'energy').ravel()
homogeneity = graycoprops(glcm, 'homogeneity').ravel()

# 拼接成特征向量
features = np.hstack([contrast, correlation, energy, homogeneity])
print(f'GLCM特征向量维度: {len(features)}')  # 输出: 16维

4.2 形状特征描述:Hu矩、面积、周长

形状特征主要用来描述缺陷的几何属性。比如这个缺陷是圆的还是长的?面积有多大?边界有多复杂?

4.2.1 面积与周长

这两个是最基础的特征,但千万别小看它们。我做过一个PCB焊点检测项目,虚焊和正常焊点的纹理、颜色几乎一样,唯一的区别就是面积——虚焊的焊点面积小了15%。

  • 面积:缺陷区域的像素总数。注意要乘以像素当量(mm/像素)才能得到真实物理面积。
  • 周长:缺陷边界的像素长度。对于不规则形状,可以用链码法精确计算。
  • 圆形度:4π × 面积 / 周长²。值越接近1,形状越圆。气泡的圆形度通常在0.8以上,划痕则在0.3以下。
实用技巧: 我习惯把面积和周长组合成「紧凑度」特征。紧凑度 = 周长² / 面积。这个值对形状的复杂程度非常敏感。同样面积的缺陷,边缘越毛糙,紧凑度越大。

4.2.2 Hu矩

Hu矩是形状特征的「老江湖」了。它有7个不变矩,对平移、旋转、缩放都不敏感。说白了,同一个缺陷不管怎么转、怎么放大缩小,Hu矩的值基本不变。

我记得有一次做瓶盖缺陷检测,瓶盖在传送带上的位置和角度每次都不同。用面积和周长根本没法统一阈值,但用了Hu矩之后,不同位置的同一类缺陷特征值非常稳定。

import cv2

# 假设binary是二值图像,缺陷区域为白色(255)
binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)[1]

# 计算轮廓
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

for cnt in contours:
    # 面积
    area = cv2.contourArea(cnt)
    
    # 周长
    perimeter = cv2.arcLength(cnt, True)
    
    # Hu矩
    moments = cv2.moments(cnt)
    hu_moments = cv2.HuMoments(moments)
    
    # 注意:Hu矩的值范围很大,要做对数变换
    hu_moments = -np.sign(hu_moments) * np.log10(np.abs(hu_moments) + 1e-10)
    
    print(f'面积: {area:.2f}, 周长: {perimeter:.2f}')
    print(f'Hu矩: {hu_moments.flatten()}')

4.3 颜色特征分析

颜色特征在缺陷检测中经常被忽视,但其实非常有用。尤其是对于色差、污渍、氧化、发黄这类缺陷,纹理和形状可能变化不大,但颜色信息一抓一个准。

4.3.1 颜色空间的选择

我建议不要直接用RGB。RGB的三个通道相关性太强,光照一变,三个值一起变,很不稳定。

我个人最常用的是HSVLab颜色空间:

  • HSV:H(色调)对光照不敏感,适合检测色相变化。比如塑料件发黄,H值会偏移。
  • Lab:L(亮度)和a、b(颜色对立通道)分离得更好。a通道对红绿色差敏感,b通道对黄蓝色差敏感。
我的经验: 在检测食品包装上的印刷缺陷时,我试过RGB、HSV、Lab三种空间。结果Lab空间的a通道对红色logo的缺失检测最灵敏,准确率从RGB的82%提升到了96%。所以别偷懒,多试试不同的颜色空间。

4.3.2 颜色特征提取方法

常用的颜色特征有三种:

  1. 颜色直方图:统计每个通道的像素分布。简单但有效。注意要归一化,消除面积影响。
  2. 颜色矩:用均值、标准差、偏度来描述颜色分布。计算量小,适合实时系统。
  3. 颜色连通域:提取特定颜色范围的区域,再结合形状特征。比如检测红色污渍,先提取H通道在0-10°和160-180°的区域。
import cv2
import numpy as np

# 转换到HSV空间
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

# 提取H通道直方图
hist_h = cv2.calcHist([hsv], [0], None, [180], [0, 180])
hist_h = cv2.normalize(hist_h, hist_h).flatten()

# 颜色矩:一阶矩(均值)、二阶矩(标准差)、三阶矩(偏度)
mean, stddev = cv2.meanStdDev(hsv)
mean = mean.flatten()
stddev = stddev.flatten()

# 三阶矩需要手动计算
skewness = []
for i in range(3):
    channel = hsv[:,:,i].astype(np.float32)
    skew = np.mean(((channel - mean[i]) / (stddev[i] + 1e-6)) ** 3)
    skewness.append(skew)

color_features = np.hstack([mean, stddev, skewness])
print(f'颜色特征向量维度: {len(color_features)}')  # 输出: 9维

4.4 特征融合与实战建议

好了,三种特征都讲完了。你可能会问:到底用哪种?

我的建议是:别单选,要融合。单一特征总有盲区。纹理特征对划痕敏感,但对色差不敏感;颜色特征对污渍敏感,但对形状不敏感。把它们组合起来,才能覆盖更多缺陷类型。

我常用的融合策略:

  • 串联融合:直接把LBP直方图、GLCM统计量、Hu矩、颜色矩拼成一个长向量。简单粗暴,适合传统机器学习(SVM、随机森林)。
  • 并行融合:每种特征单独训练一个分类器,最后投票决定。适合特征差异很大的场景。
  • 降维融合:先用PCA或LDA降维,再融合。我一般保留90%的方差信息,能有效避免维度灾难。
最后提醒一句: 特征不是越多越好。我曾经在一个项目里提取了200多维特征,结果过拟合得一塌糊涂。后来用特征选择算法(比如基于随机森林的特征重要性排序)砍到30维,效果反而更好。记住:少即是多。

嗯,特征工程这部分就聊到这儿。下一章我们聊聊怎么用这些特征训练分类器,以及如何评估模型效果。到时候我会分享一个我踩过的坑——特征归一化没做好,模型直接废了。敬请期待。