☰
VDSR超分复现实战:caffe梯度裁剪与HDF5数据生成全解析
2026/10/13 11:35:23 网站建设 项目流程

简介:VDSR-caffe 是基于 2016 年 CVPR 论文《Accurate Image Super-Resolution Using Very Deep Convolutional Networks》的 Caffe 开源实现,该工作通过 20 层卷积网络与残差学习显著提升超分重建精度,适合图像超分辨率方向的研究者、学生复现论文或开展实验对比。压缩包共 286 个文件、约 9.75MB,代码主体包括约 119 个 MATLAB 脚本、25 个 CUDA 源码文件及配套 C++/HPP 工程文件,并配有 prototxt 网络定义、caffemodel 预训练权重、Matconvnet 测试脚本、常见超分基准测试图和 BibTeX/TeX 说明文档;同时包含 Windows/Unix 编译配置,基本覆盖从模型搭建、训练、测试到评估的完整流程。已有 264 人学习下载。借助这份资源,读者既可逐层查看 VDSR 网络结构和残差学习实现,也可直接加载预训练权重快速验证超分效果;配套脚本还适合替换数据层、调整损失函数或对比不同放大倍数,为科研复现、竞赛准备和课程设计提供可直接运行的基础工程。

1. VDSR-caffe 是超分复现的试金石:不踩梯度裁剪的坑,就跑不通这个工程

VDSR-caffe 是把残差学习超分网络 VDSR 在 caffe 框架下完整实现的一套工程,包含模型定义、HDF5 数据生成、solver 配置和测试脚本。很多人第一次接触它,以为难点是 20 层卷积的网络结构,实际跑起来才发现,真正的门槛是训练策略:base_lr 要拉到 0.1,但如果不配合梯度裁剪,loss 会直接冲到 NaN;一旦 clip_gradients 配置对了,收敛速度又能比普通超分网络快一个量级。这篇文章从 prototxt 怎么写、训练数据怎么生成、solver 参数怎么调,到测试时 PSNR 怎么算、常见踩坑在哪里,把 VDSR-caffe 从零跑通的每个关口都拆开讲。适合正在复现超分论文、或者想在 caffe 里快速验证超分想法的算法工程师。这套工程跑通一次,你对残差学习、梯度裁剪和数据退化的理解会比读十遍论文都扎实。

2. VDSR 网络和 caffe 模型定义:20 层卷积的 prototxt 怎么搭

2.1 残差学习为什么撑起了 VDSR 的效果

VDSR 的核心不是“深”,而是“深 + 残差学习”。输入是低分辨率图经过 bicubic 放大到目标尺寸后的结果,网络输出的不是完整的高分辨率图,而是高分辨率图与输入之间的差值,也叫残差图。残差图里大部分像素值接近 0,只有边缘和纹理区域有非零响应,网络要拟合的目标比完整图像稀疏得多,收敛难度直接降了一个级别。

另一个关键点是感受野。20 层 3x3 卷积堆叠起来,每层的 pad 设为 1,feature map 尺寸不变,最后一层输出的感受野覆盖了输入图上相当大的区域。超分任务里,一个像素的重建结果要依赖周围像素的纹理和结构信息,感受野越大,能利用的上下文越充分。这也是为什么 VDSR 用 20 层而不是 SRCNN 那样只有 3 层。caffe 的静态图机制对这种固定层数、固定结构的网络非常合适,模型文件写清楚每一层的连接关系,编译一次之后前向和反向的效率都很稳定,这也是 VDSR-caffe 这个方向被很多人选作入门复现项目的原因。

2.2 从零生成 VDSR 的 train_val.prototxt

与其手写 20 层重复的卷积层,不如用一个 python 脚本生成模型文件,后续要改层数或者改通道数,只需要改几个参数重新跑一遍。下面是生成 VDSR 训练网络的最小脚本,输出就是 caffe 可以直接读取的 train_val.prototxt。

