简介:BM3D(Block-Matching and 3D)算法的 MATLAB 实现包,面向图像处理研究者与工程师,用于灰度、彩色图像及视频序列的高质量去噪与细节恢复。压缩包共 92 个文件,约 7.27MB,包含 8 个 .m 源文件、2 个 .p 加密脚本、覆盖多平台的 MEX 编译文件(mexw32/mexw64/mexa64/mexmaci64 等),以及 16 张 PNG 测试图像和 TIF 样例、说明文档。MEX 版本涵盖基础阈值、维纳滤波、彩色噪声、视频、锐化等变体,可在 MATLAB 中直接调用以提升运行速度;.m 源码便于理解块匹配与三维协同滤波的完整流程,方便调整参数并迁移到自定义数据上。包内还附带演示脚本和 README 说明,帮助用户快速验证算法效果、复现论文结果或对比不同去噪策略。已有 1452 人学习/下载,适合希望借助成熟实现做实验对比、算法研究或工程落地的读者。
1. 为什么绕不开BM3D:一个跑过几百张图后的结论
做图像去噪的人,大概都绕不开BM3D这套算法。哪怕现在深度学习模型满天飞,写论文做对比实验时,BM3D依然是最常被拿来做baseline的那个名字。这组BM3D的Matlab代码,我断断续续用了两年,从灰度图到彩色图,从合成噪声到真实相机RAW,踩了不少坑,也把参数调教的逻辑摸清了。它解决的问题很简单:在去除高斯噪声的同时,尽量保留边缘和纹理,不像高斯滤波那样把细节一并抹掉。适合谁?正在复现论文、做图像处理课程设计、或者只想在Matlab里快速得到一个靠谱去噪结果的开发者。如果你以为它和深度学习一样靠大量数据训练,那就错了——BM3D从头到尾都是手工设计的块匹配与协同滤波流程,没有任何学习过程,参数体系完全独立,这一点是理解它的关键。
2. BM3D在做什么:两步估计与三个核心机制
2.1 基础估计:分组、协同硬阈值、聚合
BM3D的第一步是“基础估计”,也叫第一级估计。它的核心思想是:把图像分成一个个小块,然后找相似块,把它们堆叠成三维数组,在三维空间里做硬阈值去噪,最后把每个块估计回原来位置,通过加权聚合得到整幅图的基础估计。
我刚开始接触时,最困惑的是“块匹配”到底怎么找相似块。具体来说,算法以当前块为中心,在一个更大的搜索窗口内滑动,计算每个候选块与参考块的欧氏距离(通常是归一化后的平方差之和),小于某个阈值(比如tau_match)的块就被视为相似块。这些相似块连同参考块本身,按顺序堆成一个三维矩阵。在Matlab代码里,这一过程由@bm3d_thr_3D这类函数完成,输入是分组后的三维数组,输出是硬阈值处理后的系数。
硬阈值用的变换通常是三维DCT,也可以配置为其它西变换。代码中会定义lambda_thr参数,所有绝对值小于lambda_thr * sigma的变换系数都被置零,剩下的系数被保留并归一化。这一步的直观效果是:噪声在变换域里表现为小幅值系数,硬阈值把它们干掉,而真实信号通常是大幅值系数,能存活下来。
聚合环节则决定了最终输出的质量。因为每个像素会被多个块覆盖,每个块估计出来后,要根据“该块被保留了多少非零系数”来赋权重。代码里用w数组保存每组块的置零比例,聚合时对每一像素,累加“该块恢复值 × 权重”,再除以权重总和。这就像一个加权平均,权重高的是那些去噪后仍然保留了较多结构的块,对最终结果贡献更大。
2.2 最终估计:维纳滤波与权重
基础估计完成后,得到了一张相对干净的参考图。第二步“最终估计”用这张参考图来引导维纳滤波。这一步不再用硬阈值,而是利用基础估计中恢复出来的结构做信噪比估计。
代码的执行逻辑是:先从基础估计图里重新做块匹配(同样用相似度阈值),但这次匹配的对象是基础估计图,而不是原始噪声图。因为基础估计已经去掉了大部分噪声,块与块之间的相似度计算更可靠,分组质量更高。随后,对每一组三维数组,同时对噪声原始块和基础估计块做正变换。在变换域,用基础估计块的系数能量和噪声方差计算维纳滤波系数,公式大致是coef = abs(core) .^ 2 ./ (abs(core) .^ 2 + sigma^2),再用这个系数去缩放噪声块的变换系数。
这里有个容易疏忽的细节:维纳滤波中的sigma^2是第二阶段需要重新传入的噪声方差,和第一阶段的sigma是同一个值,但代码里可能会在内部做二次缩放(比如乘上mu2)。如果你发现最终结果比预期噪,先检查这个mu2参数是不是被改动过。聚合环节与第一步相同,但权重改成了维纳系数平方的累加,效果上比硬阈值更平滑,也更精准,因为它利用的是参考图的全局结构信息,而不是硬性的零一决策。
2.3 代码文件结构:从主函数到核心函数
拿到这组Matlab代码后,先别急着运行,花十分钟看清文件结构。常见实现会包含以下核心部分:
- 主脚本:
demo.m或run_bm3d.m,负责读图、加噪、调用去噪函数、计算PSNR/SSIM并显示结果。 - BM3D主入口:
bm3d.m,内部定义参数结构,并依次调用第一步和第二步。 - 第一步核心函数:
bm3d_1st_step.m,承担块匹配、硬阈值协同滤波和聚合。 - 第二步核心函数:
bm3d_2nd_step.m,承担基于基础估计的块匹配、维纳滤波和聚合。 - 辅助工具:
get_similar_blocks.m、filtering_3D.m、group_accumulation.m这类函数,分别完成相似块搜索、三维变换与滤波、加权累加。
我给一个我惯用的调用框架,几乎每次替换图片路径就能直接用:
% demo_bm3d_quick.m clear; close all; clc; % 读入图像,并转为double org = imread('test_image.png'); if size(org, 3) == 3 org = rgb2gray(org); end org = im2double(org); % 模拟高斯噪声,sigma 可调 sigma = 20 / 255; noisy = org + sigma * randn(size(org)); % BM3D 去噪 [denoised, PSNR_out, ~] = bm3d(noisy, sigma * 255); % 注意噪声标准差要传进像素域尺度 % 展示与保存 figure(1); imshow(noisy); title('噪声图'); figure(2); imshow(denoised); title('BM3D去噪结果'); fprintf('PSNR = %.2f dB\n', PSNR_out); imwrite(denoised, 'denoised_result.png');这段脚本里有三个地方容易错,先说明白。第一,im2double之后像素范围是0到1,所以加噪时randn的标准差要与像素尺度匹配,我一般直接用sigma = 20/255这种比例换算,但bm3d主函数内部常要求噪声方差传入0到255范围的数值(即sigma * 255),不同版本约定不一样,务必看代码注释。第二,bm3d第三个返回值是运行时间或参数结构,取决于版本,不想要就写成~。第三,如果输入的噪声标准差和实际加噪值不一致,去噪会明显偏软或偏硬,这是所有调试中最不值得踩的坑。
3. 跑起来:调用方式、参数含义与可视化
3.1 最简调用与输入准备
BM3D的Matlab代码一般提供两种调用层级。一种是完整参数结构bm3d(noisy, sigma),内部有默认配置,适合快速跑通;另一种是传入自定义opts结构,可以精确控制块大小、步长、搜索窗口等。新手直接用第一种即可,但要有意识地读一遍bm3d.m里opts的初始化段。
我通常在写新代码前,会先跑一个最小的自检样例,确认环境没问题。比如下载好代码包后,先直接运行包里的demo脚本。如果demo能出图,说明核心函数都在路径上。接下来再替换自己的数据和参数。
需要注意输入图像必须是二维灰度图,范围是double的0到1,或者uint8的0到255。很多第三方实现默认接受0到255的uint8,但内部会转成double。如果你直接传一个0到1的double图,并且把sigma设成20,那相当于把20当成像素值,实际标准差远大于图像动态范围,去噪结果会糊成一团。这类错误很隐蔽,我见过有人卡了一整天。
另一个输入坑是rgb2gray之后的图像类型。Matlab的rgb2gray在uint8和double下输出范围不同,前者是0到255,后者是0到1。我建议统一走一遍:先im2double再rgb2gray,或者反过来。下面是一个稳妥的预处理模板:
function [org_double, noisy_double] = prepare_image(path, sigma) % 统一处理图片读入、灰度转换和加噪 % 返回:原始灰度图(double 0-1),带噪灰度图(double 0-1) img = imread(path); if size(img, 3) == 3 img = rgb2gray(img); end org_double = im2double(img); noisy_double = org_double + sigma * randn(size(org_double)); end这个函数做了三件事:读图、去彩色、转double。之所以把加噪也放进去,是因为做对比实验时,所有图像应该使用同一个随机种子,否则PSNR波动会干扰判断。我一般会在主脚本里固定rng(0),让实验结果可复现。
3.2 关键参数:sigma、块大小、步长、阈值
去掉那些只有极少数场景才会改的隐藏参数,真正需要理解的是下面这组:
| 参数 | 典型默认值 | 作用 | 我的调整建议 |
|---|---|---|---|
sigma | 噪声标准差 | 算法内部用于阈值计算和维纳滤波 | 必须和实际噪声一致,否则结果崩坏 |
N1/k | 8 | 块尺寸,决定最小结构单元 | 图像大且细节多时用8;过于平滑的图用12 |
Nstep/Ns | 3 | 块滑动的步长 | 步长越小,块重叠越多,去噪越彻底但越慢 |
N2 | 16 | 搜索窗口半边尺寸 | 纹理区域可以适当减小,平滑区域可以加大 |
tau_match | 3000 | 块相似度阈值 | 越大越容易分到更多异质块,越小分组越严 |
lambda_thr | 2.7 | 硬阈值系数 | 噪声强时稍微上调,但一般不用动 |
先解释N1为什么通常取8。BM3D的块既要包含足够纹理信息以区分相似度,又不能大到把不相似的区域硬凑在一起。8×8块在绝大多数512×512图像上表现均衡,和JPEG块尺寸一致,DCT基底也容易产生稀疏系数。如果图像是医学影像那种大块均匀区域,块内差异小,取12×12反而能提升聚合稳定性。但块变大,搜索的相似块数量变少,去噪效果不一定线性提升。
Nstep参数经常被忽略,但它直接决定计算量。步长设为3时,每个像素大约被多个块覆盖,聚合更平滑;步长设为8时,几乎不重叠,速度快很多,但输出会出现明显的块效应。如果你做实时预览,可以用步长5折中。我一般离线处理用3,批量测试性能用5。
tau_match这个阈值在不同代码版本里数值差异很大。有些实现用归一化距离,默认值可能是2500;有些用原始像素差平方和,默认能到几千。所以不要机械地把A版本的3000搬到B版本,要观察分组结果。怎么观察?打开相似块索引的中间数据,看看每个参考块找了多少个相似块,如果大多数块都只找到自身,说明阈值过严;如果所有块都找到一大群差异巨大的块,说明阈值过松。
3.3 输出与中间结果检查
bm3d函数返回的去噪图还需要做一步后处理:大多数代码会把输出裁掉边框。原因是块匹配涉及边界扩展,imfilter式操作会在边界产生伪影。如果你打印PSNR时发现比论文结果低1dB,先检查输出图像尺寸是否比原图小了pad_size,如果是,直接比较中心区域,或者手动去掉边界再算PSNR。
我自己有个习惯:跑完一次后,把噪声图、基础估计、最终估计三张图同时显示出来对比。基础估计通常看起来偏“油画”,细节有轻微丢失,但整体干净;最终估计会在此基础上恢复更多纹理。如果基础估计已经模糊得不像话,问题多半出在lambda_thr或sigma上,而不是最终估计。如果基础估计看起来仍有大量噪点,可能是相似块数量太少,这时把N2调大或把tau_match放宽。
检查中间结果的另一个实用技巧是把聚合权重图导出来。BM3D代码里通常会有w数组,记录每个像素的聚合权重归一化值。权重极低或为0的地方,意味着该像素几乎没被任何块覆盖,这就是黑边或空洞的来源。正常情况下,权重图应该整体平滑且极少接近0。我见过一次去噪后图像出现横条纹,查了半天,就是某个块索引数组越界导致权重数组部分行未被累加。
4. 避坑指南:BM3D调试中的五个典型问题
4.1 现象:图像越去噪越模糊,连边缘都没了
原因:把sigma设得比实际噪声大很多。比如实际噪声标准差只有10,却传了20,BM3D会认为信号中需要保留的幅度很低,硬阈值和维纳滤波把真实细节当噪声干掉。
解决:先做噪声估计。最可靠的办法是从图像均匀区域计算标准差,或者用noise_std = median(abs(noisy(:) - medfilt2(noisy))) / 0.6745这类中值法。在Matlab中,我会先用一块纯色区域验证,再整图计算。
% 估计噪声标准差(中值法) noisy_flat = noisy(:); med = medfilt2(noisy); diff = noisy - med; sigma_est = median(abs(diff(:))) / 0.6745;这段代码的假设是噪声在空间上随机且独立。如果图像本身纹理密集,medfilt2会连纹理一起滤掉,导致diff包含真实结构,标准差偏高。这时可以把范围限制在图像角落的均匀区域。
4.2 现象:输出图有方块感和周期性网格
原因:块匹配时的搜索步长Nstep太大,或者tau_match太松,导致分组的块之间关联性弱,聚合时每个像素的覆盖次数不足。
解决:优先把Nstep调到3,重新跑。如果还有块状边界,把N1增大到12试试。另外,一些实现里维度变换固定为DCT,DCT本身会把块边界隐式周期化,当块之间不重叠时,边界效应就会显现。可以在代码里把块切换为重叠更多的方式,也就是减小步长。
4.3 现象:PSNR比论文低很多,且输入换一张图结果差别巨大
原因:BM3D的默认参数是针对512×512自然图像调的。如果输入是200×200的小图,搜索窗口相对图幅过大,块匹配在边缘处找不到足够相似块,去噪能力被削弱。
解决:按图像尺寸缩放参数。我通常的做法是:如果图幅小于256×256,把N2从16改成8,tau_match降低20%;如果图幅大于1024,把N2增加到24,防止搜索范围不足。下面是一个简单映射:
if size(noisy, 1) < 256 opts.N2 = 8; opts.tau_match = opts.tau_match * 0.8; elseif size(noisy, 1) > 1024 opts.N2 = 24; end4.4 现象:Matlab报错“数组索引越界”或“矩阵维度必须一致”
原因:多数实现使用padarray扩展图像,但扩展尺寸与块大小、步长的配合逻辑在边界处容易出错。常见于图像长宽不是块大小整数倍,或者输入是RGB图但代码只写了灰度处理。
解决:第一,确保输入是灰度图;第二,把输出裁剪函数里的pad_size按floor(N1/2)计算;第三,如果代码内部有用到ndgrid和block_indices,建议自己重写一遍边界索引,而不要依赖默认的reshape顺序。我遇到这类报错时,最有效的排查方法是打开待调用的bm3d_1st_step.m,在for循环前打印图像尺寸,然后手工计算第一个块的索引范围,看是否超出。
4.5 现象:去噪后图像有白色或黑色亮点,像孤立噪点
原因:聚合权重计算时出现除零,或者个别块在硬阈值后全零,恢复时对应像素累加值为零而权重也为零,0/0产生NaN,最终显示为奇异点。
解决:在聚合函数的final = accum ./ weights之前,加一行weights(weights < eps) = eps。这是很多代码包的补丁位置。如果不想改源码,可以在调用后对输出做中值滤波修正,但这是治标不治本。我一般会直接改源码,因为BM3D这种确定性算法不该出现NaN。
5. 进阶:批量去噪、参数微调与性能权衡
5.1 批量处理脚本
实际项目中很少只有一张图。我会把BM3D包进一个批量脚本,用dir读取目录下所有图片,循环调用,并记录每张图的PSNR和处理耗时。下面是一个我常用的框架:
% batch_bm3d.m prefix = 'test_set'; files = dir(fullfile(prefix, '*.png')); result_table = table(); for i = 1:length(files) fpath = fullfile(prefix, files(i).name); [org, noisy] = prepare_image(fpath, sigma); tic; denoised = bm3d(noisy, sigma * 255); t = toc; result_table.psnr_denoised(i) = calculate_psnr(org, denoised); result_table.time(i) = t; end writetable(result_table, 'bm3d_results.csv');批量处理时要注意两个问题:一是磁盘写入不要放在循环内,否则I/O会占掉大量时间;二是每张图最好重新估计噪声,而不是用同一个sigma,因为真实图像不同区域的噪声不均匀,强行用固定值会让一部分图过去噪。
5.2 三个提高速度的取舍
BM3D慢是出了名的。如果只是验证算法,可以直接用默认参数;但做参数扫描时,不太可能每次都全图跑。这里有三个取舍办法。
第一,降分辨率预跑。把长边缩到256,快速得到一组参数,再用全分辨率验证。因为块匹配参数相对尺度不敏感,这种做法效果不错。
第二,降低Nstep从3改到5,处理时间大约能降到原来的四分之一,代价是PSNR下降0.2dB左右。这一步在很多场景下都能接受。
第三,把相似块搜索范围N2从16降到8,并且设定每个参考块最多找16个相似块(代码中一般有N_similar_max参数),计算量会大幅减少。但图像纹理复杂时,相似块数量不够会导致去噪不充分。建议先观察分组结果,再做剪裁。
5.3 验证效果:用PSNR和SSIM说话
最后别忘了量化验证。PSNR比较的是像素级误差,SSIM比较的是结构相似度。我一般同时打印两者,因为有些参数调出来PSNR高但看起来纹理怪,SSIM能纠正这种错觉。下面是一个标准验证函数:
function [psnr_val, ssim_val] = evaluate_denoise(org, denoised) mse = mean((org(:) - denoised(:)) .^ 2); psnr_val = 10 * log10(1 / mse); ssim_val = ssim(denoised, org); end这里有个小习惯:ssim函数要求输入在0到1范围,否则结果会异常。很多人在调完BM3D之后发现SSIM只有0.5,就是因为图像还是0到255的uint8。统一转成double再验证。
用BM3D这套Matlab代码做实验,我从刚开始光会跑通,到后来能按图像内容调整参数,再到把批量脚本做成自动化测试,中间差不多踩了上面提到的所有坑。最让我记住的一次是在某图像处理Demo里,我用BM3D给视频帧去噪,结果因为逐帧独立处理,帧间闪烁严重。从那以后,我每次处理序列帧都会强制把时间维也纳入块匹配,或者至少做相邻帧的相似块约束。希望这篇笔记能帮你少走点弯路,把BM3D真正用起来。
本文还有配套的精品资源,点击获取