简介:本资源为《一种融合小波变换与卷积神经网络的高相似度图像识别与分类算法》论文PDF,面向从事机器学习、深度学习与数据建模的图像识别研究者和工程实践者,聚焦高相似度图像难以区分的核心难题。文中提出以多尺度小波变换提取图像纹理特征,经小波分解得到子图能量特征并归一化,再用卷积层与池化层交替的CNN自动学习判别性特征,并通过迭代训练优化网络参数。实验选取鸡蛋与苹果两类数据集,完成散养与圈养鸡蛋识别及苹果产地判定,平均鉴别准确率超过90%,为农业、医学、安防等场景提供了可借鉴的思路与参数设置参考。资源包内含1个pdf文件,大小约793KB,便于在电脑或移动端直接阅读,也可作为课题复现与论文写作的参考资料。目前已有158人学习下载。
1. 小波变换与卷积神经网络融合:高相似度图像分类的落地思路
工业质检里同一批次的两颗螺丝、中药饮片里外形接近的两种切片、SAR 图像中轮廓几乎重合的目标,这类任务的共同点是类间差异只剩几个像素的纹理走向和边缘强度,直接把原图塞进 CNN,网络很容易过拟合到背景光照和拍摄角度上。小波变换的价值在于把图像拆成不同尺度的低频轮廓与高频细节,等于给分类器换上一副多分辨率眼镜;卷积神经网络负责在这些子带上学习真正有判别力的纹理模式。这套融合路线不需要换硬件,也不依赖超大标注集,适合已经跑通基础 CNN 分类、想在小差异数据上再抠几个百分点的人。
2. 融合小波变换与CNN的算法框架与融合粒度选型
2.1 高相似度图像的判别难点
高相似度不等于难样本少,而是类间距离被压缩到了很小的区间。举个具体的场景:两种中药材切片,颜色、大小、厚度几乎一致,区别只在于断面纹理的走向是放射状还是环状。这种差异在原始像素空间里表现为高频成分的相位和方向分布差异,而全局的均值、方差、直方图几乎完全相同,任何基于整体统计量的特征都会被稀释掉。
第二个难点是类内差异被放大。同一类样本因为拍摄距离、光源角度、切片位置的不同,低频分量波动很大,而低频能量在整幅图中占比通常超过 90%。CNN 的第一层卷积核如果全部用来拟合这些低频变化,留给高频细节的容量就非常有限,训练集准确率能冲到 99%,验证集上却在两个相似类之间反复横跳。
第三个难点是样本量。细分场景的标注成本极高,一个类别往往只有几十到几百张图。小波分解本身是固定的线性变换,不含可学习参数,用它在网络前端做一次多尺度展开,相当于免费扩充了输入的信息维度,比堆数据增强更直接。
2.2 二维离散小波变换的四个子带与物理含义
对一幅图像做一层二维离散小波变换,会得到四个子带:低频近似 LL,水平细节 LH,垂直细节 HL,对角细节 HH。LL 是原图降采样后的模糊版本,保留了整体亮度和轮廓;LH 捕捉水平方向的边缘,HL 捕捉垂直方向的边缘,HH 捕捉对角方向的高频纹理。
高相似度分类真正吃劲的地方在 LH、HL、HH 三个细节子带,因为类间差异往往就藏在这些方向性边缘的强弱对比里。把三个细节子带单独归一化后送进网络,比混合成一张图再送要有效得多,原因是方向信息被显式分离开了,卷积核不用再去猜边缘朝向。
多层分解可以继续做下去:对 LL 再分解一次,得到四个更粗尺度的子带。层数不是越多越好,3 层以后高频子带的尺寸已经很小,纹理细节基本退化成噪声。
| 分解层数 | LL 尺寸占比 | 细节子带信息量 | 适用场景 |
|---|---|---|---|
| 1 层 | 1/4 | 保留完整边缘方向 | 纹理细密、分辨率高 |
| 2 层 | 1/16 | 兼顾轮廓与纹理 | 多数高相似度分类任务的默认选择 |
| 3 层 | 1/64 | 只保留粗轮廓 | 目标尺度大、类别差异在形状 |
2.3 三种融合粒度:预处理级、特征级、网络内嵌
融合位置决定了整套流程的复杂度和可调空间,实际落地时大致分三档。
预处理级融合是把小波增强后的图像当成普通输入,后面的 CNN 结构和标准分类任务完全一致。优点是改动最小,现成的训练脚本直接复用;缺点是小波参数(小波基、层数、阈值)和网络参数是分开调的,两组超参互相不感知。
特征级融合是分别用 CNN 提取原图特征和小波子带特征,再在中间层拼接或相加。拼接后特征维度翻倍,需要注意归一化,否则两支特征的尺度不一致会拖慢收敛。
网络内嵌是把小波分解写成一个固定权重的卷积层,放在网络最前端,梯度可以继续往后传。这样做的好处是端到端训练,但小波核本身不参与更新,所以它更像一个结构先验而不是可学习模块。常见做法是只固定第一层,后面所有卷积层正常训练。
| 融合粒度 | 改动成本 | 端到端 | 超参调节难度 | 推荐起步 |
|---|---|---|---|---|
| 预处理级 | 低 | 否 | 低 | 样本量小于 500 |
| 特征级 | 中 | 是 | 中 | 样本量 500 到 5000 |
| 网络内嵌 | 中 | 是 | 低 | 想复现论文结构 |
提示:融合粒度不是越深越好。样本量不足时,特征级双分支的额外参数会先过拟合,反而比预处理级差。
2.4 融合框架的数据流与张量形状
以单通道灰度图、网络内嵌方案为例,前向过程的张量形状变化是这样的:输入 (B, 1, H, W),经过一层 Haar 小波分解得到 (B, 4, H/2, W/2),四个通道依次是 LL、LH、HL、HH。之后接常规卷积、批归一化、池化,最后全局平均池化得到 (B, C),全连接输出类别 logits。
# 张量形状推演,帮助确认每一步的维度是否符合预期 import torch x = torch.randn(8, 1, 128, 128) # B=8, 单通道, 128x128 print("input ", tuple(x.shape)) # 小波分解层后: (8, 4, 64, 64) # conv3x3+BN+ReLU 后: (8, 32, 64, 64) # maxpool2 后: (8, 32, 32, 32) # conv3x3+BN+ReLU 后: (8, 64, 32, 32) # adaptive_avg_pool 后: (8, 64, 1, 1) # flatten 后: (8, 64) # linear 后: (8, num_classes)这段推演的作用是在写模型之前把每一层的维度先算清楚,尤其是小波分解把空间尺寸减半这一步。如果输入是奇数尺寸,比如 127×127,Haar 卷积 stride=2 会向下取整丢一行一列,重构时尺寸对不上,训练不一定报错但特征会错位。稳妥做法是预处理阶段把图像统一 resize 到 2 的整数次幂边长。
3. 用小波变换做图像增强与去噪的Python实现
3.1 pywt 二维小波分解与重构的最小可运行代码
先建一个能独立跑起来的最小闭环:读图、灰度化、归一化、分解、增强、重构、保存。下面这段代码不依赖任何深度学习框架,可以在数据预处理阶段离线批量执行。
import cv2 import numpy as np import pywt def wavelet_enhance(img_gray, wavelet="db4", level=3, gain_low=1.15, gain_high=1.0, thr=0.08): """ 对单通道灰度图做小波增强 + 软阈值去噪 img_gray : float32, 像素值范围 0~1 wavelet : 小波基名称 level : 分解层数,高相似度图像常用 2~3 gain_low : 低频轮廓增益,>1 增强整体对比 gain_high: 高频细节增益,>1 强调纹理 thr : 高频软阈值,抑制噪声 """ # 多层分解,返回 [cA_n, (cH_n,cV_n,cD_n), ..., (cH_1,cV_1,cD_1)] coeffs = pywt.wavedec2(img_gray, wavelet=wavelet, level=level, mode="periodization") new_coeffs = [coeffs[0] * gain_low] # 低频整体增益 for cH, cV, cD in coeffs[1:]: # 每个方向的高频先软阈值去噪,再乘细节增益 cH = pywt.threshold(cH, thr, mode="soft") * gain_high cV = pywt.threshold(cV, thr, mode="soft") * gain_high cD = pywt.threshold(cD, thr, mode="soft") * gain_high new_coeffs.append((cH, cV, cD)) rec = pywt.waverec2(new_coeffs, wavelet=wavelet, mode="periodization") return np.clip(rec, 0.0, 1.0) img = cv2.imread("slice_001.png", cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, (256, 256)).astype(np.float32) / 255.0 out = wavelet_enhance(img) cv2.imwrite("slice_001_wt.png", (out * 255).astype(np.uint8))代码逻辑分四步:wavedec2做多层分解,返回的列表第一个元素是最高层低频,后续每个元素是该层的三个方向细节元组;低频整体乘增益用来拉对比度;高频先软阈值再乘增益,软阈值把绝对值小于 thr 的系数压缩到零,避免硬阈值带来的振铃;waverec2用修改后的系数重构,mode必须与分解时保持一致,否则尺寸会差一到两像素。
3.2 小波基、分解层数与阈值规则的参数选择
小波基的选择直接影响细节子带的稀疏性。高相似度图像的核心差异在纹理,基函数的支撑长度和消失矩要同时考虑:支撑太长会在边缘处产生拖尾,消失矩太低则无法把平滑区域的高频系数压到接近零。
| 小波基 | 支撑长度 | 消失矩 | 典型适用 |
|---|---|---|---|
| haar | 2 | 1 | 边缘锐利、块状纹理,速度最快 |
| db2 | 4 | 2 | 一般纹理图像,性能与速度平衡 |
| db4 | 8 | 4 | 纹理细密、噪声较多的图像 |
| sym4 | 8 | 4 | 近似对称,重构边缘偏移小 |
| bior3.5 | 12 | 3/5 | 需要严格线性相位时使用 |
分解层数的经验规则是level = floor(log2(min(H, W))) - 3,再往上截断到 3。256×256 的图,这个公式给出 5,但实际取 3 就够,再拆下去细节子带只剩 32×32,纹理信息量不足以支撑分类。
阈值规则有四种常见变体:universal 阈值thr = sigma * sqrt(2 * log(N)),sigma 用最高频子带的 MAD 估计;SURE 阈值基于 Stein 无偏风险估计,适合信噪比高的图;minimax 阈值偏保守;BayesShrink 按子带自适应。工程上先用 universal 阈值起步,再按验证集表现微调。
import numpy as np import pywt def universal_threshold(detail_coeff): """按最高频子带的 MAD 估计噪声水平,返回 universal 阈值""" sigma = np.median(np.abs(detail_coeff)) / 0.6745 # MAD 到标准差的换算 n = detail_coeff.size return sigma * np.sqrt(2 * np.log(max(n, 2))) cA, (cH, cV, cD) = pywt.dwt2(img, "db4") # 单层分解 thr = universal_threshold(cD) # 噪声主要落在对角子带 print("threshold =", round(float(thr), 4))这里的 0.6745 是正态分布中位绝对偏差与标准差的换算常数,漏掉它会导致阈值偏大,把真实纹理也一起削掉。np.log里的样本数取子带元素总数,图像越大阈值提升越慢,这是 universal 阈值的对数性质决定的。
3.3 低频增益与高频软阈值的联合调节
gain_low 和 thr 是一对相互牵制的参数。低频增益提高会整体抬亮图像,让后续 CNN 的输入分布发生偏移;高频阈值提高会削掉噪声,但同时削弱类间差异所在的细节。两者需要一起看。
实操顺序是先固定 gain_low=1.0、gain_high=1.0,只调阈值,找到噪声恰好被压住、纹理还清晰的那个点;再固定阈值,把 gain_low 从 1.0 往上加,每次加 0.05,观察增强后图像的直方图是否出现截断。截断说明增益过大,clip 操作会把超过 1.0 的像素全部压平,反而丢掉最亮区域的纹理。
注意:增强参数一旦确定,训练集、验证集、测试集必须使用完全相同的参数。逐张图自适应调参会让数据分布不可控,验证集指标会虚高。
3.4 批量预处理流水线与常见踩坑
批量执行时最容易出问题的是尺寸。小波重构要求所有子带尺寸一致,图像边长不是 2 的幂时,wavedec2会在边界做延拓,waverec2还原出的尺寸可能比原图多一行一列。
import os import cv2 import numpy as np SRC, DST, SIZE = "raw", "wt_enhanced", 256 os.makedirs(DST, exist_ok=True) for name in sorted(os.listdir(SRC)): path = os.path.join(SRC, name) img = cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: continue # 先 resize 到 2 的幂,再归一化,避免重构尺寸不一致 img = cv2.resize(img, (SIZE, SIZE), interpolation=cv2.INTER_AREA) img = img.astype(np.float32) / 255.0 out = wavelet_enhance(img, wavelet="db4", level=3, gain_low=1.15, gain_high=1.0, thr=0.08) out = cv2.resize(out, (SIZE, SIZE)) # 兜底,保证输出尺寸严格一致 cv2.imwrite(os.path.join(DST, name), (out * 255).astype(np.uint8))第一个坑是颜色通道。pywt.wavedec2只处理二维数组,彩色图要先转灰度或者对每个通道分别分解再合并,不能直接传三通道数组,否则会把通道维当成空间维处理。
第二个坑是归一化时机。先归一化到 0~1 再分解,阈值参数的物理含义才稳定;如果用 0~255 的原始像素值分解,阈值也要按比例放大 255 倍,否则阈值形同虚设。
第三个坑是增强后的分布偏移。小波增强会改变图像的整体亮度均值,如果直接喂给在 ImageNet 上预训练过的模型,输入分布对不上,需要重新计算增强后数据集的均值和标准差,写进Normalize层。
4. 小波子带喂给CNN:网络搭建与高相似度分类训练
4.1 主干选型:从 LeNet-5 到双分支小波CNN
LeNet-5 是最早用于手写数字分类的经典结构,两个卷积层加三个全连接层,参数量不到 6 万。放到高相似度纹理分类上,它的感受野偏小、通道数太少,细节子带的方向信息会被压平。但它的层数少、训练快,适合在正式建模前先用它跑一遍数据,确认数据本身可分。
真正用于生产的主干建议走两条路。第一条是把小波分解层放最前面,后面接一个 4 到 6 层的 VGG 风格卷积堆,通道数从 32 起步,每两次卷积做一次池化。第二条是双分支:一支吃原图,一支吃小波细节子带,两支各自提取特征后在通道维拼接,再进分类头。第一条路参数少、推理快,第二条路在小差异任务上通常能多出一到三个点的准确率。
主干深度不要往 ResNet-50 那个量级堆。高相似度任务的训练集往往只有几千张图,深网络的 BatchNorm 统计量不稳定,训到最后几层梯度反而变成噪声。
4.2 PyTorch 实现可微的小波分解层
把小波分解写成网络的第一层,关键是用固定权重的卷积核实现 Haar 变换。这样一来它继承了卷积的接口,能直接插进nn.Sequential,梯度也能正常往前传。
import torch import torch.nn as nn import torch.nn.functional as F class HaarDWT(nn.Module): """固定权重的 Haar 小波分解层,输出 4 个子带通道""" def __init__(self): super().__init__() ll = torch.tensor([[1., 1.], [1., 1.]]) / 2.0 # 低频近似 lh = torch.tensor([[1., 1.], [-1., -1.]]) / 2.0 # 水平细节 hl = torch.tensor([[1., -1.], [1., -1.]]) / 2.0 # 垂直细节 hh = torch.tensor([[1., -1.], [-1., 1.]]) / 2.0 # 对角细节 weight = torch.stack([ll, lh, hl, hh]).unsqueeze(1) # (4,1,2,2) self.register_buffer("weight", weight) # buffer 参与前向但不更新 def forward(self, x): B, C, H, W = x.shape x = x.contiguous().view(B * C, 1, H, W) out = F.conv2d(x, self.weight, stride=2) # (B*C, 4, H/2, W/2) out = out.view(B, C * 4, H // 2, W // 2) return out class WaveletCNN(nn.Module): def __init__(self, num_classes, in_ch=1, width=32): super().__init__() self.dwt = HaarDWT() c = in_ch * 4 # 分解后通道翻四倍 self.features = nn.Sequential( nn.Conv2d(c, width, 3, padding=1), nn.BatchNorm2d(width), nn.ReLU(inplace=True), nn.Conv2d(width, width, 3, padding=1), nn.BatchNorm2d(width), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(width, width * 2, 3, padding=1), nn.BatchNorm2d(width * 2), nn.ReLU(inplace=True), nn.Conv2d(width * 2, width * 2, 3, padding=1), nn.BatchNorm2d(width * 2), nn.ReLU(inplace=True), nn.AdaptiveAvgPool2d(1), ) self.classifier = nn.Sequential( nn.Dropout(0.3), nn.Linear(width * 2, num_classes), ) def forward(self, x): x = self.dwt(x) x = self.features(x) return self.classifier(x.flatten(1)) model = WaveletCNN(num_classes=8) print(sum(p.numel() for p in model.parameters() if p.requires_grad))register_buffer让权重跟着模型一起搬到 GPU,但不会出现在优化器的参数列表里,这是固定变换层该有的行为。view(B*C, 1, H, W)把批和通道合并成一个维度,是为了让所有通道共用同一组小波核,计算完再还原成四通道堆叠的输出。如果输入是奇数尺寸,H // 2会丢掉最后一行,所以前面强调过要 resize 到偶数边长。
4.3 训练超参数与类别不平衡的处理
高相似度场景的类别通常不均衡,某些类别样本只有几十张。损失函数用带类别权重的交叉熵,权重按类别频次的倒数归一化,比单纯上采样更稳,因为上采样会让少数类反复出现,网络记住的是那几张具体图片而不是类别特征。
| 超参数 | 推荐值 | 说明 |
|---|---|---|
| 优化器 | AdamW | 权重衰减 1e-4,比 SGD 对学习率更宽容 |
| 初始学习率 | 1e-3 | 配合余弦退火,最小到 1e-5 |
| 批大小 | 32 | 样本量小于 1000 时降到 16 |
| 训练轮数 | 80~120 | 早停耐心值设 15 |
| Dropout | 0.3 | 全连接层前,卷积层不加 |
| 标签平滑 | 0.1 | 抑制过度自信,改善相似类混淆 |
数据划分要按采集批次分层,不能随机打乱。同一个样品的多个拍摄角度如果被分到训练集和验证集两边,验证指标会明显偏高,这种泄漏在高相似度任务里特别隐蔽,因为同一批次的图几乎一模一样。
4.4 用混淆矩阵和分类报告看分类效果
准确率在高相似度任务上几乎没有参考价值。八个类里有两个类互相混淆,准确率可能还有 87%,但这两个类恰恰是业务上最需要分清的。
import numpy as np from sklearn.metrics import classification_report, confusion_matrix def evaluate(model, loader, device, class_names): model.eval() preds, labels = [], [] with torch.no_grad(): for x, y in loader: logits = model(x.to(device)) preds.extend(logits.argmax(1).cpu().numpy()) labels.extend(y.numpy()) preds, labels = np.array(preds), np.array(labels) cm = confusion_matrix(labels, preds, normalize="true") # 按行归一化 print(classification_report(labels, preds, target_names=class_names, digits=4)) # 只打印非对角线元素最大的三对,定位最严重的混淆 off = cm.copy() np.fill_diagonal(off, 0) for idx in np.dstack(np.unravel_index(np.argsort(off.ravel())[::-1][:3], off.shape))[0]: i, j = int(idx[0]), int(idx[1]) print(f"混淆 {class_names[i]} -> {class_names[j]}: {off[i, j]:.3f}")按行归一化的混淆矩阵,第 i 行第 j 列表示真实类别 i 被预测成 j 的比例,对角线就是召回率。classification_report里的 macro F1 比 weighted F1 更能反映少数类的表现,前者对所有类别等权,后者按样本数加权,样本多的类别会把分数拉高。定位到具体的混淆对之后,再回头看这两个类的细节子带图像,通常能发现某个方向的高频能量分布差异没被网络学到,这时候可以针对性地增加该方向的通道数,或者在损失里加一个类间距离项。
5. 高相似度分类的进阶技巧与效果验证方法
5.1 多尺度小波与通道注意力的组合
单层 Haar 分解只给出了一个尺度的细节,对差异跨越多个尺度的类别不够用。把两层分解拼起来,第一层的 LL 再分解一次,总共得到 1 个二层低频加 6 个细节子带,在通道维拼接后输入一个轻量的通道注意力模块,让网络自己决定每个子带的权重。
class SEBlock(nn.Module): """通道注意力:对每个子带通道学习一个缩放系数""" def __init__(self, channels, reduction=8): super().__init__() self.fc = nn.Sequential( nn.Linear(channels, max(channels // reduction, 4)), nn.ReLU(inplace=True), nn.Linear(max(channels // reduction, 4), channels), nn.Sigmoid(), ) def forward(self, x): w = x.mean(dim=(2, 3)) # 全局平均池化到 (B, C) return x * self.fc(w).unsqueeze(-1).unsqueeze(-1)reduction=8是常见的压缩比,通道数小于 32 时用 4 更合适,压缩太狠会让注意力权重趋同,失去区分作用。这个模块插在小波分解层之后、第一个卷积层之前,参数量只有几百,几乎不增加推理时间。
5.2 用混淆矩阵与特征可视化验证判别力
训练完之后看准确率只能说明整体可分,要确认模型真的学到了细节而不是记住了背景,可以做两件事。
第一件是遮挡测试。在测试图上随机遮掉一块区域再预测,如果遮住图像中心区域后预测结果不变,说明模型依赖的是边缘或背景,而不是目标本身的纹理。高相似度任务的模型应该对目标区域敏感。
第二件是把倒数第二层的特征做 t-SNE 降到二维,按类别着色。特征在二维平面上的簇间间距如果小于簇内半径,说明网络还没把相似类分开,这时候继续加训练轮数没有意义,要回到数据层面看这两个类的增强参数是不是把它们拉到了同一个分布上。
验证的最后一步是跨批次测试。用 A 批次数据训练,拿 B 批次数据测试,性能下降如果超过 15 个百分点,问题多半出在小波增强参数对光照变化的鲁棒性上,可以固定 gain_low,只让高频阈值随图像整体方差自适应调整。
本文还有配套的精品资源,点击获取