# gen_model.py: 生成 VDSR 的 train_val.prototxt import os def conv_relu(name, bottom, top, num_output=64, last=False): """生成一层卷积和一层ReLU的prototxt文本 last=True 时输出单通道残差,不加ReLU""" lines = [] lines.append(f'layer {{ name: "{name}_conv" type: "Convolution" bottom: "{bottom}" top: "{top}"') lines.append(f' convolution_param {{ num_output: {num_output if last else 64} kernel_size: 3 pad: 1') lines.append(' weight_filler { type: "xavier" } bias_filler { type: "constant" value: 0 } }') lines.append(' param { lr_mult: 1 decay_mult: 1 } }') if not last: lines.append(f'layer {{ name: "{name}_relu" type: "ReLU" bottom: "{top}" top: "{top}" }}') return "\n".join(lines) depth = 20 # VDSR 的卷积层数,改成 16 或 24 就是变体 lines = [] lines.append('layer { name: "data" type: "HDF5Data" top: "data" top: "label"') lines.append(' hdf5_data_param { source: "train_hdf5.txt" batch_size: 64 }') lines.append(' include { phase: TRAIN } }') prev = "data" for i in range(1, depth + 1): out = f"r{i}" if i == depth else f"c{i}" lines.append(conv_relu(f"conv{i}", prev, out, last=(i == depth))) prev = out lines.append('layer { name: "loss" type: "EuclideanLoss" bottom: "r20" bottom: "label" top: "loss" }') with open("train_val.prototxt", "w") as f: f.write("\n\n".join(lines)) print(f"train_val.prototxt generated, depth={depth}")

这个脚本生成的网络里,每一层卷积的 num_output 都是 64,这是 VDSR 的固定宽度,不是随便定的。64 个卷积核在边缘、纹理、平滑区域这些特征维度上够用,再往上加通道,参数量翻倍但精度收益很小。pad 必须显式设为 1,这样 3x3 卷积不改变空间尺寸,20 层下来输入输出分辨率一致,残差可以直接和输入相加。最后一个卷积层输出 1 个通道,得到的就是单通道残差图。bias 初始化为 0 是因为残差图的均值接近 0,一开始不偏不倚,训练初期的 loss 会比较低。

数据层用的是 HDF5Data,top 有两个:data 是放大后的低分辨率图,label 是残差图。caffe 的 HDF5Data 层要求数据预先做好归一化,它不会像 ImageData 层那样边读边做 scale,所以数据预处理必须在前置脚本里完成。生成 prototxt 之后,可以用 caffe 的 net 工具做一次前向检查,确认每一层的 blob shape 都是预期的 1x64xHxW,避免训练到一半才发现维度不匹配。

3. 从图像到 HDF5:VDSR-caffe 训练数据生成脚本与 patch 策略

3.1 训练数据长什么样:LR、HR 配对和残差 label 的生成逻辑

VDSR 的数据流和分类任务不太一样,它不需要人工标注,而是从高分辨率自然图像里“自我制造”训练样本。做法是:取一张清晰的大图,先从上面随机裁剪一块 HR patch,然后用 bicubic 插值把这块 patch 缩小到原来的 1/scale,再用同样的 bicubic 算法放大回原尺寸,得到的就是和网络输入尺寸一致的 LR 图。网络输入是这张 LR 图,label 是 HR patch 减去 LR 图得到的残差。

这里有个容易想歪的点:有人会把“先缩小再放大”写成“直接放大整张大图”再裁剪,这不对。如果先放大整图再裁剪,裁出来的区域没有对应的原始 HR 信息,label 就失真了。正确做法是先裁 HR patch,再在 patch 上做退化,这样同一个空间区域的 HR 和 LR 严格对齐。退化过程必须全程保持浮点精度,如果用 uint8 中间变量存一次缩小结果,量化误差会被第二次放大进一步放大,最终残差 label 里全是噪声,训练出来的模型重建效果会发糊。

3.2 一键生成 HDF5 训练数据的 python 脚本

HDF5 是 VDSR-caffe 比较顺手的训练数据格式,caffe 原生支持 HDF5Data 层,而且数据是随机读取的,不需要像 LMDB 那样用专门工具转换。下面这个脚本读一个目录里的高清图,随机裁剪 patch、做多尺度退化、旋转翻转增强,最后写成一个 h5 文件。

