简介:面向图像处理、计算机视觉方向的学习者与开发者,这份文档围绕 SSIM(结构相似性)图像质量评价算法,提供 Matlab 源代码与详细注释,并兼顾理论推导与代码实现,适合需要理解算法原理、复现实验或进行二次开发的读者。压缩包内仅有 1 个 PDF 文件,约 105KB,属于文档资料类,便于快速查阅与离线保存。目前已有 748 人学习下载,说明它在图像质量评估入门与代码研读中具有一定参考价值。内容系统解释亮度、对比度、结构三项比较及 SSIM 加权公式,并逐段解析 ssim 函数的自动下采样流程、滑动窗口统计、均值、标准差与协方差计算,以及 K、window、L 等参数含义;默认 K=[0.01 0.03]、L=255、高斯窗口 fspecial('gaussian',11,1.5),输出 mssim 与 ssim_map。读者可借此掌握局部质量图的可视化分析,并将其用于图像压缩、增强、视频编码与传输质量评估等场景。
1. 从一次图像质量评价说起:SSIM 到底在量什么
做过 matlab图像处理大作业的人大多踩过同一个坑:图像先加高斯噪声再做中值滤波,肉眼看恢复得不错,PSNR 却只有 26 dB 左右,和另一组明显有块效应的结果几乎打平。问题出在 PSNR 只统计逐像素误差的平方和,对结构信息的退化不敏感。SSIM 换了个思路,把两幅图放在局部窗口里,分别比较亮度、对比度和结构三个分量,最后乘起来得到 0 到 1 之间的相似度。它更贴近人眼对边缘、纹理是否被破坏的判断,因此常被用来评价去噪、压缩、超分、配准这些环节的输出质量。适合读这篇的人有三类:正在写 matlab图像处理课程设计的学生、需要给算法结果补一张客观指标表的工程师,以及想搞清 SSIM 源码里每个参数含义、而不是只会调库函数的人。标题里的“详细注释”不是装饰,SSIM 的公式只有几行,但落到 Matlab 的滑动窗口、边界填充和动态范围上,任何一个细节没对齐,分数就会偏。
2. SSIM 的数学骨架与 Matlab 向量化实现
2.1 亮度、对比度、结构三项的物理含义与公式拆解
SSIM 的原始定义建立在两幅图 x、y 的局部统计量上,局部窗口内取均值 μx、μy,标准差 σx、σy,以及协方差 σxy。亮度比较项写成 (2μxμy + C1) / (μx² + μy² + C1),它衡量两幅图局部平均灰度是否接近;对比度项写成 (2σxσy + C2) / (σx² + σy² + C2),它衡量局部反差是否一致;结构项写成 (σxy + C2/2) / (σxσy + C2/2),它衡量两幅图局部纹理走向是否相同。三项相乘就是 SSIM 的简化形式,工程实现里通常把对比度项和结构项合并成 (2σxy + C2) / (σx² + σy² + C2),这样一次除法就能算完。
C1 和 C2 是小常数,目的是避免分母接近零时结果爆掉。常见取值 C1 = (K1·L)²,C2 = (K2·L)²,其中 L 是图像动态范围,8 位灰度图 L = 255,K1 = 0.01,K2 = 0.03。把 L 设为 255 还是 1,会直接改变 C1、C2 的数量级,这是源码里最容易埋雷的地方。均值、方差、协方差如果用循环逐像素算,500×500 的图就要跑 25 万次窗口统计,Matlab 里通常几秒到几十秒,改成 imfilter 或 conv2 做向量化之后,同一张图能在毫秒级算完。
2.2 用 Matlab 写一个最小可运行的 SSIM 函数
下面这段代码没有调用任何工具箱里的 ssim 函数,全部用基础滤波实现,方便逐行对照公式。输入图像先归一化到 double 类型的 [0,1],窗口用高斯核,边界用 replicate 填充,保证输出图和输入图同尺寸。
function [mssim, ssim_map] = ssim_local(img1, img2) % SSIM_LOCAL 计算两幅灰度图的 SSIM % img1, img2 : 尺寸一致的灰度图,double,范围 [0,1] % mssim : 平均 SSIM,标量 % ssim_map : 逐像素 SSIM 图,与输入同尺寸 if ~isequal(size(img1), size(img2)) error('两幅图像尺寸不一致,无法逐像素比较'); end img1 = double(img1); img2 = double(img2); % 归一化输入对应 L = 1,常数 C1、C2 必须按 L = 1 计算 K1 = 0.01; K2 = 0.03; L = 1; C1 = (K1 * L)^2; C2 = (K2 * L)^2; % 11x11 高斯窗,sigma = 1.5,归一化后加权和为 1 win = fspecial('gaussian', 11, 1.5); win = win / sum(win(:)); % 局部均值,replicate 表示边界复制填充 mu1 = imfilter(img1, win, 'replicate'); mu2 = imfilter(img2, win, 'replicate'); % 局部平方均值与乘积均值 mu1_sq = mu1 .* mu1; mu2_sq = mu2 .* mu2; mu1_mu2 = mu1 .* mu2; % 局部方差与协方差:E[x^2] - (E[x])^2 sigma1_sq = imfilter(img1 .* img1, win, 'replicate') - mu1_sq; sigma2_sq = imfilter(img2 .* img2, win, 'replicate') - mu2_sq; sigma12 = imfilter(img1 .* img2, win, 'replicate') - mu1_mu2; % 合并对比度项与结构项后的 SSIM 图 ssim_map = ((2 * mu1_mu2 + C1) .* (2 * sigma12 + C2)) ./ ... ((mu1_sq + mu2_sq + C1) .* (sigma1_sq + sigma2_sq + C2)); mssim = mean(ssim_map(:)); end这段代码里,fspecial('gaussian', 11, 1.5)生成的是未归一化的高斯核,必须除以sum(win(:)),否则滤波结果是加权和而不是加权平均,μ 和 σ 都会被整体放大。imfilter的第三个参数'replicate'控制边界填充方式,如果不写,默认补零,图像四周的均值会被拉低,ssim_map 边缘出现一圈暗带。img1 .* img1再滤波得到的是局部二阶矩,减去均值平方才是方差,这一步和公式里的 E[x²] − μ² 一一对应。最后mean(ssim_map(:))把二维矩阵拉成一列再求均值,得到整幅图的 MSSIM。
2.3 滑动窗口方式与 imfilter/conv2 的选择
Matlab 里做局部加权统计有三条路:imfilter、conv2和手动blockproc。imfilter支持'replicate'、'symmetric'、'circular'等多种边界选项,输出尺寸默认与输入相同,最适合 SSIM 这种要求逐像素对应的场景。conv2在数学上做的是卷积,会把核翻转,对称高斯核翻转前后一样,所以结果相同,但它默认补零,需要自己用padarray补边再裁剪。blockproc按块处理,块与块之间不重叠,得到的 SSIM 图有明显方块感,一般只用于快速统计。
| 函数 | 边界行为 | 输出尺寸 | 是否适合 SSIM |
|---|---|---|---|
| imfilter | replicate / symmetric 可选 | 与输入相同 | 推荐,代码最简洁 |
| conv2 | 默认补零,需手动 pad | 可能变大,需裁剪 | 可用,但要自己处理边界 |
| blockproc | 按块独立 | 块级结果 | 仅适合粗粒度统计 |
从计算量看,一次 SSIM 需要 5 次滤波:μ1、μ2、E[x²]、E[y²]、E[xy]。如果窗口是 11×11,每次滤波的乘加量约为像素数乘以 121。用imfilter时 Matlab 会自动选择频域或空域算法,窗口小于约 15×15 时走空域,速度已经够用。真正拖慢速度的是把图像转成 uint8 后直接滤波,整数类型在乘加中会截断,所以函数入口处统一double,这一条在 matlab图像处理 的常规流程里也应该成为习惯。
3. 逐行注释版 SSIM 源码:从读图到输出相似度图
3.1 预处理:灰度化、数据类型与动态范围对齐
拿到两幅 RGB 图直接算 SSIM 是常见的错误起点。SSIM 定义在单通道上,彩色图要么转灰度,要么在 R、G、B 三个通道分别算完再平均。转灰度用rgb2gray,它按 0.2989、0.5870、0.1140 的权重加权,比自己写mean(img,3)更接近人眼亮度感知。如果两幅图之一已经是灰度图,另一幅是彩色,必须先统一通道数,否则isequal(size(...))直接报错。
动态范围对齐同样关键。参考图从 PNG 读进来是 uint8,范围 0 到 255;重建图可能是 double,范围 0 到 1。两者放在一起算,μ 的差会达到两个数量级,SSIM 分数趋近于 0。预处理阶段统一做两件事:im2double把整数图转成 [0,1] 的 double,再检查max(img(:)),如果某幅图因为归一化方式不同峰值只有 0.8,要按实际峰值重新缩放,或者把 L 改成对应值。下面这段读图和预处理的写法可以作为固定模板。
% 读取参考图与待评价图 ref = imread('ref.png'); dis = imread('distorted.png'); % 统一为单通道 if size(ref, 3) == 3 ref = rgb2gray(ref); end if size(dis, 3) == 3 dis = rgb2gray(dis); end % 统一为 [0,1] 的 double ref = im2double(ref); dis = im2double(dis); % 尺寸对齐后再计算 if ~isequal(size(ref), size(dis)) dis = imresize(dis, size(ref)); end [mssim, ssim_map] = ssim_local(ref, dis); fprintf('MSSIM = %.4f\n', mssim);im2double对 uint8 除以 255,对 uint16 除以 65535,对已经是 double 且范围在 [0,1] 的输入保持不变。imresize只在尺寸不一致时使用,缩放本身会引入模糊,可能让 SSIM 略偏高,所以评价压缩失真时最好保证两幅图原始尺寸一致,而不是靠拉伸对齐。
3.2 用高斯加权窗口替换均匀窗口
原始 SSIM 论文用的是 8×8 或 11×11 的均匀窗口,每个像素权重相同。工程实现里更常见的是高斯窗,因为人眼对窗口中心区域的敏感度高于边缘,高斯加权能减少块边界处的分数跳变。fspecial('gaussian', 11, 1.5)生成的核中心权重最大,向外按指数衰减,sigma 越大衰减越慢,窗口内的有效统计区域越大。
窗口尺寸和 sigma 要配套调。窗口太小,局部均值波动大,平坦区域容易出现过高的 SSIM;窗口太大,超过 15×15 之后对局部结构的定位能力下降,ssim_map 会变得模糊。常用组合是 11×11 配 sigma = 1.5,或者 7×7 配 sigma = 1.0。下表给出几组经验值,可以直接抄进代码里做对比实验。
| 窗口大小 | sigma | 适用场景 | 备注 |
|---|---|---|---|
| 7×7 | 1.0 | 小尺寸图像、细节丰富 | 分数略低,定位更细 |
| 11×11 | 1.5 | 通用场景 | 论文常用,兼容性最好 |
| 15×15 | 2.0 | 大尺寸图像、压缩失真 | 分数更平滑,边缘定位变粗 |
替换窗口只需要改fspecial那一行,其他滤波调用不变。注意fspecial生成的高斯核本身是按二维高斯函数离散采样得到的,边界权重不会严格截断为零,归一化之后误差可以忽略。
3.3 输出 SSIM 图与 MSSIM 均值,并画出来
ssim_map 每个像素的值都在 −1 到 1 之间,1 表示局部完全一致,接近 0 表示结构不相关,负值表示局部反差方向相反。直接看 MSSIM 一个数会丢掉空间信息,把 ssim_map 用imagesc画出来,再叠加原图轮廓,能一眼看出失真集中在哪些区域。
figure('Name', 'SSIM 分布'); subplot(1,3,1); imshow(ref); title('参考图'); subplot(1,3,2); imshow(dis); title('待评价图'); subplot(1,3,3); imagesc(ssim_map); axis image off; colormap(gca, jet); colorbar; title(sprintf('SSIM 图,MSSIM = %.4f', mssim));imagesc会自动把 ssim_map 的数值范围映射到颜色,axis image保持像素长宽比,colorbar显示颜色与数值的对应关系。如果 ssim_map 出现大面积深蓝,说明这些区域的结构已经被严重破坏;如果是压缩图像,深蓝往往集中在块边界和高频纹理处。配合 matlab画图 里的subplot布局,一张图就能同时展示原图、失真图和误差分布,放在报告里比单独一行 MSSIM 更有说服力。
4. 参数调优与典型坑:窗口、K1/K2、位深与彩色图
4.1 窗口大小与高斯标准差怎么配
窗口和 sigma 不是独立参数,窗口边长大约取 6σ 再加 1,高斯权重在边界处才接近零。11×11 配 1.5 对应 6×1.5 + 1 ≈ 10,刚好落在窗口边缘。如果强行用 11×11 配 sigma = 3,高斯核在窗口边界仍有明显权重,等效于一个更大的窗口,局部统计被过度平滑,ssim_map 会失去定位能力。反过来,7×7 配 sigma = 2 会让边界权重被截断,相当于窗口边缘被强制置零,均值估计出现偏差。
判断组合是否合理,可以打印高斯核的行和列分布。sum(win,1)得到的行向量应该中间高、两端低,两端数值小于中心的 5%。如果两端还很大,就要么加大窗口,要么减小 sigma。这个检查在换用不同尺寸图像时尤其有用,因为同一组参数在 256×256 上表现正常,换到 1024×1024 未必合适。
4.2 K1、K2 与 L 的取值对分数的影响
K1 和 K2 控制常数项的大小,直接影响低对比度区域的稳定性。K1、K2 越大,C1、C2 越大,平坦区域的分母被抬高,SSIM 更接近 1,分数整体偏高;K1、K2 越小,噪声和微小差异更容易拉低分数。常见取值固定为 0.01 和 0.03,是为了让 C1、C2 在 L = 255 时分别约为 6.5 和 58.5,这个量级与 8 位图像的灰度方差相当。
真正的坑在 L 与输入范围不匹配。如果图像已经im2double到 [0,1],却仍然用C1 = (0.01 * 255)^2,C1 会变成 6.5,而分母里的 μ²、σ² 只有 0 到 1 的量级,C1 完全主导分母,SSIM 几乎恒等于 1,指标彻底失效。反过来,图像保持 0 到 255 的 double,却用 L = 1 算 C1、C2,常数项接近 0,分母在平坦区域接近 0,ssim_map 会出现 NaN 或爆炸值。
| 输入范围 | L 取值 | C1 | C2 | 结果倾向 |
|---|---|---|---|---|
| [0, 255] | 255 | 6.5025 | 58.5225 | 标准实现 |
| [0, 1] | 1 | 1e-4 | 9e-4 | 正确归一化 |
| [0, 1] 但 L=255 | 255 | 6.5025 | 58.5225 | 分数恒接近 1,错误 |
| [0, 255] 但 L=1 | 1 | 1e-4 | 9e-4 | 分数抖动或 NaN,错误 |
实际写代码时,把 L 和输入范围绑定检查一次:assert(max(img1(:)) <= 1 && max(img2(:)) <= 1)之后再设 L = 1,或者全部保持 0 到 255 并在函数开头显式注明。两条路线都行,关键是别混用。
4.3 常见报错与结果偏差排查
SSIM 算出来和ssim内置函数差 0.01 以上,或者分数明显反常,多数不是公式抄错,而是边界和数据类型的问题。下面列出几类高频现象和处理方式。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| ssim_map 四周出现暗带 | imfilter 默认补零 | 加'replicate'或'symmetric' |
| 分数恒为 0.99 以上 | C1、C2 相对输入过大 | 检查 L 与 im2double 是否匹配 |
| 分数为负或 NaN | 局部方差出现负值、除零 | 检查输入是否含 NaN,必要时对分母加 eps |
| 彩色图分数远低于预期 | 未转灰度或通道未对齐 | 用 rgb2gray,或逐通道算完再平均 |
| 与内置 ssim 差 0.02 左右 | 窗口、sigma、边界方式不同 | 对齐窗口参数和动态范围后再比 |
局部方差理论上非负,但浮点误差可能让它变成 −1e-16,分母因此接近零。稳妥做法是在除法前对sigma1_sq、sigma2_sq做一次max(..., 0),或者依赖 C2 兜底。如果输入图本身有 NaN,比如从某些 mat 文件读出来的无效像素,imfilter会把 NaN 扩散到整幅图,先做img(isnan(img)) = 0再进入 SSIM 函数。
5. 进阶:把 SSIM 接入图像处理大作业的完整流程
5.1 批量评价与结果表格化
做 matlab图像处理大作业时,通常要对比多组算法、多张测试图。手动一张张调用 ssim_local 效率低,写一个循环把文件名、算法名、MSSIM 和 PSNR 一起收集到 table 里,最后导出成 CSV,报告里直接贴表。下面这段代码假定参考图放在 ref 目录,失真图按算法分子目录存放。
ref = im2double(rgb2gray(imread(fullfile('ref', 'lena.png')))); algos = {'gaussian', 'median', 'wiener'}; rows = {}; for a = 1:numel(algos) files = dir(fullfile('distorted', algos{a}, '*.png')); for k = 1:numel(files) dis = im2double(rgb2gray(imread(fullfile(files(k).folder, files(k).name)))); [mssim, ~] = ssim_local(ref, dis); mse = mean((ref(:) - dis(:)).^2); psnr_val = 10 * log10(1 / mse); rows(end+1, :) = {algos{a}, files(k).name, mssim, psnr_val}; %#ok<SAGROW> end end T = cell2table(rows, 'VariableNames', {'Algorithm', 'Image', 'MSSIM', 'PSNR'}); writetable(T, 'quality_metrics.csv'); disp(T);cell2table把元胞数组转成表格,VariableNames指定列名,writetable直接写出 CSV。注意 PSNR 在归一化图像上计算时用10*log10(1/mse),如果图像保持 0 到 255,则改成10*log10(255^2/mse),两者相差约 48 dB,不能混用。批量跑之前先确认每张失真图和参考图尺寸一致,否则 imresize 会引入额外模糊,指标对比失去意义。
5.2 用 SSIM 图定位失真区域
MSSIM 是一个全局标量,真正有价值的是 ssim_map 的空间分布。把 ssim_map 小于 0.9 的区域做二值化,再叠加到失真图上,可以快速圈出算法失效的位置。做法是mask = ssim_map < 0.9;,然后用imoverlay或者手动改通道值把 mask 标红。如果发现暗区集中在图像边缘,多半是滤波边界填充方式不一致;如果集中在纹理区,说明算法对高频细节的保留不足。
还可以对 ssim_map 做分块平均,把图像分成 8×8 的块,每块取均值,得到一个粗粒度的质量分布图。这样既能保留空间信息,又不会被单个像素的噪声干扰。分块后用imagesc画出来,颜色越深代表该块质量越差,配合原图对照,很快就能定位到具体区域。
5.3 与 PSNR、MS-SSIM 的取舍
PSNR 计算简单、物理意义明确,但对结构退化不敏感;SSIM 更贴近人眼,却对窗口和参数敏感;MS-SSIM 在多尺度上做 SSIM,对模糊和压缩的排序更稳,但实现复杂度高,计算量约为单尺度 SSIM 的 3 到 5 倍。做课程设计或算法对比时,我的习惯是同时给出 PSNR 和 MSSIM,前者作为传统基线,后者作为感知指标,两者结论一致时可以直接下判断,不一致时把 ssim_map 调出来看失真类型。
如果时间允许,再补一个 MS-SSIM 的多尺度版本:把图像连续下采样 4 次,每个尺度算一次 SSIM,最后按权重相乘。权重通常取 0.0448、0.2856、0.3001、0.2363、0.1333,对应五个尺度。这样得到的分数对模糊和压缩更敏感,在超分、去噪这类任务里比单尺度 SSIM 更接近主观打分。把尺度数、权重和窗口参数写进函数注释里,别人复现时就不用再猜。把窗口设为 7×7、sigma 设为 1.5,再把 ssim_map 低于 0.9 的区域叠加到原图上,就能在一张图里同时看到分数和失真位置。
本文还有配套的精品资源,点击获取