ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

VDSR-caffe实战:20层残差网络实现图像超分辨率

VDSR-caffe实战:20层残差网络实现图像超分辨率

简介:该资源面向超分辨率重建方向的研究者与工程实践者,提供CVPR2016论文《Accurate Image Super-Resolution Using Very Deep Convolutional Networks》的Caffe实现版本,并附带MatConvNet测试代码,可用于复现VDSR的深层卷积网络结构、验证模型效果或作为二次开发的基础工程。压缩包共286个文件,约9.75MB,以Caffe核心源码为主,包含119个m文件、25个cu与25个cpp、24个hpp等实现文件,另有proto与prototxt网络定义、caffemodel权重、py与sh脚本、md与tex文档及少量测试图像,覆盖训练、测试与文档说明各环节。已有265人学习下载,适合希望对照论文理解VDSR细节、快速搭建实验环境并开展对比实验的读者参考使用。

1. 从一张糊掉的监控截图说起:VDSR-caffe 到底能干什么

上周帮朋友处理一段监控截图,车牌区域糊成一团马赛克,他问我“能不能用 AI 把它变清楚”。这类需求背后其实是一个很成熟的课题:单图像超分辨率。VDSR-caffe 就是这条技术路线上一份相当经典的工程实现——它把 VDSR(Very Deep Super-Resolution)网络用 Caffe 框架落地,输入一张低分辨率图,输出放大 2 倍或 4 倍的高分辨率图。适合谁?手里有 Caffe 环境、想复现经典超分模型、或者要把它嵌进自己图像处理流水线的开发者。它不是那种开箱即用的桌面软件,而是一份能读、能跑、能改的工程代码,你得愿意动手配环境、读 prototxt、跑训练脚本。下面我按“这项目是什么 → 怎么跑起来 → 怎么改”的顺序,把它拆开讲清楚。

2. 架构与目录:VDSR 为什么用残差学习,Caffe 工程怎么组织

2.1 VDSR 的网络设计思路

VDSR 的核心贡献是把“极深卷积网络”引入超分任务,同时用残差学习解决深层网络的训练难题。传统超分模型往往只有三五层,感受野有限,重建细节靠猜;VDSR 堆到 20 层卷积,感受野扩大到 41×41,网络能看到更大范围的上下文,边缘和纹理的重建质量明显提升。但 20 层网络直接学“低分到高分”的映射,梯度容易在深层衰减,训练很难收敛。VDSR 的做法是让网络只学残差:输入低分图经过双三次插值放大到目标尺寸,网络学习的是“高分图减去插值图”的那部分高频细节,最后把网络输出加回插值图。这样网络只需要关注高频残差,学习目标更聚焦,收敛也稳得多。

另一个关键设计是单模型多尺度。训练时把不同放大倍数(2x、3x、4x)的样本混在一起喂给网络,网络学会根据输入内容自适应处理不同尺度,推理时一个权重文件就能应付多种放大需求,不用为每个倍数单独训一个模型。这个思路在当年算是很实用的工程取舍。

2.2 Caffe 工程的目录组织

Caffe 项目的目录结构有比较固定的惯例,VDSR-caffe 这类工程通常长这样:

VDSR-caffe/ ├── data/ # 训练/测试数据存放 │ ├── train/ # 训练集(HR 图,训练时在线降采样) │ └── test/ # 测试集 ├── models/ # 网络定义与求解器配置 │ ├── VDSR_train.prototxt # 训练网络结构 │ ├── VDSR_deploy.prototxt # 推理网络结构 │ └── solver.prototxt # 优化器超参 ├── snapshots/ # 训练产出的权重快照 ├── scripts/ # 数据准备、训练、推理脚本 │ ├── prepare_data.py │ ├── train.sh │ └── test.py └── README.md

models/下三个 prototxt 是整个工程的核心:VDSR_train.prototxt定义训练时的数据层、20 个卷积层和损失层;VDSR_deploy.prototxt去掉数据层和损失层,换成输入输出声明,供推理用;solver.prototxt管学习率、动量、迭代次数这些训练超参。scripts/里放的是把数据、训练、推理串起来的胶水脚本。理解了这个分工,后面改网络、换数据、调超参就知道该动哪个文件。

2.3 训练网络的关键层配置

训练 prototxt 里最值得看的是数据层和残差连接的处理。VDSR 的训练数据是在线生成的:读入高分图,随机裁剪 patch,再降采样成低分图,同时用双三次插值放大回原尺寸作为残差基准。Caffe 里通常用 Python 数据层或者预生成 lmdb 来实现。下面是一段典型的卷积层定义,展示 20 层网络里单层的写法:

layer { name: "conv1" type: "Convolution" bottom: "data" top: "conv1" param { lr_mult: 0.1 decay_mult: 1 } convolution_param { num_output: 64 kernel_size: 3 pad: 1 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0 } } } layer { name: "relu1" type: "ReLU" bottom: "conv1" top: "conv1" }