# generate_hdf5.py: 生成 VDSR 训练用的 HDF5 数据 import numpy as np import cv2 import h5py import glob import os hr_dir = "./data/hr" h5_path = "./data/train.h5" patch_size = 41 # 网络输入patch尺寸,VDSR论文经典值为41 patches_per_image = 64 # 每张图随机裁剪的patch数 scale_list = [2, 3, 4] # 多尺度退化,模拟2x/3x/4x超分 imgs = [] for f in sorted(glob.glob(os.path.join(hr_dir, "*.png"))): img = cv2.imread(f) img = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)[:, :, 0] # 只用Y通道 imgs.append(img.astype(np.float32) / 255.0) total = len(imgs) * patches_per_image h, w = imgs[0].shape assert h >= patch_size and w >= patch_size with h5py.File(h5_path, "w") as h5: data_ds = h5.create_dataset("data", (total, 1, patch_size, patch_size), dtype=np.float32) label_ds = h5.create_dataset("label", (total, 1, patch_size, patch_size), dtype=np.float32) idx = 0 for img in imgs: H, W = img.shape for _ in range(patches_per_image): scale = np.random.choice(scale_list) lr_size = patch_size // scale x = np.random.randint(0, H - patch_size + 1) y = np.random.randint(0, W - patch_size + 1) hr_patch = img[x:x+patch_size, y:y+patch_size] # 模拟退化: 先缩小到lr_size,再放大回patch_size lr_patch = cv2.resize(hr_patch, (lr_size, lr_size), interpolation=cv2.INTER_CUBIC) lr_patch = cv2.resize(lr_patch, (patch_size, patch_size), interpolation=cv2.INTER_CUBIC) # 旋转翻转增强: 4个旋转角度 x 水平翻转 k = np.random.randint(0, 4) flip = np.random.rand() > 0.5 hr_aug = np.rot90(hr_patch, k) lr_aug = np.rot90(lr_patch, k) if flip: hr_aug = hr_aug[:, ::-1] lr_aug = lr_aug[:, ::-1] residual = hr_aug - lr_aug # 残差作为label data_ds[idx, 0] = lr_aug label_ds[idx, 0] = residual idx += 1 # caffe的HDF5Data层通过文本文件指定h5路径,写绝对路径最稳 with open("train_hdf5.txt", "w") as f: f.write(os.path.abspath(h5_path) + "\n") print(f"done, total patches={total}, saved to {h5_path}")

脚本里几个参数值得细说。patch_size 取 41,是因为 VDSR 的理论感受野是 41x41,patch 比感受野小会让网络在边界处利用不到完整上下文,比感受野大又浪费计算。scale_list 设置成 [2, 3, 4],每次随机抽一个,这样同一个模型能同时学会处理不同放大倍率,这就是论文里的多尺度训练。退化用 INTER_CUBIC,也就是 bicubic,训练和测试必须统一用这个插值方式,否则就是典型的训练测试不一致,后面 PSNR 会莫名低一截。旋转翻转增强把每张 patch 变成最多 8 个变体,小数据集下能把有效样本量撑起来,但对超分任务来说,翻转增强不能在退化之后做,否则放大伪影的方向会和真实退化不匹配,我一般始终在退化之后做翻转,这样网络见到的退化模式仍然是真实的。

3.3 为什么只用 Y 通道:评估口径和计算开销的双重考虑

很多人第一次做超分,习惯把 RGB 三个通道都丢进网络,VDSR 只取 Y 通道是有原因的。超分效果的评价指标 PSNR 通常只在 YCbCr 色彩空间的 Y 通道上计算,Y 代表亮度,人眼对亮度细节最敏感,而 Cb、Cr 两个色度通道的信息在人类视觉系统里分辨率本来就低,直接用 bicubic 放大也几乎察觉不到差异。只在 Y 通道上训练和评估,网络专注学习亮度的高频细节,输出质量更集中,训练数据量也变成原来的三分之一。如果最终要彩色结果,常规做法是把 Cb、Cr 通道用 bicubic 放大到目标尺寸,和网络输出的 Y 通道拼回去再转回 RGB,这个流程不需要网络参与色度重建。当然,如果某个具体场景里色度伪影很碍眼,也可以在 Cb、Cr 通道上单独各训一个模型,但那种情况很少见。

