做图像恢复这几年,我越发觉得“模糊”这个问题一点也不简单。成像设备的光学系统、拍摄时的相对运动、传感器自身的热噪声,都会让一张图像退化得面目全非。很多时候我们不但拿不到清晰的原图,连点扩散函数(PSF)也是未知的,这时候传统反卷积算法等于“无米之炊”。盲反卷积算法就是来解决这种“米也没着落”的场景:它在不知道PSF的前提下,只需要一张模糊图像,就能同时估计模糊核和清晰图像。最近我在MATLAB R2018A环境下整理并实现了一套改进的盲反卷积算法,重点解决了原始方法对噪声敏感、振铃明显、PSF初始化刁钻这三大痛点。整个过程踩了不少坑,也沉淀了不少可直接参考的代码和调参经验,写成这篇文章分享给正在做图像恢复、盲去模糊相关课题的同学。
2. 改进盲反卷积算法的整体设计思路
2.1 退化模型与盲反卷积的求解难点
要理解改进算法,得先回到退化模型本身。在一张灰度图像中,退化过程通常建模为:
y = h ⊗ x + n
y是观测到的模糊图像,x是未知的清晰图像,h是点扩散函数,也就是模糊核,n是传感器噪声。非盲反卷积解决的是“h已知”的情况,直接用维纳滤波、Lucy-Richardson等方法就能恢复。但现实里h往往是未知的,比如镜头离焦程度不明、运动模糊方向和长度不确定,这时候就需要盲反卷积。
盲反卷积难,核心难在“病态性”。给定一张模糊图像y,理论上可以找到无穷多对(x, h)都满足上面的方程:只要把x模糊得更多、让h变得更尖锐,依然能解释同一个y。这种多解性让算法很容易收敛到一些看起来合理、实际完全错误的解。为了从众多解里选出真正符合物理规律的那一个,必须在目标函数中加入先验约束。常见的先验包括:清晰图像具有分段平滑的梯度稀疏性,PSF具有非负性、能量和为1、支撑域有限等。这些东西拼在一起,就是把盲反卷积从“碰运气”拉回到“有约束地找解”。
如果打一个生活化的比方,模糊图像就像是一杯混浊的糖水。非盲反卷积是“知道加了多少糖,再把糖滤出来”;盲反卷积是“只知道水是甜的,还要同时猜出糖量和杂质”。病态性就是“没加糖但加了很多甜味剂也能让水变甜”这种情况,先验约束相当于告诉算法,糖一定来自糖块而不是别的东西。
1.2 经典算法存在的三大问题
我最早用MATLAB图像处理工具箱自带的deconvblind,也单独调用过deconvlucy。这几个函数在PSF已知或PSF已经非常接近真实值的情况下效果还行,但一旦进入真正的盲场景,问题就暴露出来了。
第一是噪声放大问题。deconvlucy本质上是一种最大似然迭代,在低光照、高ISO这类低信噪比场景下,迭代次数稍微多一点,恢复图像就会出现大量颗粒状噪声。第二是振铃伪影。当PSF尺寸估计偏大,或者初始PSF方向不一致时,恢复结果会出现一圈圈的环状条纹,尤其在图像边缘附近特别明显。第三是初始值敏感。deconvblind交替估计图像和PSF,如果初始PSF给得离谱,迭代很容易陷入局部最优,最终输出一张“看着像锐化过但依然很糊”的图像。
这些问题不是算法不能用,而是缺乏有效的约束。实验室里的干净合成图还好说,真实场景里的模糊图像很少是单一退化类型,噪声和模糊叠加在一起,必须针对性地加正则项和改进迭代结构。
1.3 改进方案:TV正则项加支撑域约束的交替优化
我的改进思路很简单,就是把原来的“最大似然型盲反卷积”改造成“正则化交替最小化”框架。目标函数写成:
J(x, h) = ||y - h⊗x||² + λ_tv·TV(x) + λ_h·||h||²
第一项是数据保真项,保证恢复结果和观测值一致;第二项是对图像的全变差正则项,用来保持边缘的同时抑制噪声;第三项是对PSF的L2正则项,防止PSF在迭代中变得过激。整个优化过程拆成两个子问题交替求解:
固定h,更新图像x,这时问题变成带TV正则的非盲反卷积,可以用梯度下降法或近端算子迭代求解;固定x,更新PSF h,这个问题在频域里有解析解,再加上非负约束、支撑域约束和能量归一化。
这种交替结构的好处很直接:每次只估计一个未知量,子问题要么是凸优化,要么有闭式解,稳定性和收敛性比直接同时优化强得多。而且TV正则项能明显抑制振铃和噪声放大,PSF支撑域约束能让模糊核估计更贴近物理实际。方案思路先定在这里,下面讲具体的MATLAB实现和参数怎么配。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. MATLAB R2018A环境与核心实现准备
2.1 环境规划与工具箱依赖
这次算法运行环境是MATLAB R2018A。R2018A属于比较稳的一个版本,图像处理工具箱里反卷积相关函数已经非常成熟,deconvblind、deconvlucy、deconvreg、edgetaper都可用,ssim、psnr这些评价指标也有现成函数。需要注意,做反卷积时图像一律转成double类型,不要用uint8直接算,否则卷积累加时溢出会非常头疼。
建议在脚本开头加三行:
matlab复制clc; clear; close all;
rng(42); % 固定随机种子,保证实验结果可复现
我习惯把模糊图像统一标准化到[0, 1]区间:
matlab复制I_blur = im2double(imread('blurred.png'));
if size(I_blur,3) == 3
I_blur = rgb2gray(I_blur);
end
如果工具箱加载有问题,可以用ver('images')命令确认图像处理工具箱版本。R2018A没有深度学习工具箱也能跑通整个工程,这也是我喜欢用传统迭代方法的原因——对硬件和工具箱依赖都小。
2.2 模拟退化图像的生成
验证算法前,先要造出可控的测试退化图像。对运动模糊,用fspecial生成motion核;对离焦模糊,用disk核;高斯模糊则直接用gaussian核。给一个我自己常用的生成代码:
matlab复制% 生成运动模糊核
psf_true = fspecial('motion', 15, 30); % 15像素长度,30度方向
% 生成高斯模糊核
psf_true = fspecial('gaussian', [21 21], 3);
% 卷积加噪
I_clear = im2double(imread('cameraman.tif'));
I_blur = imfilter(I_clear, psf_true, 'circular', 'conv');
I_blur = I_blur + 0.005 * randn(size(I_blur));
I_blur = max(min(I_blur, 1), 0);
这里imfilter用的是'circular'边界选项,模拟的是周期性边界下的模糊,可以避免边界振铃干扰后续实验对比。真实图像上跑算法时,通常先用edgetaper处理一下边界区域,效果会更好。生成图像之后我一般会顺手存成my_blur.png,后续各种参数实验都基于同一张图,保证可对比性。
2.3 初始PSF估计:模糊方向与尺寸的预判
盲反卷积对初始PSF非常敏感,我在实操里从不会用随机噪声初始化。最常用的方法是先用边缘检测加Hough变换估计运动模糊方向,再把初始PSF设置成对应方向的线段核。这一步虽然不是算法主体,但直接影响最终收敛结果。
matlab复制% 用Canny边缘检测提取边缘
edge_map = edge(I_blur, 'canny', [0.05 0.15]);
% Hough变换统计主方向
[H, T, R] = hough(edge_map);
peaks = houghpeaks(H, 5);
lines = houghlines(edge_map, T, R, peaks);
angle_est = median([lines.theta]);
% 按估计角度生成直线型psf
len = 15;
psf_init = zeros(len, len);
cx = ceil(len/2); cy = ceil(len/2);
for k = 1:len
idx = round(cx + (k - len/2) * sind(angle_est));
idy = round(cy + (k - len/2) * cosd(angle_est));
psf_init(idx, idy) = 1;
end
psf_init = psf_init / sum(psf_init(:));
这段代码不复杂,但能把“初始PSF方向和真实方向差90度”这种低级错误直接消掉。离焦模糊则可以直接用fspecial('disk', r)生成圆形盘状核,半径r根据模糊程度从3到10试。还有一个备选方案是用Radon变换做方向估计,Robust性稍好,但Hough变换在MATLAB里调用更简洁,工程上够用。
2.4 边界处理与支撑域掩膜
实际图像不可能满足循环边界假设,所以进入主算法前建议先做边界衰减。MATLAB里edgetaper是专门干这个的,代码就一行:
matlab复制I_blur_pre = edgetaper(I_blur, psf_init);
它会根据PSF尺寸对图像边缘做渐变衰减,把边界振铃的影响压到最低。支撑域掩膜也很关键,我一般生成一个和psf_init等尺寸的中心椭圆掩膜:
matlab复制mask = zeros(size(psf_init));
[yy, xx] = meshgrid(1:size(psf_init,2), 1:size(psf_init,1));
mask(((xx-ceil(size(psf_init,1)/2)).^2/1.5^2 + ...
(yy-ceil(size(psf_init,2)/2)).^2/1.5^2) <= ceil(size(psf_init,1)/2)^2) = 1;
这个掩膜的作用是强制PSF集中在中心区域,不允许出现分散的离群亮斑。
3. 核心迭代实现与参数调优过程
3.1 交替迭代主循环
下面给出我自己实现的核心函数。为了便于阅读,我把图像子问题和PSF子问题拆成两个helper函数。
matlab复制function [I_est, P_est] = improved_blind_deconv(I_blur, psf_init, opts)
if nargin < 3, opts = struct(); end
lambda_tv = getopt(opts, 'lambda_tv', 0.02);
lambda_h = getopt(opts, 'lambda_h', 0.01);
mu = getopt(opts, 'mu', 0.3);
Niter = getopt(opts, 'Niter', 80);
support = getopt(opts, 'support', ones(size(psf_init)));
H = psf_init;
X = I_blur;
for iter = 1:Niter
X_old = X;
X = update_image_tv(X, H, I_blur, lambda_tv, mu);
H = update_psf_l2(X, H, I_blur, lambda_h, support);
H = max(H, 0);
H = H / sum(H(:));
rel_chg = norm(X - X_old, 'fro') / norm(X_old, 'fro');
if mod(iter, 10) == 0
fprintf('iter %d: rel_chg=%.4f\n', iter, rel_chg);
end
if rel_chg < 1e-4
break;
end
end
% 最后用估计出的PSF再做一轮非盲恢复
[I_est, ~] = deconvblind(I_blur_pre, H, 20);
P_est = H;
end
function val = getopt(opts, name, default)
if isfield(opts, name)
val = opts.(name);
else
val = default;
end
end
主循环里,每次迭代先更新图像,再更新PSF,固定一个变量交替前进。最后用deconvblind基于估计出的H再精修一轮,主要是利用它内部的阻尼项把残余噪声压一遍。这一步在实际实验中收益很大,恢复出的图像比直接输出X更平滑、更干净。
3.2 图像子问题:TV正则约束实现
图像更新子问题用梯度下降求解。对数据保真项求梯度,需要把当前残差h⊗x-y和翻转后的PSF做相关运算,对应MATLAB里用rot90(h,2