num_output: 64表示这一层输出 64 个特征通道,VDSR 所有卷积层统一用 64 通道;kernel_size: 3配pad: 1保证卷积后空间尺寸不变,这是堆深网络的基本要求;lr_mult: 0.1是给预训练微调留的余量,从头训练时可以设成 1。20 层里前 19 层都是“卷积 + ReLU”的组合,最后一层只做卷积输出单通道残差图,不加 ReLU,因为残差有正有负,加了 ReLU 会把负值截断,重建就废了。这个细节是很多人第一次改网络时容易翻车的地方。

2.4 损失层与残差相加的实现

VDSR 的损失不是直接对比网络输出和高分图,而是对比“网络输出 + 插值基准”和高分图。Caffe 里可以用Eltwise层做相加,再用EuclideanLoss算均方误差:

layer { name: "residual_sum" type: "Eltwise" bottom: "conv20" bottom: "bicubic" top: "pred" eltwise_param { operation: SUM } } layer { name: "loss" type: "EuclideanLoss" bottom: "pred" bottom: "label" top: "loss" }

Eltwise的SUM操作把第 20 层输出的残差和bicubic基准逐元素相加,得到最终预测图pred,再和标签label算欧氏损失。bicubic这个 blob 来自数据层,是低分图插值放大后的结果。推理时deploy.prototxt里同样保留这个相加结构,只是把损失层去掉,输出改成pred。改网络结构时,这两处的对应关系必须保持一致,否则训练和推理对不上,结果会莫名其妙地偏。

3. 跑起来:环境、数据、训练、推理四步走

3.1 Caffe 环境准备

Caffe 的环境配置是这套工程最大的门槛,血泪经验是版本和依赖必须对齐。推荐用 Ubuntu 16.04/18.04 配 CUDA 8.0/9.0,Python 2.7 或 3.5。依赖清单大致是:libprotobuf-dev、libleveldb-dev、libsnappy-dev、libopencv-dev、libhdf5-serial-dev、libboost-all-dev,加上protobuf-compiler和python-numpy。编译前先改Makefile.config:

cp Makefile.config.example Makefile.config # 打开以下开关 # USE_CUDNN := 1 # OPENCV_VERSION := 3 # WITH_PYTHON_LAYER := 1 # 把 INCLUDE_DIRS 和 LIBRARY_DIRS 里的 hdf5 路径改成 /usr/include/hdf5/serial make all -j8 make pycaffe make test -j8

WITH_PYTHON_LAYER := 1必须打开,因为数据准备脚本可能用到 Python 数据层;hdf5 路径不改的话编译到一半会报找不到hdf5.h,这是 Caffe 编译的经典坑。make pycaffe生成 Python 接口,后面推理脚本要用。编译完把python/目录加进PYTHONPATH,跑一句python -c "import caffe"不报错就算环境通了。

3.2 数据准备与 lmdb 生成

VDSR 训练需要大量高分图,训练时在线降采样。数据准备脚本一般做三件事:收集高分图、统一转成灰度或 RGB、生成 lmdb。下面是一段典型的 lmdb 生成逻辑:

import caffe import numpy as np import cv2 # 读入高分图,转 float32,VDSR 常用 YCbCr 的 Y 通道训练 img = cv2.imread(path, cv2.IMREAD_COLOR) img = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)[:, :, 0].astype(np.float32) / 255.0 # 写入 lmdb,datum 的 data 存高分 patch,label 留空 datum = caffe.io.array_to_datum(img[np.newaxis, :, :]) txn.put(key, datum.SerializeToString())

这里把图转成 YCbCr 只取 Y 通道,是因为人眼对亮度更敏感,超分主要在亮度上做,色度通道用插值放大就够,能省大量计算。array_to_datum要求输入是(channel, height, width)形状,所以加了个np.newaxis。lmdb 生成后,训练 prototxt 的数据层用type: "Data"指向这个 lmdb,batch_size一般设 64,patch 尺寸 41×41,和感受野对齐。

3.3 训练脚本与超参设置

solver.prototxt决定训练能不能收敛,关键参数如下:

net: "models/VDSR_train.prototxt" base_lr: 0.1 lr_policy: "step" stepsize: 20 gamma: 0.1 momentum: 0.9 weight_decay: 0.0001 max_iter: 100000 snapshot: 5000 snapshot_prefix: "snapshots/VDSR" solver_mode: GPU

base_lr: 0.1配momentum: 0.9是 SGD 的常规组合;lr_policy: "step"每 20 个 epoch 把学习率乘 0.1,让训练后期精细调整;weight_decay: 0.0001做 L2 正则防过拟合。启动训练就一行:

caffe train --solver=models/solver.prototxt --gpu 0 2>&1 | tee train.log

--gpu 0指定显卡,tee把日志同时写文件,方便事后查 loss 曲线。训练初期 loss 会在 0.01 量级波动,收敛后降到 0.001 以下算正常。如果 loss 一直不降,先查数据层输出的 blob 形状对不对,再查学习率是不是太大导致震荡。

3.4 推理与结果保存

推理用deploy.prototxt加载训练好的权重,流程是:读图 → 插值放大 → 前向 → 残差相加 → 保存。核心代码如下:

import caffe import cv2 import numpy as np caffe.set_mode_gpu() net = caffe.Net('models/VDSR_deploy.prototxt', 'snapshots/VDSR_iter_100000.caffemodel', caffe.TEST) img = cv2.imread('test.png', cv2.IMREAD_COLOR) y = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)[:, :, 0].astype(np.float32) / 255.0 # 双三次插值放大到目标尺寸 y_up = cv2.resize(y, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC) net.blobs['data'].data[...] = y_up[np.newaxis, np.newaxis, :, :] out = net.forward() residual = out['conv20'][0, 0] # 网络输出的残差 pred = np.clip(y_up + residual, 0, 1) # 残差加回基准并截断 cv2.imwrite('result.png', (pred * 255).astype(np.uint8))

fx=2, fy=2是放大倍数,要和训练时覆盖的尺度一致;net.blobs['data']的形状必须和 deploy 里声明的输入尺寸匹配,图太大要分块推理,否则显存会爆。np.clip截断到 [0,1] 是必须的,残差相加后可能超出范围,不截断保存出来会有噪点。色度通道用cv2.resize插值放大后和预测的 Y 通道合并,再转回 BGR 保存,这样出来的图才是彩色的。

4. 避坑与排查:五个真实踩过的坑

4.1 编译报错找不到 hdf5.h

现象是make all跑到一半报fatal error: hdf5.h: No such file or directory。原因是 Ubuntu 把 hdf5 头文件装在/usr/include/hdf5/serial而不是默认路径。解决方法是改Makefile.config里的INCLUDE_DIRS,加上/usr/include/hdf5/serial,LIBRARY_DIRS加上/usr/lib/x86_64-linux-gnu/hdf5/serial,然后make clean重编。这个坑几乎每个新装 Caffe 的人都踩过。

4.2 训练 loss 变成 nan

现象是训练几十个 iter 后 loss 突然变 nan,之后再也不降。原因通常是学习率太大或者输入数据没归一化。VDSR 的输入必须缩到 [0,1],如果直接喂 0-255 的像素值,第一层卷积输出就爆了。解决方法是检查数据层输出范围,把base_lr从 0.1 降到 0.01 试一轮,确认 loss 稳定后再逐步调回。

4.3 推理结果整体偏灰或偏亮

现象是推理出来的图比预期整体偏灰。原因是残差相加后没做截断,或者色度通道和亮度通道合并时色彩空间搞反了。检查np.clip有没有加,以及cv2.cvtColor的转换方向是不是BGR2YCrCb和YCrCb2BGR配对。另一个可能是 deploy 里漏了Eltwise相加层,网络只输出了残差没加基准,图自然偏灰。

4.4 显存不足导致推理中断

现象是推理大图时抛Check failed: error == cudaSuccess (2 vs. 0) out of memory。原因是整图一次性送进网络,中间层特征图占满显存。解决方法是分块推理:把图切成带重叠的 patch,逐块前向,重叠区域做加权融合,最后拼回整图。重叠宽度取感受野的一半左右(约 20 像素),能消除拼接缝。

4.5 训练和推理结果对不上

现象是训练时 loss 很低,推理出来效果却很差。原因多半是train.prototxt和deploy.prototxt的层结构不一致,比如训练时有 BatchNorm 推理时漏了,或者输入 blob 名字对不上。排查方法是逐层对比两个 prototxt 的name和bottom/top,确保卷积层、ReLU、Eltwise 的顺序和连接完全一致,只允许数据层和损失层有差异。

5. 二次开发:换尺度、换损失、接进自己的流水线

改这个工程,我一般从三个方向下手。第一是换放大尺度。VDSR 单模型多尺度的特性意味着你可以在数据准备阶段把 2x、3x、4x 的样本按比例混进同一个 lmdb,训练时网络自然学会多尺度。如果只想支持单一尺度,把数据层的降采样倍数固定,推理时cv2.resize的fx/fy跟着改就行,权重不用重训。第二是换损失函数。EuclideanLoss对高 PSNR 友好但对视觉感知一般,想提升观感可以换成基于 VGG 特征的感知损失,在 prototxt 里加一个预训练 VGG 的卷积层提取特征,用EuclideanLoss对比特征图而不是像素。这个改动要动网络结构,训练和推理的 prototxt 都得同步改,改完先用小数据集跑 1000 iter 验证 loss 能降再上全量。第三是接进自己的流水线。最省事的做法是把推理脚本封装成一个函数,输入输出都是 numpy 数组,外面套一层 Flask 或者命令行工具就能用。我习惯在封装时加一个尺寸检查:输入小于 41×41 的图直接走插值,不进网络,避免边界效应导致边缘发虚。

验证改动是否有效,别只看单张图的肉眼效果,跑一个包含几十张图的测试集,算平均 PSNR 和 SSIM,和改动前的基线对比。PSNR 涨 0.1dB 以上、SSIM 涨 0.005 以上才算有意义的提升,否则可能只是随机波动。改网络结构后第一次训练,我会强制把max_iter设成 2000 跑一轮快速验证,确认 loss 曲线正常下降再放开训。从那以后我每次动 prototxt 都先跑这个小循环,省下不少通宵等训练结果的时间。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进