这里有个注意事项:HDF5 数据层要求数据通道数和网络输入严格一致,脚本里写入的 shape 是 (total, 1, 41, 41),对应的 prototxt 里 HDF5Data 层就不需要额外指定 input dim。写完数据之后,建议先用 h5py 把第一个样本的 data 和 label 分别打印出来看看均值、范围,data 应该在 [0,1] 区间,label 应该在 [-0.5,0.5] 左右,如果 label 出现明显超过这个范围的异常值,多半是退化过程中某个 resize 步骤写错了。

4. solver 配置与训练启动:VDSR-caffe 的梯度裁剪和三组参数

4.1 完整 solver.prototxt 与逐项参数解释

VDSR-caffe 的 solver 配置是整个工程里最不能照搬其他超分项目的地方。普通 CNN 训练 base_lr 用到 0.01 已经很激进,VDSR 直接用 0.1,原因就是它为训练过程配了梯度裁剪。下面是完整可用的 solver 配置,抄的时候把 snapshot_prefix 改成自己机器的路径。

# solver.prototxt net: "train_val.prototxt" test_iter: 100 test_interval: 1000 base_lr: 0.1 lr_policy: "step" stepsize: 20000 gamma: 0.1 momentum: 0.9 weight_decay: 0.0001 clip_gradients: 0.01 display: 20 max_iter: 100000 snapshot: 5000 snapshot_prefix: "models/vdsr" solver_mode: GPU

逐项拆开看。base_lr 是 0.1,远高于常规网络的 0.01 或 0.001,这是 VDSR 收敛速度快的直接原因。但高学习率带来的问题是梯度爆炸,clip_gradients: 0.01 就是为此设置的,它的作用是限制梯度的 L2 范数上限,超过 0.01 就把梯度整体缩放回这个阈值以内。学习率和裁剪阈值之间有个经验比例关系,阈值取学习率的 1/10,也就是 0.1 配 0.01,效果最稳,这个比例是我跑多个超分模型之后确认的,不要随意改成一比一。momentum 0.9 和 weight_decay 0.0001 是 caffe 训练图像任务的通用配置,VDSR 这种回归任务对 weight_decay 更敏感,调大到 0.0005 会让残差输出偏平滑,细节丢失,不建议动。lr_policy 用 step 而不是 multistep,因为 VDSR 论文的标准做法是固定步长衰减,stepsize 设 20000 表示每 2 万次迭代学习率乘以 gamma 的 0.1,100000 次迭代总共衰减到 0.0001。如果训练数据量很小,比如只有几张图扩出来的样本,可以把 stepsize 缩小到 8000,让衰减来得更早,避免后期学习率过大在最优解附近震荡。

还有一个参数容易被忽略:test_iter 和 test_interval 要和测试数据量匹配。test_iter 设为 100,意思是每次验证时跑 100 个 batch,所以验证集的总样本数要能整除 batch_size 乘 100,否则验证阶段会报数据不足的错误。max_iter 我建议先设 100000,训练过程中每 5000 次迭代存一个快照,方便中途停下来看某一阶段的 PSNR 已经到多少。

4.2 训练启动命令与日志判断:loss 正常下降的节奏长什么样

模型和数据都准备好之后,训练启动命令很直接。

# 启动训练,日志同时打印到终端和train.log caffe train --solver=solver.prototxt --gpu 0 2>&1 | tee train.log

启动后不要急着离开,头 50 行日志能看出很多问题。正常情况是:solver 加载完网络之后,会打印每一层的 blob shape,这时候可以确认数据层输入的 channel 数是 1 而不是 3,卷积层输出的空间尺寸没变。第一次迭代的 loss 通常在 0.005 到 0.02 之间,因为网络初始输出接近 0,而残差 label 的数值也确实很小。如果初始 loss 在 1 以上,说明 label 和网络输出量纲不匹配,多半是数据归一化没做对。

