简介:这份资源是《基于图方法的单幅图像去模糊》配套源码包,面向计算机视觉与图像处理方向的研究者、研究生及开发者,聚焦盲图像去模糊这一经典难题——在缺乏模糊核形状与大小等先验信息时,仅凭单张模糊照片恢复清晰图像。项目以图理论建模,将像素视为节点、相邻像素关系视为边,借助图信号处理估计模糊过程并逆向反卷积,可应对图像不同区域受非均匀模糊影响的情况。压缩包共51个文件,约6.49MB,以15个p文件、14个m文件(MATLAB脚本与函数)为核心,辅以11个png、6个jpg、2个bmp测试图像及mat数据、readme与md说明,覆盖预处理、图构建、图信号平滑、反卷积与结果评估等环节。已有264人学习,适合借此理解算法实现细节、复现实验并探索图像恢复与增强的延伸应用。
1. 图盲去模糊到底在解决什么:从一张糊片说起
你手里有一张因为相机抖动或者物体运动而糊掉的图,想把它救回来。如果只知道模糊后的图,不知道模糊核长什么样、有多大、方向如何,这就是盲去模糊。而“Graph-Based”意味着用图结构来建模像素或特征之间的关系,把去模糊当成图上的信息传播问题。这个方向适合两类人:一是做图像复原的算法工程师,想找一个比传统卷积更灵活的结构;二是做手机摄影、监控、医学影像的开发者,需要在没有配对训练数据的情况下也能恢复细节。我见过太多人一上来就堆深度网络,结果在真实模糊上翻车,因为合成模糊和真实模糊的分布差得太远。图方法的优势在于,它不依赖规则网格,能把非局部相似块连起来,对纹理和边缘的恢复更稳。这一章不展开公式,只把问题边界划清楚:输入是一张模糊图,输出是清晰图,中间要估计模糊核或者直接端到端映射。后面几章会从图构建、核估计、训练策略到避坑,一步步拆开。
2. 图结构怎么建:从像素图到特征图的三种落地方式
2.1 为什么用图代替卷积:非局部连接的实际收益
卷积核在空间上是局部的,感受野靠堆层扩大,但模糊核往往把远处像素混在一起。图结构可以把任意两个相似块连边,信息一步传到位。常见做法是把图像切成 patch,每个 patch 当节点,特征相似度当边权。这样在去模糊时,一个清晰边缘可以从相似区域借来高频信息。我一般会先用 SLIC 超像素做粗分割,再在超像素上建图,节点数从百万降到几千,计算量可控。另一种是直接在特征图上建动态图,每层都重新算邻接矩阵,适合端到端训练。选哪种取决于你的算力:超像素图适合传统优化,动态图适合 GPU 训练。注意,图拉普拉斯矩阵的稀疏性很关键,全连接图会直接爆显存。
2.2 用 Python 构建图像图的最小代码
import numpy as np from sklearn.feature_extraction import image from scipy.sparse import csr_matrix def build_patch_graph(img, patch_size=8, stride=4, k=10): # img: 灰度图,shape (H, W) patches = image.extract_patches_2d(img, (patch_size, patch_size)) # 展平并归一化,避免亮度影响相似度 feats = patches.reshape(patches.shape[0], -1) feats = (feats - feats.mean(axis=1, keepdims=True)) / (feats.std(axis=1, keepdims=True) + 1e-8) # 计算余弦相似度 norm = np.linalg.norm(feats, axis=1, keepdims=True) feats_norm = feats / (norm + 1e-8) sim = feats_norm @ feats_norm.T # 每个节点只保留 top-k 邻居,构建稀疏邻接矩阵 n = sim.shape[0] rows, cols, data = [], [], [] for i in range(n): idx = np.argsort(sim[i])[-k-1:-1] # 去掉自身 for j in idx: rows.append(i) cols.append(j) data.append(sim[i, j]) adj = csr_matrix((data, (rows, cols)), shape=(n, n)) return adj这段代码把图像切成重叠 patch,每个 patch 当节点,用余弦相似度找 top-k 邻居。参数patch_size控制局部细节,太小会丢结构,太大会模糊边缘,我一般从 8 开始试。stride决定重叠程度,stride 越小节点越多,图越密,计算越慢。k是每个节点的邻居数,k=10 在多数去模糊任务里够用,太大反而引入噪声。返回的adj是稀疏矩阵,可以直接喂给图卷积或者谱聚类。注意,这里只用了灰度图,彩色图可以每个通道单独建图再融合,或者把 RGB 拼成特征向量。
2.3 图拉普拉斯在去模糊优化中的角色
建好图之后,图拉普拉斯矩阵 L = D - A 用来做正则化。在盲去模糊里,清晰图 x 的图平滑先验可以写成 x^T L x,意思是相似节点上的像素值应该接近。优化目标通常是 min ||y - k * x||^2 + λ x^T L x,其中 y 是模糊图,k 是模糊核,* 是卷积。λ 控制平滑强度,太大图会糊,太小噪声会放大。我一般用 L1 范数代替 L2,对异常值更鲁棒。求解时可以用交替方向乘子法,把 x 和 k 分开更新。图拉普拉斯是稀疏的,矩阵向量乘很快,但要注意归一化,否则节点度数差异大会导致数值不稳。常见做法是用对称归一化 L_sym = I - D^{-1/2} A D^{-1/2}。
3. 盲去模糊的核估计:图方法怎么绕开传统假设
3.1 模糊核的图表示与参数化
传统盲去模糊假设核是均匀的或者简单的运动模糊,但真实场景里核可能空间变化。图方法可以把核也建成图:每个位置的核当节点,核之间的相似度当边。这样空间变化的核可以通过图传播来估计。我一般会把图像分成若干区域,每个区域估一个核,然后用图平滑让相邻区域的核过渡自然。参数化方面,运动模糊核可以用线段长度和角度表示,散焦核用高斯半径表示。图上的节点特征就是这些参数,边权是区域间颜色或深度的相似度。这样做的好处是,即使某个区域纹理少、核估计不准,也能从相似区域借信息。
3.2 交替优化:固定核更新图,固定图更新核
def alternating_optimize(y, adj, init_k, lambda1=0.1, lambda2=0.05, iters=20): # y: 模糊图,adj: 图邻接矩阵,init_k: 初始核 from scipy.signal import convolve2d x = y.copy() k = init_k.copy() L = csr_matrix(adj.shape[0], adj.shape[0]) # 实际用归一化拉普拉斯 for it in range(iters): # 固定 k,更新 x:解线性系统 (K^T K + lambda1 * L) x = K^T y # 这里简化为梯度下降 for _ in range(5): grad_x = convolve2d(convolve2d(x, k, 'same'), k[::-1, ::-1], 'same') - convolve2d(y, k[::-1, ::-1], 'same') grad_x += lambda1 * L.dot(x.flatten()).reshape(x.shape) x -= 0.01 * grad_x # 固定 x,更新 k:在核空间做梯度下降 for _ in range(5): grad_k = convolve2d(convolve2d(x, k, 'same'), x[::-1, ::-1], 'same') - convolve2d(y, x[::-1, ::-1], 'same') k -= 0.01 * grad_k k = np.clip(k, 0, None) k /= k.sum() + 1e-8 return x, k这个交替优化框架里,lambda1控制图平滑强度,lambda2本来用于核的图正则,这里省略了。iters是交替轮数,一般 20 轮左右收敛。内层梯度下降步数可以调,步长 0.01 是经验值,太大发散,太慢收敛。注意每次更新 k 后要非负归一化,否则能量不守恒。实际跑的时候,图拉普拉斯 L 要用稀疏矩阵,否则内存扛不住。如果核估计不准,可以加一个核的图先验:相邻区域的核应该相似,用 L 作用在核参数上。
3.3 从粗到细:多尺度图金字塔的搭建
真实模糊核可能很大,直接在全分辨率上估计算量大且容易陷局部极小。常见做法是建图像金字塔,从低分辨率开始估核,再逐级上采样作为下一级初始值。图金字塔可以每层单独建图,也可以跨层连边。我一般会在每层用超像素建图,低分辨率节点少,图小,优化快;高分辨率节点多,但核已经接近真值,只需要微调。跨层连边可以让低层的结构信息传到高层,比如低层的一个超像素对应高层多个超像素,边权用面积重叠度。这样从粗到细,核估计的鲁棒性明显提升。注意每层上采样核时要插值,否则会引入块效应。
4. 训练与推理:图神经网络在去模糊中的实操配置
4.1 图卷积层的实现与参数选择
import torch import torch.nn as nn import torch.nn.functional as F class GraphConv(nn.Module): def __init__(self, in_features, out_features, bias=True): super().__init__() self.weight = nn.Parameter(torch.FloatTensor(in_features, out_features)) self.bias = nn.Parameter(torch.FloatTensor(out_features)) if bias else None self.reset_parameters() def reset_parameters(self): nn.init.xavier_uniform_(self.weight) if self.bias is not None: nn.init.zeros_(self.bias) def forward(self, x, adj): # x: (N, in_features), adj: 稀疏归一化邻接矩阵 (N, N) support = torch.mm(x, self.weight) out = torch.spmm(adj, support) if self.bias is not None: out = out + self.bias return out这个图卷积层用稀疏矩阵乘法实现,adj要是归一化的,否则节点度数差异会导致梯度爆炸。in_features和out_features根据你的特征维度定,去模糊任务里第一层可以设 64,后面 128 或 256。bias一般保留。初始化用 Xavier 均匀分布,适合图卷积。前向传播里torch.spmm是稀疏矩阵乘稠密矩阵,比稠密乘法省显存。注意,如果图是动态的,每层都要重新算adj,这时可以把adj作为 forward 的参数传进来。训练时用 Adam 优化器,学习率 1e-3 起步,权重衰减 1e-5。
4.2 损失函数:像素损失、感知损失与图平滑损失的组合
def total_loss(pred, target, adj, lambda_pix=1.0, lambda_per=0.1, lambda_graph=0.01): # 像素 L1 损失 loss_pix = F.l1_loss(pred, target) # 感知损失:用预训练 VGG 提取特征 vgg = torchvision.models.vgg16(pretrained=True).features[:16].eval() for p in vgg.parameters(): p.requires_grad = False feat_pred = vgg(pred.repeat(1, 3, 1, 1)) feat_target = vgg(target.repeat(1, 3, 1, 1)) loss_per = F.mse_loss(feat_pred, feat_target) # 图平滑损失:pred 在图上应该平滑 pred_flat = pred.view(pred.shape[0], -1) loss_graph = torch.mean(pred_flat @ adj @ pred_flat.t()) return lambda_pix * loss_pix + lambda_per * loss_per + lambda_graph * loss_graph像素 L1 损失保底,感知损失让纹理更自然,图平滑损失约束相似块一致。lambda_pix一般最大,1.0 左右;lambda_per0.1 量级,太大会让颜色偏移;lambda_graph0.01 量级,太大图会过度平滑。注意感知损失用的 VGG 要冻结参数,且输入要归一化到 ImageNet 分布。图平滑损失里adj是归一化邻接矩阵,pred_flat @ adj @ pred_flat.t()算的是所有节点对的加权平方差,取均值。如果显存不够,可以只对部分节点采样算。训练时先训像素损失,稳定后再加感知和图损失,否则容易震荡。
4.3 推理阶段:图构建与网络前向的时序
推理时,先对输入模糊图建图,可以用超像素或者固定网格。如果训练时用的是动态图,推理也要动态算,但可以固定 top-k 邻居加速。我一般会先把图构建好,存成稀疏矩阵,然后网络前向。注意推理时不需要梯度,用torch.no_grad()包起来。如果图很大,可以分块推理,块之间重叠几个像素避免边界效应。输出清晰图后,可以再做一次非局部均值或者导向滤波做后处理,但别过度,否则会引入伪影。整个流程在 GPU 上,512x512 的图大概几十毫秒,具体看图和网络深度。
5. 避坑与排查:图盲去模糊里最容易翻车的五个点
5.1 图构建太密导致显存爆炸
现象:一跑就 OOM,即使 batch size 设为 1。原因:patch 数量太多,top-k 邻居又大,邻接矩阵非零元过多。解决:增大 patch_size 或 stride,减少节点数;降低 k 到 5 左右;用稀疏矩阵而不是稠密矩阵;如果还不行,改用超像素建图,节点数降一个数量级。
5.2 核估计收敛到平凡解
现象:估计出的核是一个 delta 函数,去模糊后图没变化。原因:优化时核没有归一化或者没有非负约束,梯度把核推向零。解决:每次更新后k = np.clip(k, 0, None)再k /= k.sum();加核的图先验,让相邻区域核相似;从粗到细多尺度估计,低分辨率先给一个好初始值。
5.3 图平滑损失过大导致细节丢失
现象:去模糊后图很干净但纹理没了,像水彩画。原因:lambda_graph设得太大,图拉普拉斯正则过度平滑。解决:降低lambda_graph到 0.001 量级;改用 L1 范数而不是 L2;只在低分辨率层用图平滑,高分辨率层关掉;或者用自适应权重,边缘处权重小。
5.4 训练集和真实模糊分布不匹配
现象:合成模糊上 PSNR 很高,真实照片上全是伪影。原因:合成模糊用高斯核或者线性运动核,真实模糊有噪声、饱和、非线性响应。解决:在合成时加噪声、加 JPEG 压缩、模拟相机响应曲线;用域适应,在真实模糊上做无监督微调;或者直接用真实模糊数据集,但要注意配对问题。
5.5 图邻接矩阵归一化错误
现象:训练 loss 不下降或者 NaN。原因:邻接矩阵没有归一化,节点度数差异大,梯度爆炸。解决:用对称归一化D^{-1/2} A D^{-1/2},或者行归一化D^{-1} A;加一个小的对角线项A + I避免孤立节点;检查稀疏矩阵的索引是否越界。
6. 进阶技巧:用图注意力机制动态调整邻居权重
图注意力网络在去模糊里比固定权重的图卷积更灵活。核心是每个节点对邻居的权重不是预先算好的相似度,而是通过网络学习。实现上,先算节点特征之间的注意力系数,再 softmax 归一化,最后加权聚合。我一般会在图卷积层里加一个注意力头,头数 4 或 8,每个头独立算权重再拼接。这样在模糊核空间变化时,网络能自动关注更可靠的邻居。验证方法很简单:在合成模糊上对比固定图卷积和注意力图卷积的 PSNR,通常能涨 0.3 到 0.5 dB。但要注意,注意力会增大计算量,推理时间可能翻倍。如果部署在移动端,可以只在前几层用注意力,后面用固定图。另一个技巧是残差图连接,把输入特征和输出特征相加,避免过平滑。我自己的习惯是,每次改图结构,先在小数据集上跑 10 个 epoch,看 loss 曲线和验证集 PSNR,再决定要不要全量训练。图盲去模糊不是玄学,但图怎么建、权重怎么学,确实需要反复试。希望帮到你。
本文还有配套的精品资源,点击获取