054、Real-ESRGAN 实战:基于退化模型的盲超分训练与推理全流程
054、Real-ESRGAN 实战基于退化模型的盲超分训练与推理全流程从一张模糊到怀疑人生的照片说起上个月帮朋友处理一张老照片原图是2000年初的数码相机拍的分辨率低、噪点多、还有明显的JPEG压缩痕迹。我试了EDSR、RCAN这些经典模型结果要么把噪点放大成纹理要么把边缘修得跟塑料一样。朋友说“还不如我自己用美图秀秀”。这句话让我意识到真实世界的退化远比我们训练集里模拟的复杂——模糊、噪声、压缩伪影、下采样混在一起而且你根本不知道它们的具体参数。这就是盲超分要解决的问题。Real-ESRGAN的核心思路很直接既然真实退化未知那就先学一个能模拟真实退化的模型再用这个模型生成训练数据最后用ESRGAN的对抗训练框架做超分。整个过程像是一个“以毒攻毒”的闭环。退化模型别再用简单的双三次下采样了传统超分训练喜欢用双三次插值做下采样这在学术benchmark上表现不错但真实场景里几乎没有纯双三次退化的图像。Real-ESRGAN的退化模型包含两个核心阶段第一阶段模糊下采样# 这里踩过坑模糊核不能只用高斯要混合各向异性的运动模糊deffirst_order_degradation(img,sf4):# sf是缩放因子别写成2我一开始就写错了kernelgenerate_kernel(kernel_size21,sigma_range[0.2,3])# 注意sigma_range的上限不能太大否则图像会变成一团糊img_blurcv2.filter2D(img,-1,kernel)# 下采样用最近邻别用双三次因为我们要模拟真实相机的采样过程h,wimg_blur.shape[:2]img_downimg_blur[::sf,::sf]returnimg_down第二阶段噪声JPEG压缩# 真实噪声是异方差性的别用固定的高斯噪声defsecond_order_degradation(img):# 噪声强度随亮度变化暗部噪声更大noise_levelnp.random.uniform(1,15)# 这里有个trick用泊松-高斯混合噪声更接近真实传感器noisenp.random.poisson(img/255.0*10)*255.0/10-img noisenp.random.normal(0,noise_level/255.0,img.shape)img_noisynp.clip(imgnoise,0,255)# JPEG压缩质量随机选别固定为95qualitynp.random.randint(30,95)encode_param[int(cv2.IMWRITE_JPEG_QUALITY),quality]_,encimgcv2.imencode(.jpg,img_noisy,encode_param)img_jpegcv2.imdecode(encimg,1)returnimg_jpeg这两个阶段可以随机组合也可以只用一个阶段。论文里建议用“随机顺序”来模拟更复杂的退化链。训练数据生成自己动手丰衣足食有了退化模型接下来就是生成训练对。这里有个关键点不要用ImageNet直接下采样。因为ImageNet本身就有压缩伪影再退化一次会放大这些伪影。我用的方案是收集高分辨率素材DIV2K、Flickr2K、还有自己爬的一些4K壁纸用退化模型生成低分辨率图像保存为LMDB格式加速读取# 别这样写一次性把所有数据加载到内存# 正确做法用迭代器逐条处理defgenerate_paired_dataset(hr_dir,lr_dir,num_pairs10000):fori,hr_pathinenumerate(glob.glob(hr_dir/*.png)):ifinum_pairs:breakhrcv2.imread(hr_path)# 这里踩过坑hr图像尺寸必须大于128x128否则退化后太小ifhr.shape[0]256orhr.shape[1]256:continuelrdegradation_pipeline(hr)cv2.imwrite(f{lr_dir}/lr_{i:05d}.png,lr)cv2.imwrite(f{lr_dir}/hr_{i:05d}.png,hr)网络架构ESRGAN的升级版Real-ESRGAN的生成器基于RRDBResidual-in-Residual Dense Block但做了两个重要改动去掉BN层BN层在超分任务中会引入伪影特别是当训练和测试的退化分布不一致时。直接去掉BN用PixelShuffle做上采样。加入SFT层Spatial Feature Transform可以根据退化信息调整特征。这里我踩过一个坑——SFT的输入是退化特征图不是原始图像。classRRDB(nn.Module):def__init__(self,nf64):super().__init__()# 别用nn.BatchNorm2d这里踩过坑self.bodynn.Sequential(DenseBlock(nf),DenseBlock(nf),DenseBlock(nf),nn.Conv2d(nf,nf,3,1,1))defforward(self,x):returnxself.body(x)*0.2# 残差缩放因子0.2别写成0.1判别器沿用U-Net结构的PatchGAN但输出是单通道的判别图不是标量。这样能更好地保留局部纹理细节。训练策略对抗训练不是万能的训练过程分两个阶段第一阶段预训练生成器只用L1损失训练生成器学习率1e-4batch size 16。这一步的目的是让生成器学会基本的超分能力避免对抗训练初期的不稳定。# 别这样写直接上对抗损失# 正确做法先预训练100k iterationsforstepinrange(100000):lr,hrnext(train_loader)srgenerator(lr)loss_l1L1Loss(sr,hr)optimizer.zero_grad()loss_l1.backward()optimizer.step()第二阶段联合训练加入GAN损失和感知损失。这里有个trick感知损失用VGG19的relu5_4层别用relu4_3后者对纹理细节不够敏感。# 这里踩过坑GAN损失的权重不能太大否则会引入伪影loss_gan0.1*GANLoss(discriminator(sr),True)loss_percep0.01*PerceptualLoss(sr,hr)loss_l10.01*L1Loss(sr,hr)total_lossloss_ganloss_perceploss_l1学习率调度用余弦退火但别从1e-4直接降到0我习惯保留一个最小学习率1e-7防止后期震荡。推理部署别直接拿训练代码跑训练好的模型在推理时有几个容易忽略的细节输入图像预处理先做一次简单的去噪再用模型。因为Real-ESRGAN对噪声敏感特别是低光照图像。多尺度测试把输入图像缩放到0.8、1.0、1.2倍分别推理后取平均。这个操作能提升PSNR约0.3dB但速度会慢3倍。边界处理模型对图像边界的效果较差建议先padding 16个像素推理完再裁剪掉。definference(model,img,scale4):# 别这样写直接输入原始尺寸# 正确做法先paddingh,wimg.shape[:2]pad_h(16-h%16)%16pad_w(16-w%16)%16img_padnp.pad(img,((0,pad_h),(0,pad_w),(0,0)),modereflect)# 多尺度测试sr_list[]forsin[0.8,1.0,1.2]:img_resizedcv2.resize(img_pad,None,fxs,fys)srmodel(torch.from_numpy(img_resized).cuda())srcv2.resize(sr,(w*scale,h*scale))sr_list.append(sr)sr_avgnp.mean(sr_list,axis0)returnsr_avg[:h*scale,:w*scale]踩坑记录那些年我交过的学费退化模型的随机种子训练和测试时退化模型的随机种子必须固定否则结果不可复现。我吃过这个亏跑了三天发现结果对不上。JPEG压缩的陷阱OpenCV的imencode默认保存为BGR格式而训练时用的是RGB。这个bug让我浪费了两天。显存爆炸Real-ESRGAN的生成器有16个RRDB每个RRDB有3个DenseBlock参数量约16M。在1080Ti上batch size只能开到4。建议用梯度累积模拟更大的batch。对抗训练的不稳定判别器训练太快会导致生成器梯度消失。我加了谱归一化SpectralNorm到判别器的每一层效果立竿见影。个人经验什么时候该用Real-ESRGAN如果你的任务满足以下条件Real-ESRGAN是首选退化类型未知且复杂老照片、监控视频、网络图片对视觉质量要求高不追求极致PSNR有足够的高分辨率训练数据至少1000张以上但如果你的场景退化类型已知且固定比如固定的下采样高斯噪声用ESRGAN或者RCAN就够了Real-ESRGAN的退化模型反而会引入不必要的复杂度。另外别指望Real-ESRGAN能处理所有退化。我试过用它修复严重过曝的照片结果把过曝区域修成了奇怪的纹理。这种情况下先做曝光校正再超分效果更好。最后说一句代码写完了记得跑一遍完整的测试流程别像我一样训练了三天才发现数据加载的路径写错了。