之后每 display 20 次迭代打印一次 loss,正常节奏是前 1000 次迭代内从 0.01 量级掉到 0.001 量级,中间会有小波动,但不该出现突然暴涨到 10 以上的情况。如果看到 loss 在几百次迭代内变成 nan,先不要怀疑网络结构,99% 是梯度爆炸了,这时候要回去检查 clip_gradients 是否真的被 caffe 读进去了。训练日志里最开始会打印 solver 参数,用grep -i clip train.log看一眼配置是否生效,比反复调参快得多。如果只有一张显卡,solver_mode 保持 GPU 就行,caffe 也支持--gpu 0,1多卡数据并行,但 VDSR 这类模型在单卡上训练本身就不慢,多卡的 batch 分配反而容易因为样本数不是卡数整数倍而报错,前期没必要上多卡。

4.3 训练时长与模型选择:快照怎么挑

训练时长和数据规模强相关。以常见的几十张高清自然图、每张图 64 个 patch 的数据规模为例,单卡中端 GPU 上跑 100000 次迭代大概需要几小时到半天,具体取决于 patch_size 和 batch_size。max_iter 不一定要跑满,我一般每 5000 次迭代存一个快照,从 30000 次迭代开始,每隔一个快照跑一次测试集 PSNR,画一条曲线,曲线变平或者开始缓慢下降的时候就停。这个操作虽然看起来笨,但比盯着 loss 曲线猜收敛点可靠得多,因为 loss 降到某个值之后下降空间有限,PSNR 却还能稳定涨一点,两者的拐点并不完全同步。

5. VDSR-caffe 训练避坑:梯度裁剪失效、显存溢出与 PSNR 不涨排查

5.1 坑:solver 里写了 clip_gradients,loss 照样变 NaN

现象:配置文件里明明白白写着 clip_gradients: 0.01,启动训练后前几百次迭代 loss 还在正常下降,突然某一次 display 显示 loss 变成 nan,之后再也回不来。

原因:最常见的是 caffe 版本问题,部分早期编译的 caffe 分支没有把 clip_gradients 真正实现进 SGD 更新里,写了参数但内核没执行。另一种情况是训练数据里混入了 NaN,HDF5 文件中某个 patch 的禁用像素全部是 0,退化后除零产生非法值,被网络前向传播一路放大。

解决:先用命令grep -i "clip" train.log看启动日志里 solver 打印的参数块,确认 clip_gradients 有值,如果压根没打印这个字段,说明编译的 caffe 内核不支持,重新编译或换一个完整版本的 caffe 源码。数据侧的排查用 h5py 打开训练文件,检查 data 和 label 的数值是否全部有限,执行np.isfinite(ds[:]).all(),返回 False 就把那个索引打印出来对照原始图看。两个都排掉之后还有一个常用调试手段:把 base_lr 临时降到 0.01 再跑 500 次迭代,如果 loss 恢复正常,说明裁剪确实没生效,问题锁定在框架而不是数据。

5.2 坑:训练启动直接报显存溢出 Out of Memory

现象:caffe 启动命令敲下去,几秒后终端报 CUDA error: out of memory,前向传播还没跑完就退出。

原因:20 层卷积、每层 64 个 3x3 卷积核,中间还要保存每一层的激活值用于反向传播,显存占用随 patch_size 和 batch_size 线性上涨。patch_size 从 41 调到 81,单层 feature map 面积变 4 倍,20 层累积起来非常可观。很多人习惯从其他项目复制 batch_size 64 到 VDSR,直接就把显存挤爆。

解决:优先把 batch_size 降到 16 或 8,VDSR 对 batch 大小不敏感,16 和 64 的最终精度差距通常在 0.1dB 以内。patch_size 维持 41 不变,因为改小会砍掉感受野,影响重建质量。如果降 batch 还不够,在 solver 里加一行 iter_size: 4,意思是累计 4 个小 batch 的梯度再更新一次参数,等效 batch 大小不变,但显存峰值只按小 batch 算。另外检查一下 cudnn 卷积的 workspace 设置,caffe 默认 workspace 会预留较大空间,cudnn_convolution_param { engine: CUDNN }可以切换实现方式,个别驱动版本下能省出一块显存。

