简介这份资源是SRCNN图像超分辨率算法的TensorFlow实现面向具备一定深度学习基础、希望复现经典超分模型的研究者与开发者。包内共308个文件以302个bmp图像数据为主覆盖训练与测试数据集另有3个py脚本承担主流程与模型定义2个m文件负责Matlab端的图像裁剪与归一化预处理并附1个md说明文档压缩包约27.72MB。代码结构清晰main_process.py统一管理训练与测试入口srcnn_model.py封装模型类utils.py提供数据预处理函数preprocess.m与modcrop.m则完成训练图像的裁剪和尺寸适配便于读者对照原论文理解网络结构与数据管线。目前已有388人学习下载适合想从零搭建超分实验、研究图像重建流程或进行算法对比的读者参考。1. 从一份 SRCNN 的 TensorFlow 复现包说起它到底能跑出什么很多人第一次接触图像超分辨率是从 SRCNN 这篇 2014 年的论文开始的。它只有三层卷积结构简单到几乎不像一个深度学习模型但偏偏就是它把「双三次插值 稀疏编码」那套传统流程按在地上摩擦第一次证明了端到端的卷积网络可以直接学出低分辨率到高分辨率的映射。这份资源就是一份用 TensorFlow 实现 SRCNN 的完整代码包同时标题里带了 matlab 字样说明它大概率还保留了和 MATLAB 版本对照的痕迹——对于做图像处理大作业、想拿超分当课程设计、或者单纯想搞明白「深度学习到底比插值强在哪」的人来说这是一个非常合适的起点。它不追求 SOTA不堆 trick跑起来快改起来也快适合拿来当第一个能看见效果的超分项目。2. 把 SRCNN 拆开看三层卷积为什么能顶事2.1 结构本身比你想的还朴素SRCNN 的全部计算就是三次卷积。第一层负责从低分辨率图像里提取 patch 特征论文里用的是 9×9 的卷积核输出 64 维特征图第二层做非线性映射把 64 维压到 32 维核大小 1×1本质是在特征通道之间做全连接第三层做重建把 32 维映射回 3 通道的 RGB 或 1 通道的 Y 通道核大小 5×5。整个网络没有池化、没有 BN、没有残差参数量也就几万级别。这种设计在今天看当然简陋但它有一个被很多人忽略的好处输入输出尺寸一致不需要上采样层也不需要下采样层。低分辨率图像先被双三次插值放大到目标尺寸再送进网络网络只负责「修正」插值带来的模糊和锯齿。所以你在代码里会看到数据预处理阶段一定有一次 resize 操作而且这个 resize 用的是 bicubic 而不是 nearest因为 nearest 会引入块状伪影网络很难学回来。2.2 为什么训练时用的是 YCbCr 而不是 RGB这是 SRCNN 复现里最容易翻车的地方。论文明确说了只在 Y 通道上做超分Cb 和 Cr 通道直接双三次插值放大。原因有两个人眼对亮度远比对色度敏感Y 通道承载了大部分结构信息色度通道本身高频成分少插值放大后肉眼几乎看不出差别。如果你把 RGB 三通道一起送进网络训练会变慢而且色偏问题很难调。在 TensorFlow 里做这个转换常见做法是用tf.image.rgb_to_yuv或者手动矩阵乘。注意tf.image.rgb_to_yuv的输出范围是 [0,1]而很多 MATLAB 版本的代码用的是 [0,255] 的 YCbCr两者不能直接混用。我一般会在数据加载阶段统一转成 float32 的 [0,1]训练完再转回去保存。2.3 数据准备从 DIV2K 到自定义图片这份代码包大概率自带了一个小规模数据集或者数据加载脚本。如果没有你需要自己准备。SRCNN 的训练集可以用 DIV2K、Set5、Set14 或者 BSD200但注意 SRCNN 原文用的是 ImageNet 的一个子集而且训练时是随机裁剪 33×33 的 patch。你如果直接用整张图训练显存会爆而且效果不一定好。下面是一个常见的数据管道写法用tf.data构建import tensorflow as tf def load_image(path): img tf.io.read_file(path) img tf.image.decode_image(img, channels3, expand_animationsFalse) img tf.image.convert_image_dtype(img, tf.float32) # 转到 [0,1] return img def preprocess(lr_path, hr_path, scale3, patch_size33): lr load_image(lr_path) hr load_image(hr_path) # 随机裁剪同一位置 lr_patch tf.image.random_crop(lr, [patch_size, patch_size, 3]) hr_patch tf.image.random_crop(hr, [patch_size * scale, patch_size * scale, 3]) # 转 Y 通道 lr_y tf.image.rgb_to_yuv(lr_patch)[..., 0:1] hr_y tf.image.rgb_to_yuv(hr_patch)[..., 0:1] return lr_y, hr_y dataset tf.data.Dataset.from_tensor_slices((lr_paths, hr_paths)) dataset dataset.map(preprocess, num_parallel_callstf.data.AUTOTUNE) dataset dataset.batch(16).prefetch(tf.data.AUTOTUNE)这段代码的关键点有三个convert_image_dtype把像素值压到 [0,1]避免梯度爆炸random_crop保证低分和高分 patch 空间对齐rgb_to_yuv后只取第 0 通道。参数patch_size33是 SRCNN 原文的设置scale3是放大倍数你可以改成 2 或 4但网络结构不用动因为 SRCNN 本身不依赖 scale 参数。2.4 模型定义与训练循环模型部分非常短三层卷积加 ReLUfrom tensorflow.keras import layers, Model def build_srcnn(): inputs layers.Input(shape(None, None, 1)) x layers.Conv2D(64, 9, paddingsame, activationrelu, kernel_initializerhe_normal)(inputs) x layers.Conv2D(32, 1, paddingsame, activationrelu, kernel_initializerhe_normal)(x) outputs layers.Conv2D(1, 5, paddingsame, kernel_initializerhe_normal)(x) return Model(inputs, outputs) model build_srcnn() model.compile(optimizertf.keras.optimizers.Adam(1e-4), lossmse, metrics[mae]) model.fit(dataset, epochs100)注意第一层和第三层的paddingsame是必须的否则输出尺寸会比输入小和标签对不上。损失函数用 MSE 是 SRCNN 原文的选择虽然现在很多人用 L1 或者感知损失但复现原论文就用 MSE。学习率 1e-4 是常见起点如果你发现 loss 震荡降到 1e-5 再试。3. 从零跑通环境、数据、训练、推理四步走3.1 TensorFlow 环境安装与版本选择这份代码包标题里写了 TensorFlow但没有指定版本。根据热搜词里出现的tensorflow 2.5.0 cuda cudnn nvidia 驱动我推测它可能是在 TF 2.x 早期版本上验证的。实际跑的时候我建议用 TF 2.10 或 2.12这两个版本对 CUDA 的支持比较稳而且tf.image.rgb_to_yuv的行为没有变。安装命令pip install tensorflow2.12.0 # 如果需要 GPU pip install tensorflow-gpu2.12.0如果你用的是 NVIDIA 显卡注意驱动版本和 CUDA 版本的对应关系。TF 2.12 默认带 CUDA 11.8驱动版本建议 520 以上。装完之后用下面这行验证import tensorflow as tf print(tf.config.list_physical_devices(GPU))如果输出空列表说明 GPU 没认到先检查驱动和 CUDA 路径不要急着改代码。3.2 数据集的两种组织方式这份资源可能自带数据也可能只给了脚本。如果没有数据我一般会去下载 DIV2K 的验证集或者 Set5然后自己造低分高分对。目录结构建议这样data/ train/ hr/ img_001.png img_002.png lr/ img_001.png img_002.png val/ hr/ lr/低分图用双三次下采样生成缩放因子和训练时一致。注意低分图不要提前放大放大操作放在网络输入之前做这样你可以灵活切换 scale。生成低分图的脚本import cv2 import os hr_dir data/train/hr lr_dir data/train/lr scale 3 for fname in os.listdir(hr_dir): hr cv2.imread(os.path.join(hr_dir, fname)) h, w hr.shape[:2] lr cv2.resize(hr, (w // scale, h // scale), interpolationcv2.INTER_CUBIC) cv2.imwrite(os.path.join(lr_dir, fname), lr)这里用INTER_CUBIC而不是INTER_AREA因为 SRCNN 原文的下采样方式就是双三次。如果你用INTER_AREA低分图的统计特性和训练时不一致推理效果会掉。3.3 训练时的 batch 与学习率调整SRCNN 原文的 batch size 是 128但那是 2014 年的设置现在显存普遍够大你可以用 16 或 32。学习率方面原文用的是 1e-4 固定但实际训练中我发现前 10 个 epoch 用 1e-3 快速下降后面降到 1e-5 微调收敛更快。initial_learning_rate 1e-3 lr_schedule tf.keras.optimizers.schedules.ExponentialDecay( initial_learning_rate, decay_steps1000, decay_rate0.9, staircaseTrue) optimizer tf.keras.optimizers.Adam(lr_schedule)decay_steps1000表示每 1000 步衰减一次decay_rate0.9是衰减系数。这个 schedule 不是原文的是我自己试出来的你可以根据 loss 曲线调整。3.4 推理把低分图放大并保存训练完之后推理脚本要处理两件事把低分图双三次放大到目标尺寸然后送进网络预测 Y 通道最后把 Y 通道和插值放大的 CbCr 合并。def infer(model, lr_path, scale3): lr cv2.imread(lr_path) lr cv2.cvtColor(lr, cv2.COLOR_BGR2YUV) h, w lr.shape[:2] # 双三次放大 lr_y cv2.resize(lr[..., 0], (w * scale, h * scale), interpolationcv2.INTER_CUBIC) lr_cb cv2.resize(lr[..., 1], (w * scale, h * scale), interpolationcv2.INTER_CUBIC) lr_cr cv2.resize(lr[..., 2], (w * scale, h * scale), interpolationcv2.INTER_CUBIC) # 归一化并预测 lr_y_norm lr_y.astype(float32) / 255.0 pred model.predict(lr_y_norm[None, ..., None])[0, ..., 0] pred np.clip(pred * 255.0, 0, 255).astype(uint8) # 合并 out cv2.merge([pred, lr_cb, lr_cr]) out cv2.cvtColor(out, cv2.COLOR_YUV2BGR) return out注意cv2.cvtColor的 YUV 和tf.image.rgb_to_yuv的 YUV 范围不一样OpenCV 用的是 [0,255]TensorFlow 用的是 [0,1]。如果你训练时用 TF 的转换推理时也要用 TF 的转换不要混用。4. 避坑与排查SRCNN 复现里最容易翻车的五件事4.1 现象训练 loss 一直不降输出全是灰色原因数据归一化没做或者做了但范围不对。SRCNN 对输入尺度很敏感如果你把 [0,255] 的图直接送进网络第一层卷积的输出会非常大ReLU 之后梯度直接死掉。解决确保输入在 [0,1] 之间标签也在 [0,1] 之间。用tf.image.convert_image_dtype而不是手动除以 255因为前者会自动处理类型转换。4.2 现象推理结果比双三次插值还模糊原因训练时用的低分图生成方式和推理时不一致。比如训练时用INTER_AREA下采样推理时用INTER_CUBIC放大两者的频域特性对不上网络学到的映射就失效了。解决训练和推理的下采样、上采样方式必须一致。SRCNN 原文用的是双三次你就全程双三次。4.3 现象GPU 显存爆了batch 降到 1 还是爆原因输入 patch 太大或者模型里某层没有paddingsame导致特征图尺寸逐层缩小但中间某次 reshape 或者 concat 操作把尺寸又拉大了。解决先检查每层输出的 shape用model.summary()看一遍。SRCNN 三层都是 same padding输出尺寸应该和输入完全一致。如果中间某层尺寸变了说明 padding 写错了。4.4 现象保存的图片颜色发绿或发紫原因YUV 和 RGB 的转换矩阵不匹配。TensorFlow 的rgb_to_yuv用的是 BT.601 还是 BT.709和 OpenCV 的COLOR_BGR2YUV不一定一样。解决要么全程用 OpenCV 做转换要么全程用 TensorFlow。我一般训练时用 TF推理时也用 TF 的yuv_to_rgb避免混用。4.5 现象训练集 loss 很低验证集 loss 很高原因过拟合。SRCNN 参数量虽然少但如果你用的训练集只有几十张图过拟合是必然的。解决加数据增强随机翻转、旋转、裁剪。或者直接用 DIV2K 的 800 张训练图不要用 Set5 这种只有 5 张图的测试集来训练。5. 进阶技巧把 SRCNN 当基线怎么改才不白改5.1 用 PSNR 和 SSIM 量化对比跑通之后你肯定想知道效果到底怎么样。最直接的方法是在 Set5 或 Set14 上算 PSNR 和 SSIM和双三次插值对比。下面是一个计算脚本import numpy as np from skimage.metrics import peak_signal_noise_ratio, structural_similarity def evaluate(hr, pred): psnr peak_signal_noise_ratio(hr, pred, data_range255) ssim structural_similarity(hr, pred, multichannelTrue, channel_axis2) return psnr, ssim注意data_range255是因为图片是 uint8如果你的图片是 float要改成 1.0。SSIM 的channel_axis参数在旧版 skimage 里叫multichannel版本不同写法不同报错了就查一下文档。5.2 把 SRCNN 改成 FSRCNN 或 ESPCNSRCNN 最大的问题是推理慢因为它在放大后的图像上做卷积。FSRCNN 把反卷积放在最后ESPCN 用亚像素卷积两者都能在低分辨率空间做大部分计算速度提升明显。如果你已经跑通了 SRCNN改 FSRCNN 只需要把第一层的输入改成低分图最后一层换成Conv2DTranspose或者DepthToSpace。# FSRCNN 的最后一层 x layers.Conv2D(scale * scale * 1, 5, paddingsame)(x) outputs layers.DepthToSpace(scale)(x)DepthToSpace是 TensorFlow 自带的亚像素卷积实现scale就是放大倍数。注意输入通道数要是scale * scale * out_channels这里 out_channels 是 1。5.3 一个我踩过的坑不要用测试集调参我刚开始做超分的时候习惯在 Set5 上试各种学习率和 batch size结果模型在 Set5 上 PSNR 很高换到 Set14 就掉点。后来才明白Set5 只有 5 张图任何微小的调参都会过拟合到这 5 张图上。正确的做法是划一个验证集比如从 DIV2K 里拿 10 张图出来调参只看验证集测试集只在最后跑一次。从那以后我每次做超分实验都强制把数据分成 train/val/test 三份val 用来调参test 用来写报告。这个习惯帮我省了很多后悔药。希望这份 SRCNN 的复现包能帮你把超分的第一公里跑通后面的路就好走了。本文还有配套的精品资源点击获取