简介:这份Python源码包实现了区域差分信息熵(RDE)算法,用于从图像局部统计特性出发评估超分辨率图像的感知质量,较传统像素级误差指标更贴近人眼视觉机制。资源面向计算机视觉方向的学生、研究人员及工程师,适合进行图像质量评价学习、算法复现或开发扩展。压缩包共64个文件,以png图像样本、py算法脚本为主,另含md说明文档与jpg流程示意图,包体大小25.23MB,代码结构清晰且附注释。已有38人学习浏览,可作课程设计、毕设或科研入门参考。读者可获得完整可运行的RDE实现、示例图片与使用说明,便于快速理解算法流程并迁移至其他图像质量评估场景。
1. 区域差分信息熵:一个比PSNR更懂高频细节的超分质量评价指标
上周我调一个轻量级超分模型,验证集PSNR涨了0.4dB,把重建图和原始高清图并排放大看,边缘该糊的还是糊。像素级评价指标的高分与观感脱节不是新鲜事,问题出在PSNR对空间结构不敏感。区域差分信息熵(RDIE)是专门评价超分辨率图像感知质量的指标:先取图像差分信息抓高频结构,再分块统计信息熵,最后与参考图的熵分布对比,把“细节恢复得好不好”量化成一个分数。它适合两类从业者:做超分模型对比时当第二裁判;调图像增强模型时快速判断新权重是否值得保留。下文给出可直接运行的Python实现。
2. 为什么PSNR和SSIM不够:RDIE的三个理论支点
2.1 像素级误差指标的盲区:高频区域才是人眼关注的焦点
PSNR的计算本质是MSE取对数,它对全图每个像素的误差等权处理。一张超分图如果只在平坦区域有轻微亮度偏差,MSE很小,PSNR会给出高分;可如果边缘区域出现振铃伪影、纹理细节丢失,PSNR通常不会有剧烈变化,因为边缘像素在全图占比太小。而在图像超分辨率重建的实际评测里,人眼盯着的恰好是这些高频区域——发丝、草叶、建筑轮廓,像素级指标对这些位置的误差几乎“看不见”。
SSIM引入了亮度、对比度和结构的局部统计相似度,比PSNR进了一步,但它对各类失真的响应不是线性的。对边缘过冲这种“结构信息保住了但观感生硬”的情况,SSIM往往给出还不错的分数。两个指标都建立在像素值的局部统计上,没有直接把“高频信息是否与参考图一致”这一维度分离出来量化。RDIE补的正是这个缺口:它关心的不是像素值的绝对误差,而是高频差分信息的局部丰富程度。
2.2 差分信息:把边缘和纹理变成可测量的信号
差分信息在图像处理里就是相邻像素相减:水平差分dx(x,y)=I(x+1,y)-I(x,y),垂直差分dy(x,y)=I(x,y+1)-I(x,y),梯度幅值g=sqrt(dx²+dy²)。平坦区域g接近0;边缘处g很大;纹理区域g大小交错、分布复杂。超分重建的本质就是恢复这些g不为零的位置及其强度分布,所以用差分图做评价基础,比直接用原始像素更贴近任务目标。
差分算子一般用一阶梯度。为什么不直接用拉普拉斯?拉普拉斯是二阶差分,对噪声比一阶差分更敏感,一点椒盐噪声就会产生很大的二阶响应,把纹理信息淹掉。一阶梯度幅值已经能表达边缘强度和纹理丰富度,RDIE的实现通常用一阶差分,配合可选的高斯预滤波来控制噪声。后面代码里的np.gradient就是中心差分,等价于一阶梯度,不需要额外安装库。
2.3 区域信息熵:用信息论量化细节密度
信息熵H=-Σ(p_i·log2(p_i))衡量一个分布的“混乱程度”。把差分图的直方图看成概率分布,熵值大意味着差分幅度分散、结构信息丰富;熵值小则意味着差分幅度集中在少数几个级别,区域趋于平坦。如果只对全图算一个熵值,会丢掉空间分布信息——一张左边全是纹理、右边全是天空的图像,全局熵可能和一张纹理均匀分布的图像一样。所以要对图像分块,让每个块给出一个熵值,最终得到一张熵值空间分布图。
熵值分布图的含义很直观:左半边缘密集、右半平坦的图像,熵图左高右低;过度平滑的重建结果,整体熵值偏低;引入伪影的结果,局部熵值忽高忽低。对比两张熵图的差异,比对比两张像素图的差异更能对齐感知质量。这个思路也被用在ai对CT超分辨率重建的质控里,CT影像中组织边界和纹理密度的空间分布差异,用熵图能清楚暴露。
2.4 RDIE的完整计算流程与评分逻辑
RDIE的完整流程可以拆成五步:输入HR参考图与SR重建图,都先转灰度;分别用一阶梯度算子计算各自的差分图;将差分图按固定窗口分成互不重叠的块;对每个块统计梯度幅值直方图并计算信息熵,得到熵图;最后比较两张熵图,输出评分。
评分用两个数值组合:熵图的平均绝对差(Mean Absolute Difference, MAD),衡量SR各局部块的信息量与HR的偏差,越小越好;熵图的空间相关系数(Spatial Correlation),衡量高频结构的空间分布是否与HR吻合,越接近1越好。两个数值组合能避免“整体相似但细节错位”的情况——比如SR把左上角的纹理错误复制到右下角,熵值的总量可能不变,MAD很小,但相关系数会明显下降。注意RDIE评价的是“细节还原度”,不完全等价于“主观好看”,但它比PSNR更早暴露高频结构问题。
3. 从公式到Python:区域差分信息熵的源码实现
3.1 依赖环境与函数接口设计
实现RDIE只需要NumPy,建议版本1.20以上。可选依赖两个:SciPy用于高斯滤波,Pillow用于读取图像。如果你手头是超分模型输出的ndarray,直接传给函数就行,不需要落到磁盘。我习惯把源码拆成三个文件:rdie.py放核心函数,demo.py放调用示例,README.md记录参数含义和典型取值,这样不管是自己维护还是给别人跑,都不需要翻代码猜参数。
主函数的接口设计成输入两个ndarray,输出一个字典。字典里同时给熵图差异和相关系数,方便后续扩展:
import numpy as np def rdie_score(hr_img: np.ndarray, sr_img: np.ndarray, block_size: int = 16, bins: int = 64, max_grad: float = 255.0) -> dict: """ 区域差分信息熵评分。 参数说明: hr_img: 高分辨率参考图,形状 (H, W) 或 (H, W, C),RGB顺序。 sr_img: 超分重建图,形状与 hr_img 保持一致。 block_size: 熵值统计的窗口边长,默认 16。 bins: 差分直方图分箱数,默认 64。 max_grad: 梯度幅值归一化上限,默认 255.0。 返回值: dict,包含熵图平均绝对差、空间相关系数、双方熵图均值。 """ hr_gray = _to_gray(hr_img) sr_gray = _to_gray(sr_img) hr_grad = _gradient_map(hr_gray) sr_grad = _gradient_map(sr_gray) hr_ent = _entropy_map(hr_grad, block_size, bins, max_grad) sr_ent = _entropy_map(sr_grad, block_size, bins, max_grad) entropy_diff = np.mean(np.abs(sr_ent - hr_ent)) spatial_corr = np.corrcoef(hr_ent.ravel(), sr_ent.ravel())[0, 1] return { 'entropy_diff': float(entropy_diff), 'spatial_corr': float(spatial_corr), 'hr_entropy_mean': float(hr_ent.mean()), 'sr_entropy_mean': float(sr_ent.mean()), }这里的接口设计有一个关键决定:返回字典而不是单个数值。原因是RDIE的真实使用场景里,你通常需要同时看“总量偏差”(entropy_diff)和“空间分布一致性”(spatial_corr),只看其中一个容易误判。比如一个SR图把所有纹理都磨平了,熵图整体偏低,MAD会变大;但如果模型把纹理位置全部移动了,MAD可能很小,相关系数却很难看。把两个值都抛出来,分析时能直接看到是幅度问题还是位置问题。
3.2 灰度转换与差分图计算
灰度转换用BT.601权重,不用简单的三通道平均。人眼对绿色通道的亮度感知最强,平均会高估蓝色通道的贡献,导致后续梯度计算在蓝色主导的色块上产生偏差。差分图用np.gradient实现中心差分,它不会像np.diff那样缩减数组尺寸,边界处自动退化为单边差分,省去手动padding的麻烦。
def _to_gray(img: np.ndarray) -> np.ndarray: """RGB转灰度图,返回float32,值域保持0-255。""" if img.ndim == 2: return img.astype(np.float32) return (0.299 * img[..., 0] + 0.587 * img[..., 1] + 0.114 * img[..., 2]).astype(np.float32) def _gradient_map(gray: np.ndarray) -> np.ndarray: """一阶中心差分,返回梯度幅值图。""" gy, gx = np.gradient(gray) return np.sqrt(gx ** 2 + gy ** 2)逻辑说明:np.gradient在二维float32输入上返回两个数组,分别是y方向和x方向的中心差分,梯度幅值把两个方向的差分信息合并成一个标量场。中心差分在相邻像素间的步长是1,所以梯度幅值的单位仍然是“灰度差/像素”,不会因为差分实现方式不同引入额外缩放。
参数说明:这里没有暴露算子选择参数,是因为对8bit图像来说,中心差分和Prewitt算子得到的梯度幅值差异极小,基本不影响最终熵值排名。如果你在做医学影像重建,图像噪声水平高,可以在调用_gradient_map前先做一次高斯滤波,我习惯用scipy.ndimage.gaussian_filter,sigma取0.8到1.2之间,下面第4章会细说。
3.3 分块信息熵:直方图、range与边界
熵图计算是整个RDIE的核心。先把梯度图裁剪成block_size的整数倍,再按块统计直方图。直方图的range必须固定为(0, max_grad),不能动态取每张图的最大值——否则两张图的直方图范围不一致,熵值之间没有可比性,这是RDIE实现里最容易翻车的地方。
def _entropy_map(grad: np.ndarray, block_size: int = 16, bins: int = 64, max_grad: float = 255.0) -> np.ndarray: """把梯度幅值图分块,统计每块的信息熵。""" h, w = grad.shape bh, bw = h // block_size, w // block_size grad_crop = grad[:bh * block_size, :bw * block_size] blocks = grad_crop.reshape(bh, block_size, bw, block_size) blocks = blocks.transpose(0, 2, 1, 3).reshape(bh, bw, -1) ent = np.zeros((bh, bw), dtype=np.float32) for i in range(bh): for j in range(bw): hist, _ = np.histogram(blocks[i, j], bins=bins, range=(0.0, max_grad)) total = hist.sum() if total == 0: ent[i, j] = 0.0 continue prob = hist / total prob = prob[prob > 0] ent[i, j] = float(-(prob * np.log2(prob)).sum()) return ent逻辑说明:reshape和transpose的组合把裁剪后的梯度图从(bh, block_size, bw, block_size)重排成(bh, bw, block_size²),每个块的数据连续排在最后一维,这样histogram一次只处理一个块的数据,代码直观且不容易出错。直方图的bin宽度由bins和range共同决定:range固定为0到max_grad,bins=64时每个bin宽度约4个灰度级。
参数说明:total = hist.sum()而不是直接除以block_size²,是因为range固定后,梯度幅值超过max_grad的像素会被直方图忽略。如果某块内大量像素梯度超过255,hist.sum()会小于block_size²,直接用block_size²做分母会让概率和不为1,计算出的熵值偏小。用hist.sum()能保证概率分布归一化。total为0的情况理论上只出现在全黑且max_grad=0的输入里,但防御性写法还是加上,避免log2(0)给出nan。
3.4 主函数:把HR和SR的熵图对比起来
3.1节已经给出rdie_score的主函数,这里补一个完整的调用示例,包含图像读取和结果打印:
from PIL import Image import numpy as np def demo(): hr = np.array(Image.open('hr.png').convert('RGB')) sr = np.array(Image.open('sr.png').convert('RGB')) result = rdie_score(hr, sr, block_size=16, bins=64) print(f"entropy_diff = {result['entropy_diff']:.4f}") print(f"spatial_corr = {result['spatial_corr']:.4f}") if __name__ == '__main__': demo()逻辑说明:PIL读取图像后转RGB,再转ndarray,传入rdie_score。如果超分模型在训练循环里直接输出张量,需要先把张量detach()并转成numpy数组,再调这个函数。注意输入的HR和SR形状必须一致,否则两张熵图的尺寸不同,np.corrcoef会直接报维度错误。
参数说明:block_size=16是默认值,适合512×512左右的测试图;bins=64对应约4个灰度级的直方图分辨率,对8bit图像来说足够细。如果发现不同模型间的分数差异不明显,可以先调大bins到128,看是否被直方图粒度限制住了。
4. 参数怎么调:窗口大小、bins数与差分算子的取舍
4.1 block_size:熵图的分辨率与稳定性的权衡
block_size决定熵图的空间分辨率,也决定每个块内统计样本的数量。block_size=8时,每个块只有64个像素,直方图统计噪声大,熵值在块与块之间跳变明显,容易出现“某一块熵值特别高、相邻块特别低”的假纹理。block_size=32时,每个块1024个像素,熵值稳定,但空间分辨率低,局部细节损失会被平均掉。我实测下来,16是大多数自然图像和遥感图像的甜点值。
| block_size | 每个块像素数 | 表现 | 适用场景 |
|---|---|---|---|
| 8 | 64 | 熵值波动大,对局部伪影敏感但不够稳定 | 小尺寸贴片评测 |
| 16 | 256 | 平衡稳定性与局部性,默认推荐 | 通用超分模型对比 |
| 32 | 1024 | 熵值平滑,突出区域级差异 | 4K大图、整体观感评估 |
另一个选择依据是图像本身的纹理尺度。人脸皮肤纹理细密,block_size=8更容易区分“磨皮过度”和“真实毛孔”;建筑和遥感影像结构块大,block_size=32反而更稳。如果你做的是ai对CT超分辨率重建这类医学影像任务,我建议固定用16,因为CT图像的纹理尺度相对统一,改参数只会增加对比时的变量。
4.2 bins与max_grad:跨图可比的关键参数
bins控制直方图的分箱粒度。bins=16时每个bin宽度约16个灰度级,两个熵值差距很大的模型可能被压到同一个分数区间,区分度不足;bins=256时每个bin宽度只有1个灰度级,直方图对噪声过于敏感,同一模型跑两次分数可能都不一样。64是平衡点,每个bin约4个灰度级,既保留区分度又不会被单像素噪声干扰。
max_grad固定为255.0是一个重要的设计决定。如果把max_grad设为每张图的梯度最大值,那么梯度幅值范围不同的两张图会在不同的尺度上算熵,分数完全不可比。固定为255.0后,所有图像的直方图都落在同一套坐标轴上,熵值才有横向比较的意义。对于float类型的超分输出,如果值域不在0-255,先统一缩放到0-255再传入。
4.3 差分算子与预滤波:噪声场景下的取舍
中心差分对单像素噪声敏感,因为噪声像素与邻域的差分会形成虚假的高梯度。超分模型的输出往往带着轻微的振铃和高频噪声,直接算梯度熵,噪声会成为熵值的主要贡献者,掩盖真实的纹理信息。处理办法是在计算梯度前加一个轻量级高斯滤波,sigma取0.8到1.2,只压掉单像素级别的高频抖动,不会明显损失真实边缘。
from scipy.ndimage import gaussian_filter def rdie_score_robust(hr_img: np.ndarray, sr_img: np.ndarray, sigma: float = 1.0, **kwargs): """带预滤波的RDIE计算,适合噪声明显的重建输出。""" hr_gray = gaussian_filter(_to_gray(hr_img), sigma=sigma) sr_gray = gaussian_filter(_to_gray(sr_img), sigma=sigma) hr_grad = _gradient_map(hr_gray) sr_grad = _gradient_map(sr_gray) hr_ent = _entropy_map(hr_grad, kwargs.get('block_size', 16), kwargs.get('bins', 64), kwargs.get('max_grad', 255.0)) sr_ent = _entropy_map(sr_grad, kwargs.get('block_size', 16), kwargs.get('bins', 64), kwargs.get('max_grad', 255.0)) return { 'entropy_diff': float(np.mean(np.abs(sr_ent - hr_ent))), 'spatial_corr': float(np.corrcoef(hr_ent.ravel(), sr_ent.ravel())[0, 1]), }逻辑说明:gaussian_filter对灰度图做各向同性高斯平滑,sigma决定平滑半径。sigma=0表示不滤波;sigma=1.0时对单像素噪声的抑制明显,但对2像素以上的真实边缘影响很小。注意这里对HR和SR要使用相同的sigma,否则两者的噪声水平不一致,熵值对比会偏向滤波更重的那一方。
参数说明:如果你不想引入SciPy依赖,可以用NumPy实现一个简单的3×3均值滤波,但效果不如高斯,且容易在边缘位置产生额外的平滑梯度。我建议直接装SciPy,这个库在超分评测里本来就常用。
5. 区域差分信息熵避坑记录:5个真实场景下的坑
5.1 现象:梯度直方图全为零,熵值输出NaN
第一次跑通代码时,如果输入图像包含全黑的区域,且图像尺寸刚好等于block_size的整数倍,那块区域的所有梯度都为0,直方图里只有0这个bin有值,其余bin都是0。log2(0)会直接给出nan,导致整张熵图出现nan,最终分数变成nan。
原因:概率为0的bin在信息熵公式里贡献为0,但直接对0做log2运算在浮点数层面是未定义操作。解决:在计算熵之前过滤掉prob为0的bin,代码里用prob = prob[prob > 0]处理,这一步虽然简单,但漏掉它后续排查会耗费半小时。
5.2 现象:用三通道平均值转灰度,彩色图像的分数与主观评测相反
在彩色图像上,RDIE分数可能和你的主观判断完全对不上:一幅明显更清晰的SR图,熵值反而更低。原因大概率出在灰度转换上——如果用img.mean(axis=2)做灰度化,三通道等权,蓝通道的噪声会被抬高,导致梯度图里出现大量虚假的高频响应。
解决:统一使用BT.601权重(0.299, 0.587, 0.114),代码里的_to_gray已经实现。这条在对比两个超分模型时尤其重要,因为GAN类模型常会对色度通道做不同的处理策略,错误的灰度权重会把色度噪声当成纹理,把真实亮度边缘淹没。
5.3 现象:带噪声的SR图像得分虚高,比HR还“好”
噪声是一种高频信号,梯度幅值大且分布随机,信息熵天然偏高。一个被高斯噪声污染的SR图,差分熵可能比干净的HR还要高,RDIE会给出“重建优于参考”的荒谬结论。在ai对CT超分辨率重建这种场景里特别明显,CT原始数据自带噪声,如果直接把噪声也当成纹理,指标就废了。
解决:先做轻量去噪再算RDIE。用4.3节的rdie_score_robust,sigma取1.0到1.5。去噪后梯度图保留真实边缘,压掉随机噪声,分数才对得齐主观判断。注意HR和SR必须用相同的sigma,否则引入的系统性偏差比噪声本身还大。
5.4 现象:图像尺寸不是block_size的整数倍,边缘信息被静默丢弃
当输入图像尺寸为517×513,block_size=16时,裁剪后只剩512×512,右侧5列和下侧1行的梯度信息全部丢失。如果这5列恰好包含建筑轮廓或文字边缘,SR的熵图会比HR多丢一块信息,spatial_corr失真,分数偏低。
原因:_entropy_map开头用了裁剪,边缘位置的中心差分本身是单边差分,数值不稳定,裁剪也能顺带规避。解决:在调用rdie_score前,把HR和SR统一resize到能被block_size整除的尺寸,比如用cv2.resize或PIL的resize,双线性插值即可。不要靠裁剪对齐,因为裁剪会让两张图的视场不一致。
5.5 现象:HR和SR尺寸不一致,corrcoef直接报错
超分评测里常见一个错误:SR输出的边长和GT不一样,比如模型输出是480×480,GT是512×512,直接把两张图传入rdie_score,熵图尺寸分别为(30,30)和(32,32),np.corrcoef处理不同长度的数组时抛出维度不匹配的异常。
原因:没做尺寸对齐。解决:在调用rdie_score前先断言shape一致,不一致时提示先resize或crop。我一般会在主函数里加一个检查,如果hr_img.shape != sr_img.shape,直接抛出带有size信息的ValueError,省得在模型训练流程里静默出错:
if hr_img.shape != sr_img.shape: raise ValueError( f"HR shape {hr_img.shape} 与 SR shape {sr_img.shape} 不一致," "请先统一尺寸再计算RDIE" )6. 把RDIE嵌入超分模型评测管线:组合评分卡与早期停止
RDIE单独用还不够,它和PSNR、SSIM、LPIPS是互补关系。PSNR最能反映像素保真度,SSIM反映结构相似性,LPIPS通过深度特征响应衡量感知相似度,RDIE则专门量化高频细节的空间分布。我在实际评测里会列一张表:PSNR排在前面做粗筛,低于阈值的直接淘汰;通过粗筛的模型再算SSIM和LPIPS,最后用RDIE的entropy_diff和spatial_corr做最终排序。这样安排的原因是RDIE对过度平滑的惩罚很敏感,而这类问题往往在PSNR上分数不低。如果你的超分模型是GAN类,RDIE还能帮你识别“假细节”——生成器制造的高频伪影会让熵图与HR错位,spatial_corr立刻掉下来,这比肉眼对比效率高得多。
另一个实用做法是用RDIE做训练中的早期停止判断。超分模型训练到后期,PSNR和SSIM可能还在缓慢上升,但重建质量实际已经开始过拟合训练集的纹理模式。此时在验证集上每5个epoch算一次RDIE的entropy_diff,连续10次没有下降,就停掉:
best_entropy_diff = float('inf') best_epoch = 0 patience = 10 wait = 0 for epoch in range(total_epochs): train_one_epoch() sr_imgs = inference(val_loader) diff = 0.0 for hr, sr in zip(val_hr_list, sr_imgs): result = rdie_score(hr, sr) diff += result['entropy_diff'] diff /= len(val_hr_list) if diff < best_entropy_diff: best_entropy_diff = diff best_epoch = epoch wait = 0 save_checkpoint(model.state_dict(), f'best_rdie_epoch{epoch}.pth') else: wait += 1 if wait >= patience: break逻辑说明:这段代码用early stopping的经典逻辑,在验证集上累计RDIE的entropy_diff,取最小时作为最佳断点。和只看PSNR相比,RDIE对“细节过度平滑”的惩罚更直接,停下来的模型通常在感知质量上更符合直观判断。
还有一个可视化技巧:把sr_ent与hr_ent的差值画成热力图,能直接看出模型在哪些区域翻车。当熵图尺寸比较大时,注意设置x轴刻度步长,否则matplotlib的默认刻度会挤成一团看不清——用plt.xticks(np.arange(0, w, step=8))这类方式控制密度,观察值大于0的色块是模型产生了虚假细节,小于0的色块是细节丢失。
这套RDIE实现我已经沿用了一段时间,最大的感受是:它不能替代主观观察,但能把“哪里细节修复得不对”这个问题从黑匣子变成一个可定位的色块分布图。选超分模型时,先看PSNR,再用RDIE看细节,两者都过关才敢上生产环境。希望这些实现经验能帮你在超分评测里少走弯路。
本文还有配套的精品资源,点击获取