5.3 坑:测试集 PSNR 比论文低 2dB 以上

现象:训练过程一切正常,loss 收敛得很好,但拿测试集评估 PSNR 时,结果比论文里报告的低 1.5 到 2.5dB,看起来网络白训了。

原因:绝大多数是测试推理脚本写错了。VDSR 网络输出的是残差,最终的超分结果必须是sr = lr + residual,很多人直接把网络输出当成重建结果去算 PSNR,等于只输出了残差图,当然比 bicubic 还差。另一个常见原因是在 RGB 空间评估,但训练是在 Y 通道做的,RGB 直接算 PSNR 会把色度通道的 bicubic 误差也统计进去,数值自然会低一截。还有边界问题,卷积 padding 会在图像边界产生伪影,如果不把每条边裁掉 scale 个像素再算 PSNR,边界误差会让整体 PSNR 掉 0.2dB 左右。

解决:测试代码里先用 bicubic 放大 LR 图得到输入,网络 forward 之后把输出和输入相加,再 clip 到 [0,1],最后转成像素值计算 PSNR。评估前先做一个 sanity check:不用网络,直接拿 bicubic 插值结果算一个 PSNR 当作基线,再跑网络输出算一次,如果网络结果比基线还差,那一定是推理链路有 bug,而不是模型没学好。边界裁剪按每个测试集的惯例来,输入宽度除以 scale 不是整数时,裁剪尺寸要向下取整。

5.4 坑:多尺度训练之后,某个倍率的效果反而比单尺度差

现象:用 [2,3,4] 混合尺度训练完的模型,4x 超分效果挺好的,但 2x 超分结果却不如只拿 2x 数据训出来的模型,边缘有重影。

原因:不同倍率下 LR 图的信息损失程度差别很大,2x 时低分辨率图保留的高频信息多,网络有更多线索可用,4x 时输入几乎只剩低频,网络必须更依赖先验去猜测纹理。混合训练时,同一个网络要同时处理这两种差异很大的输入分布,参数会往中间值妥协。如果训练数据和测试数据的退化函数不完全一致,比如训练脚本里缩小用了 INTER_AREA、放大用了 INTER_CUBIC,而测试时两个阶段都用 INTER_CUBIC,那么网络的输入分布和测试时的实际分布就错位了。

解决:首先保证训练和测试统一用 INTER_CUBIC 做整个退化和放大流程,这个基础检查能解决一大半问题。其次,混合尺度训练时把 scale 的选择概率调成向较大倍率倾斜,比如 [2,3,4] 改成 [2 权重 0.2, 3 权重 0.3, 4 权重 0.5],因为大倍率任务更难,需要更多样本。如果项目只关注 2x,那就老老实实只训单尺度,不要为了炫技上混合尺度。

5.5 坑:HDF5 数据层报 Data layer prefetch 失败

现象:训练启动后 caffe 报错HDF5DataLayer failed to prefetch,数据层读取直接中断。

原因:文本文件 train_hdf5.txt 里写的是相对路径 h5 文件路径,而 caffe 进程的工作目录和生成脚本的工作目录不同,运行时找不到文件。另一个很隐蔽的原因是 HDF5 样本总数不能被 batch_size 整除,caffe 的 HDF5DataLayer 每次按 batch 顺序读,读到文件末尾发现样本不足,就直接报 prefetch 错误。

解决:生成 train_hdf5.txt 时用绝对路径。样本总数方面,把 patches_per_image 调成和 batch_size 有倍数关系的值,比如 batch_size 是 64,每张图出 64 个 patch,这样图数是整数时,总数必然能整除。如果数据集本身不能改,可以裁掉尾部少量样本,让样本数变成 batch_size 的整数倍,这一步在生成脚本里加一个total = total - total % batch_size的逻辑就行。

