简介:RealESRGAN退化代码包面向图像超分辨率与GAN模型研究者,专注于真实图像退化过程的模拟,涵盖模糊、噪声、JPEG压缩等关键环节,为训练高分辨率重建模型提供标准数据预处理方案。压缩包共20个文件,含9个Python源码、9个编译后的pyc文件及2张样例图片,整体仅70KB,结构轻量清晰,便于直接阅读与调用。源码部分实现了退化模型、DiffJPEG差分压缩模块、图像处理与变换工具、日志与分布式工具等,并配有realesrgan_degration.py入口脚本,可快速生成低分辨率训练对,复现论文数据流程。当前已有860人学习,适合希望搭建RealESRGAN训练数据流水线或深入理解图像退化原理的开发者参考。
1. 为什么我盯上了RealESRGAN的退化代码
做图像超分的人大概都经历过这个场景:用官方放出来的预训练模型跑自己收集的图,效果惊艳,细节清晰得像是原图本来就长那样;但一旦自己从零开始训练一个超分模型,无论怎么调loss、调学习率,出来的结果总差一口气——边缘发虚、纹理糊成一片、甚至出现奇怪的水波纹。我以前一直以为是模型结构或者训练策略的问题,直到有一次把官方训练日志里的参数全部照抄、连batch size都换成一样,模型效果仍然不对劲,我才意识到问题可能出在喂给模型的数据上,具体来说,是训练时用来生成低分辨率图像的退化代码。
RealESRGAN的退化代码是这套方案的核心资产。很多人都知道它引入了"高阶退化模型",但绝大多数教程只停留在"用官方代码训练"的层面,很少有人把degradation部分单独拆出来讲清楚。这篇文章就是我反复读源码、改参数、跑实验之后的一份笔记:它到底是怎么把一张高清图一步步"折腾"成低清图的,改哪里会影响什么,以及我踩过的几个非常隐蔽的坑。
这篇内容适合两类人:一类是准备拿RealESRGAN做二次开发、想要自己控制退化方式的研究者;另一类是本身做图像复原、想理解"盲超分训练数据到底该长什么样"的人。如果你只是拿预训练模型推理,这篇文章也能帮你理解为什么模型对某些图效果好、对另一些图效果差。
2. 退化模型的全链路拆解:一张高清图是如何变成低清图的
2.1 训练时喂给模型的那张"低清图"是怎么来的
RealESRGAN的退化流程集中在basicsr/data/degradations.py里,整体思路不是从一个固定的公式里生成低清图,而是把多个退化操作按随机顺序、随机参数串起来,模拟真实世界中一张图从"清晰"到"模糊+噪声+压缩"的物理过程。
核心数据结构是一个OrderedDict,用来按顺序存放每一步退化操作。代码里把这些操作组织成两个退化阶段(degradation stage),每个阶段内部又包含四类基本操作:
- 模糊:高斯模糊核 + sinc模糊核,模拟镜头失焦、运动模糊等
- 下采样:用插值方式把分辨率降下来
- 噪声:高斯噪声或泊松噪声,模拟传感器噪声
- JPEG压缩:模拟图片在存储、传输过程中产生的压缩伪影
这里有一个非常关键的细节:每一步是否执行、执行强度是多少,都是由随机数决定的。也就是说,每一次读取同一张高清原图,生成的低清图都不完全一样。这种"随机性"是盲超分模型能泛化到真实世界的关键,但也是后面调试时最容易让人抓狂的地方——你对着同一张图调了半天参数,下一次运行它又变成另一种退化方式了。
2.2 第一阶段的模糊、下采样、噪声与JPEG压缩
我用伪代码形式把第一阶段的核心逻辑整理如下,实际上对应degradation.py中degradation函数的前半段:
# 第一阶段:随机选取模糊核 kernel1 = random_mixed_kernels( kernel_list=['iso', 'aniso', 'generalized_iso', 'generalized_aniso', 'plateau'], kernel_prob=[0.45, 0.25, 0.12, 0.03, 0.15], kernel_size=21, sigma_x_range=[0.2, 3], sigma_y_range=[0.2, 3], angle_range=[-math.pi, math.pi], blur_sigma_range=[0.3, 3], betag_range=[0.5, 4], betap_range=[1, 2] ) # 对原图做模糊 img = filter2d(img, kernel1) # 随机选择一个下采样方式 upscale = random.choice([1, 2, 3, 4]) mode = random.choice(['area', 'bilinear', 'bicubic']) img = imresize_np(img, 1 / upscale, mode) # 加噪声 if random.random() < 0.5: img = add_gaussian_noise(img, sigma_range=[1, 30]) else: img = add_poisson_noise(img, lam_range=[0.05, 3]) # JPEG压缩 if random.random() < 0.8: img = add_jpg_compression(img, quality=[30, 95])这里有几个参数值得展开说。kernel_size=21表示模糊核的半径大小,核太小了模拟不了严重的失焦效果,核太大了计算量又直线上升。sigma_x_range和sigma_y_range控制模糊核的长宽比,当两个方向的sigma差异很大时,生成的就是明显的运动模糊效果,而不是均匀的高斯模糊。kernel_prob里的概率分布决定了五种模糊核类型各自出现的频率,其中plateau核的顶部比较平坦,模拟的是复杂镜头系统产生的中间平顶型点扩散函数。
下采样这一步很多人容易忽略它的重要性。代码里upscale是在1到4之间随机选一个整数,也就是说每次降分辨率倍数可能不同。注意这里有个细节:如果原图是256x256,随机选中upscale=2,下采样后变成128x128,这时候后面的噪声和JPEG压缩都作用在这张128x128的图上。这个顺序其实很有讲究——真实的图像退化过程中,噪声和压缩伪影是在低分辨率图像上产生的,而不是在原始高清图上先加噪再缩小。
2.3 第二阶段:为什么还要再来一轮
第一阶段做完之后,图片已经"糊了一半"。但RealESRGAN的高阶退化模型并没有就此结束,而是对同一个结果再做一遍"模糊→下采样→噪声→JPEG"流程,只是这次的模糊核选取范围、噪声强度、压缩质量都与第一阶段不同:
# 第二阶段 kernel2 = random_mixed_kernels( kernel_list=['iso', 'aniso', 'generalized_iso', 'generalized_aniso', 'plateau'], kernel_prob=[0.45, 0.25, 0.12, 0.03, 0.15], kernel_size=21, sigma_x_range=[0.2, 1.5], # 比第一阶段更小的范围 sigma_y_range=[0.2, 1.5], angle_range=[-math.pi, math.pi], blur_sigma_range=[0.3, 2], betag_range=[0.5, 4], betap_range=[1, 2] ) img = filter2d(img, kernel2) # 第二步下采样 upscale = random.choice([1, 2, 3, 4]) mode = random.choice(['area', 'bilinear', 'bicubic']) img = imresize_np(img, 1 / upscale, mode) # 第二步噪声 if random.random() < 0.5: img = add_gaussian_noise(img, sigma_range=[1, 25]) else: img = add_poisson_noise(img, lam_range=[0.05, 2]) # 第二步JPEG压缩 if random.random() < 0.8: img = add_jpg_compression(img, quality=[30, 95])第二阶段结束之后,代码还会做一次最终的sinc模糊,用sinc核把图像再做一次卷积。这是一个非常微妙的设计:sinc核会让图像边缘产生振铃效应,模拟真实世界中一些光学系统的截止频率特性。这种模糊在视觉上看起来非常"不自然",但恰恰是它能帮助模型在真实低质量图像上表现得更好——因为真实图像里确实存在各种奇怪的振铃和过冲。
最后还有一个容易被忽略的操作:add_random_color_jitter。这一步会随机调整图像的亮度、对比度、饱和度和色相,模拟不同拍摄设备、不同环境光照下的色彩差异。如果没有这一步,模型可能会依赖图像的固定色彩分布来做超分,一旦遇到真实图像色彩偏移就失效了。
2.4 为什么要绕这么大一圈
我当时第一次看完这套流程时,心里的疑问是:既然最终目标就是得到一张低分辨率图像,为什么不直接对高清图做一次随机双三次下采样就完事了?
答案是:真实世界的退化过程从来不是单一、确定的函数。一张图片从拍摄到最终显示,经历了镜头光学模糊、传感器噪声、图像处理芯片的缩放算法、存储压缩、传输再压缩等多个环节。如果只用"双三次下采样"这一种方式生成低清图,模型学习到的逆过程也只是"双三次下采样的逆操作",遇到真实图像时就会严重失配。
RealESRGAN把退化模型设计成两阶段、随机组合的方式,本质是想让训练数据的退化分布尽量覆盖真实世界的退化分布。两阶段退化可以组合出很多单阶段无法表达的退化模式,比如"先轻微模糊再压缩"和"先压缩再强模糊"产生的伪影特征是完全不同的。这也是它在真实图像盲超分任务上明显优于SRGAN、EDSR这些老方案的核心原因之一。
3. 退化代码里最容易踩坑的三个细节
3.1 随机种子的坑:退化代码的随机性管理
RealESRGAN的所有随机退化参数都不是用Python的random模块,而是用np.random.RandomState实例来管理的。在degradation.py的Degradation类中,初始化时会创建多个RandomState,分别服务于模糊核生成、噪声生成、JPEG质量选择等不同环节。
这个设计是有意为之的。如果用全局随机种子,那么数据加载线程之间的随机状态会互相干扰,导致同一份数据在不同epoch里退化的随机性分布不均匀;而且多线程训练时,每个线程拿到的随机序列完全不可控,复现实验会变得极其困难。用独立的RandomState实例后,你可以精确控制"模糊核这次选哪个""噪声这次加多大",只需要给对应实例传入不同的seed即可。
调试时最建议的做法是:先把degradation.py里所有RandomState的种子固定成一个值,然后把你关心的那张测试图跑一遍退化流程,打印每一步的中间结果和参数。等你确定退化策略符合预期了,再放开随机性去训练。否则你永远不知道当前效果不好是模型的问题还是退化参数恰好抽到了极端情况。
3.2 边界效应:卷积模糊后图像边缘的"黑边"问题
filter2d函数在实现时默认对图像边缘做了扩展(padding),但边缘扩展的方式会影响最终的退化效果。RealESRGAN用的是reflect填充方式——把图像边缘的像素像镜子一样反射过来作为卷积的边界。这种方式比补零填充要自然,不会在边缘引入明显的突兀暗边。
但我自己试过之后发现它有个问题:当模糊核比较大、或者图像本身尺寸较小时,reflect填充会让模糊后的图像边缘出现"重影"。比如一张64x64的小图,用size为21的核做模糊,边缘大约有10个像素受影响,对64x64的图来说已经是很大一片区域了。
如果你在训练时发现模型在图像边缘区域的重建效果总是不好,建议检查一下是不是退化代码的边界处理方式造成的。可以做一个简单的消融实验:同一张图,分别用reflect和复制的边界填充方式生成退化图,再比较模型对边缘区域的重建误差。如果差异明显,说明需要换边界策略或者对训练图像做随机裁剪时避开边缘。
3.3 sinc核的数值稳定性问题
sinc核的数学形式是sinc(x) = sin(pi*x) / (pi*x),在x=0处需要特殊处理,否则会出现除零错误。RealESRGAN代码里有一个kernel_sinc函数,用来生成一维sinc核,再通过外积得到二维核:
def kernel_sinc(kernel_size, omega, freq): x = np.arange(-kernel_size // 2 + 1, kernel_size // 2 + 1) y = np.sinc(freq * x / omega) # 直接用numpy的sinc函数 y /= y.sum() return y这里直接用np.sinc已经处理了x=0时的特殊情况(np.sinc(0)=1),所以不会真的崩溃。真正容易出问题的是freq参数设置得过大会导致核出现负值。sinc核有负瓣是物理上的正常现象(对应光学系统的振铃),但负值核作用在图像上会降低图像的整体亮度范围。如果生成退化图后你发现图像整体变暗、或者边缘出现一圈圈明显的过冲纹路,很可能就是sinc核的频率参数设置过大了。
我的经验是:如果需要模拟轻微的光学振铃,把omega设成大于freq * 2的值;如果你想完全避免振铃伪影,就把sinc核这一项从退化流程里去掉,只用高斯核,操作起来就是把kernel_list中的'sinc'项概率设为0。
4. 改造成自定义退化策略的实操记录
4.1 调整模糊核参数:让模型适配特定场景
官方默认的模糊核sigma范围是[0.2, 3],这个范围覆盖了大部分真实场景的模糊程度。但我遇到的场景是:要做的是手机拍摄文档的超分,这种场景下的模糊主要是轻微的手抖和低光噪声,几乎不会出现严重的大核失焦。
所以我做了两处修改:一是把sigma_x_range和sigma_y_range整体缩小到[0.2, 1.5];二是把generalized_iso和plateau这两种核的概率调高,因为文档拍摄中的镜头模糊通常更接近高斯型,而不是那种长尾的运动模糊型核。改完后再训练,模型对文档图像的文字边缘重建明显更锐利了。
这个经验可以推广:不要盲目照抄官方的退化参数。先分析你的目标数据到底存在什么样的退化模式,再反过来设定退化代码的概率分布。官方参数只是对ImageNet这类通用图像最鲁棒的配置,未必是你业务场景的最优解。
4.2 单独控制JPEG压缩强度:老图片修复的一个隐藏技巧
官方代码中JPEG压缩的quality范围是[30, 95],这意味着大约有相当比例的退化图会经受非常强的压缩。但对于老照片修复任务来说,真正的退化主力根本不是JPEG伪影,而是胶片颗粒、划痕和褪色。
针对老照片,我做了两个实验对比。第一个实验保持官方参数不变,第二个实验把JPEG quality拉到[60, 95]、同时把高斯噪声的sigma范围从[1, 30]改成[1, 15]。两组实验训练相同的模型结构,结果第二组在老照片验证集上的PSNR略高,更重要的是主观视觉上颗粒感更自然。原因是JPEG压缩和高斯噪声都会给图像引入高频伪影,如果两种伪影叠加过强,模型会把它们统一误判为"需要保留的细节",结果重建出来的图像表面总是有一层脏兮兮的纹理。
4.3 简化退化策略:什么时候应该砍掉第二阶段
并不是所有任务都需要两阶段退化。如果你的任务是对已经相对干净的图像做轻度超分,比如扫描文档、渲染图、动漫截图,两阶段退化反而会让训练数据与真实数据差距过大。模型花了一部分能力去学习"消除复杂伪影",而这些能力在测试时根本用不上。
我做过一次消融实验:只保留第一阶段退化和最终的sinc模糊,去掉第二阶段,在动漫图像超分任务上训练。结果模型在动漫测试集上的主观效果明显更干净,因为动漫图像本身几乎不含传感器噪声,两阶段退化生成的过度噪声会让模型把原始干净的边缘也当成噪声抹掉。所以,退化模型的复杂度要和目标数据的复杂度匹配,不是越复杂越好。
5. 那些"看起来没问题但实际效果很不对劲"的退化配置
5.1 模糊核太小,退化图看起来还清晰
一个非常隐蔽的坑是:当你把sigma_x_range上限调小时,生成的退化图看起来依然清晰,视觉上几乎看不出模糊。你会觉得"这不就等效于没退化吗"。但实际上,哪怕是很小的模糊核,只要在训练数据里一直存在,模型就会把"略有模糊的图像"也当成输入分布的一部分。
问题在于,如果退化强度太小,模型在训练时很容易走捷径:它可能学会"反正输入已经很清晰了,我直接做一个接近恒等映射的输出就行",从而放弃真正学习超分能力。测试时一旦遇到模糊严重的真实图像,模型就彻底失效。我的判断标准是:生成的退化图,人眼看起来应该能明显感觉到画质下降,但不至于丢失所有细节。如果退化图看起来像原图的轻微压缩版本,说明强度不够。
5.2 噪声类型与真实场景错配:高斯噪声不是万能的
RealESRGAN中高斯噪声和泊松噪声是二选一、各50%概率。高斯噪声的强度分布与像素值无关,而泊松噪声的信号相关——亮部噪声更明显。如果你的目标数据是夜间监控视频,暗部噪声远强于亮部,那么纯高斯噪声模型会高估亮部的噪声,低估暗部的噪声。
这种错配很难通过调参解决,因为实际传感器噪声往往不是纯粹的泊松或高斯,而是两者的混合。我当时在低光图像超分任务上做了一组对比实验:把噪声模型改成"先加泊松噪声再加少量高斯噪声"的组合方式,在低光验证集上的主观效果显著优于只使用单一噪声类型的配置。这个改动在代码上实现起来很直接,就是把原来二选一的分支改成顺序执行两个噪声函数。
5.3 训练与测试失配:最容易被忽视的那个"坑"
最后说一个特别容易踩但很多人意识不到的坑:训练时做了两次退化,测试时输入图像只经历了一次退化。RealESRGAN的两阶段退化是为了让训练数据覆盖更复杂的退化分布,但如果你的实际应用场景很单一,两阶段退化的训练数据反而会让模型过拟合到"复杂退化"的模式,在简单退化输入上产生不必要的伪影。
我在调试一个工业视觉超分项目时遇到的真实情况是:训练时用完整的两阶段退化,模型在验证集(同样用两阶段退化生成)上指标很好,但部署到产线相机拍的图像上时,图像边缘总有一圈淡淡的振铃。排查了很久才发现,产线相机本身的退化链很简单(无压缩、低噪声、轻微模糊),而训练模型被"教育"要消除的复杂JPEG伪影在真实输入里根本不存在,模型反而在尝试消除不存在的伪影时生成了振铃。
解决办法也简单:针对部署场景单独做一组"轻量退化"训练,用单阶段退化 + 低噪声 + 高质量压缩,再对比测试效果。这类问题在论文里几乎不会被提及,实际工程里却非常常见,建议做实际项目时先把你业务的真实低清图拿来喂给退化代码生成一批同类数据,看看分布是不是真的一致。
6. 验证退化代码是否有效的两个土办法
很多时候改完退化代码,你没法立刻判断改得对不对,尤其是超分模型训练一次要好几天,不能等训练完再看效果。这里分享两个我在实践中用下来比较靠谱的验证方法。
方法一:可视化退化后的图像与原始图像的频谱差异。把一张高清图送入退化流程,保存退化前后两幅图,分别做傅里叶变换,对比频谱图。如果退化有效,你会看到高频区域明显衰减;如果退化太弱,频谱几乎重合。这个方法能比人眼更早发现退化强度是否合适。
方法二:用一个小模型快速验证退化数据的"可学习性"。拿一个非常小的超分网络(比如只有几层卷积的轻量模型),在退化代码生成的数据上训练50个epoch,然后对另一批退化图做测试。如果小模型都能在这批数据上取得不错的超分效果,说明退化代码生成的映射关系是合理、可逆的;如果小模型完全无法收敛,要么是退化强度过大、要么是退化过程破坏了太多信息,这时候需要对参数做回调。
这两个方法都不需要训练完整的大模型,半天时间就能出结论,节省的却可能是一整周的训练资源和调试时间。
回到开头说的那个问题——如果你发现自己的超分模型怎么训练都不对劲,建议先别急着换loss或者换backbone,把退化代码完整地跑一遍,拿几张图认真看看低清图长什么样、频谱结构是否合理、噪声分布是否符合目标场景。RealESRGAN最值钱的不是那几层网络结构,而是这套精心设计的退化代码,学会调整和验证它,你才算是真正掌握了自己训练数据的关键。
本文还有配套的精品资源,点击获取