6. 用 caffemodel 做测试推理:从 prototxt 到 PSNR 的计算脚本

6.1 最小可用的单图超分测试代码

训练完拿到 caffemodel 之后,测试阶段需要把训练网络里的数据层和 loss 层去掉,换成直接接收输入 blob 的部署网络。这里给出一个不依赖 deploy.prototxt 的最小测试方式,直接用训练的 net 定义,通过 reshape 和 forward 跑前向,适合快速验证单个模型。

# test_vdsr.py: 单图超分测试与PSNR计算 import caffe import cv2 import numpy as np caffe.set_mode_gpu() net = caffe.Net("train_val.prototxt", "models/vdsr_iter_100000.caffemodel", caffe.TEST) # 手动把数据层断开,绕过HDF5读取 net = caffe.Net("deploy.prototxt", "models/vdsr_iter_100000.caffemodel", caffe.TEST) scale = 4 img = cv2.imread("test.png") h, w = img.shape[:2] img_ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb) y = img_ycrcb[:, :, 0].astype(np.float32) / 255.0 cr = img_ycrcb[:, :, 1].astype(np.float32) / 255.0 cb = img_ycrcb[:, :, 2].astype(np.float32) / 255.0 target_h, target_w = h * scale, w * scale lr = cv2.resize(y, (w, h), interpolation=cv2.INTER_CUBIC) # 先放大输入 net.blobs["data"].reshape(1, 1, h, w) net.blobs["data"].data[...] = lr[None, None, :, :] out = net.forward()["r20"][0, 0] # 残差输出 sr_y = np.clip(lr + out, 0, 1) # 残差加回输入 sr_ycrcb = np.zeros((h, w, 3), dtype=np.float32) sr_ycrcb[:, :, 0] = sr_y sr_ycrcb[:, :, 1] = cv2.resize(cr, (w, h), interpolation=cv2.INTER_CUBIC) sr_ycrcb[:, :, 2] = cv2.resize(cb, (w, h), interpolation=cv2.INTER_CUBIC) sr_ycrcb = (sr_ycrcb * 255.0).astype(np.uint8) sr_bgr = cv2.cvtColor(sr_ycrcb, cv2.COLOR_YCrCb2BGR) cv2.imwrite("sr.png", sr_bgr) # 如果测试图本身是HR,可以算PSNR;这里是LR输入,需要先有ground truth

测试代码里最关键的一行是lr + out,这是残差学习和普通 CNN 超分在推理上的本质区别。deploy.prototxt 里没有 HDF5Data 层,通常用一个 Input 层占位,如果 caffe 版本较老不支持 Input 层,也可以保留数据层但把 hdf5_data_param 里的 source 指向一个空的文本文件。PSNR 计算要放到 Y 通道上进行,10 * np.log10(1.0 / np.mean((sr_y - gt_y) ** 2)),其中 gt_y 是原始高分辨率图的 Y 通道,两者尺寸必须一致,否则先裁边再算。

6.2 白捡涨点技巧:自集成推理

每次测试时把输入图做 8 个方向变换,分别过网络,再把输出逆变换后取平均,这个操作叫自集成。做法是:把输入图分别旋转 0、90、180、270 度,每个旋转角度再做一次水平翻转,总共 8 张图各自 forward,得到 8 张超分结果,逆变换回原始方向后取平均。这个技巧能在不重新训练的情况下把 PSNR 稳定提高 0.1 到 0.2dB,因为网络在不同方向上的重建偏差方向不完全一致,平均之后互相抵消了一部分。代价是推理时间变成 8 倍,适合做最终结果验证而不是批量出图。

我自己的习惯是每次训练完一个快照,先拿测试集里的 10 张图跑一遍自集成验证,如果自集成的增益超过 0.15dB,说明模型还有提升空间,回去继续训练;如果增益不到 0.05dB,说明模型已经收敛得差不多了,可以停。这个验证逻辑比单纯看 loss 曲线更能反映模型的实际边界。VDSR-caffe 这套工程跑通之后,再去看其他超分模型的残差结构、梯度裁剪配置和数据退化设计,基本一眼就能看出门道,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询