
简介毕设项目围绕红外图像非均匀性校正展开基于Python和卷积神经网络实现提出一种借助两个残差块级联进行残差学习的RNUC网络。资源面向毕业设计、课程设计及工程实训人群既适合初学者入门深度学习与图像处理也能为中期项目立项提供参考。压缩包内含8个文件以3个Python源码脚本和模型结构摘要、训练日志文本为主另含模型导出图片与说明文档zip包整体约374KB体积小巧但结构清晰。当前已有165人学习下载。通过阅读模型定义、训练入口与摘要文件可以复现非均匀性校正网络的基本流程理解残差级联如何提升校正效果也能观察特征提取与模型导出环节在此基础上进行参数调整或模块替换再结合自己的实验数据训练即可较快形成一篇可供答辩使用的毕业设计原型。对于图像处理方向的课程作业或初期项目这份资源是一份紧凑而完整的参考样例。1. 红外图像非均匀性校正用卷积神经网络把条纹噪声交给残差网络处理红外焦平面阵列IRFPA成像时探测器像元响应率不一致出图总带竖向条纹或固定斑块这就是红外图像非均匀性。传统两点校正需要黑体标定标定条件一变就容易失效。这个项目给的思路是把校正当成图像到图像的回归任务用 Python 实现的卷积神经网络处理RNUCResidual-learning based Non-uniformity Correction Net用两个残差块级联做残差学习配合合并式特征提取单元从单帧红外图上直接预测条纹噪声再与原图相减得到干净输出。它对毕业设计、课程设计尤其合适——网络轻、效果能用图表说话训练流程也适合在普通显卡上跑完。接下来我把结构拆开从代码到训练再到排查一条条过。2. RNUC网络结构拆解残差块级联、合并式特征提取与model_summary解读2.1 非均匀性校正的数学建模为什么学噪声比学干净图更容易收敛先看问题本质。红外图像的非均匀性可以近似写成I(x, y) G(x, y)·S(x, y) B(x, y) n(x, y)其中 G 是像元的非均匀增益B 是偏置S 是真实辐射响应n 是随机噪声。G 和 B 在整幅图上形成固定模式噪声Fixed Pattern Noise, FPN也就是肉眼看到的竖条纹。传统两点校正法通过黑体标定计算增益和偏置系数优点是直观、计算快缺点是系数随环境温度和工作状态漂移一旦漂移校正效果直接变差。这个项目的出发点不一样把 FPN 当做一个可学习的残差分量。网络输入是带条纹的红外图不直接回归干净图而是回归残差噪声 n̂再用原图 I 减去 n̂ 得到校正图。这就是残差学习Residual Learning在非均匀性校正里的应用。相比直接学干净图学噪声分量更符合物理意义——噪声和条纹是高度结构化的固定模式网络要拟合的映射更简单。在训练数据量不大的毕业设计场景里残差学习收敛更快、更稳。那训练数据从哪来真实 IRFPA 很难同时拿到“带条纹图”和“完美干净图”。常见做法是合成取灰度图作为真值在列方向乘一个缓慢变化的增益、叠加列偏置生成退化图。用一批成对数据训练 I → (I − n̂)不需要真实硬件标定这也是这个项目能在纯 Python 环境里复现的原因。2.2 合并式特征提取单元三路卷积拼接与1×1卷积降维的实现网络骨干不是标准 VGG 式堆叠而是先过合并式特征提取单元。它的设计动机很直接竖条纹的宽度不是固定的窄的几条像素宽的能到十几条单用 3×3 卷积感受野不够所以用 3×3、5×5、7×7 三路并行把多尺度条纹响应都捕捉到再拼接、降维。我给出一份可用的实现# models.py —— 合并式特征提取单元 import tensorflow as tf from tensorflow.keras import layers, Model def merge_extract_unit(x, filters32): # x: 输入特征图, shape(batch, h, w, c) b1 layers.Conv2D(filters, 3, paddingsame, activationrelu)(x) b2 layers.Conv2D(filters, 5, paddingsame, activationrelu)(x) b3 layers.Conv2D(filters, 7, paddingsame, activationrelu)(x) # 在通道维拼接三路特征 merged layers.Concatenate(axis-1)([b1, b2, b3]) # 1x1卷积压缩通道并保留原始通道数 out layers.Conv2D(x.shape[-1], 1, paddingsame)(merged) return out核心在后面两行。三路卷积各自输出 filters 个通道拼接后总通道数是 3×filters直接往下传后续残差块的计算量会成倍上涨。1×1 卷积在这里做两件事通道混合与降维把 96 通道压回输入通道数保证后面残差块的输入尺寸和主干一致。filters 我一般先给 32显存紧张就 16图像噪声复杂给到 48。5×5、7×7 卷积参数多但对条纹这种列方向相关性很强的噪声模式更大感受野能覆盖更宽的条纹区域这个开销值得花。2.3 两个残差块级联串行精修条纹特征的结构设计与参数量网络主体是两个残差块级联。这里“级联”要着重强调第二个残差块的输入是第一个的输出两个块串行连接形成“特征提取 → 残差精修 → 再精修”的层级。每个残差块内部是经典结构两组“Conv BN ReLU”加一条跳跃连接最后主分支与 shortcut 逐点相加。# models.py —— 残差块定义 def residual_block(x, filters32): shortcut x h layers.Conv2D(filters, 3, paddingsame)(x) h layers.BatchNormalization()(h) h layers.ReLU()(h) h layers.Conv2D(filters, 3, paddingsame)(h) h layers.BatchNormalization()(h) h layers.ReLU()(h) # 主分支输出要回到shortcut的通道数 h layers.Conv2D(x.shape[-1], 3, paddingsame)(h) return layers.Add()([shortcut, h])从参数量看单通道输入时第一层 3×3 卷积输入 1 通道输出 32 通道权重是 1×32×3×3288 个残差块内两个 32 通道的 3×3 卷积每个有 32×32×3×39216 个权重最后压回单通道的卷积只有 32×1×3×3288 个。整个网络跑下来参数量在 3 万到 5 万区间属于轻量级模型CPU 都能做前向推理。这个体量对答辩现场演示非常关键——笔记本上也能实时跑。注意两级残差块级联和“双分支并联”不同。并联是两条支路同时处理后面再融合优势是梯度传播路径短、训练稳级联强调逐层抽象第一块定位条纹候选区域第二块修正幅度和边界对全局方向性强的条纹噪声级联的精修作用更明显。代价是训练时梯度要穿过两级块学习率控制不好容易后面某块学不到东西。所以第 4 章讲训练时会强调学习率衰减这里先记住这个因果。2.4 完整网络组装与model_summary、export model.png的生成把合并式特征提取单元和两个残差块串起来再接一个 3×3 卷积输出单通道噪声图最后用 Subtract 层做原图减噪声。完整结构如下# models.py —— RNUC 完整模型 def build_rnuc(input_shape(256, 256, 1)): inputs layers.Input(shapeinput_shape) x merge_extract_unit(inputs, filters32) x residual_block(x, filters32) x residual_block(x, filters32) # 预测固定条纹噪声单通道尺寸与原图一致 noise layers.Conv2D(1, 3, paddingsame, namenoise_head)(x) # 残差学习原图减预测噪声得到校正图 corrected layers.Subtract(namecorrected)([inputs, noise]) return Model(inputs, corrected)训输出的是 corrected 层直接和干净真值算 MSE。生成 model_summary.txt 用重定向python -c from models import build_rnuc; m build_rnuc(); print(m.summary()) model_summary.txt看 summary 时我关注三个位置。第一MergeExtractUnit 末尾的 1×1 卷积输出 shape 是否和输入一致不一致说明后面 Add 层维度对不上。第二两个残差块之间特征图尺寸是否稳定保持某个 Conv 漏了 paddingsame 中间尺寸缩小就接不上。第三noise_head 输出通道必须是 1否则 Subtract 层广播规则不对训练 loss 可能变 nan。仓库里还带了 export model.png这是模型结构图用一行代码导出from tensorflow.keras.utils import plot_model plot_model(build_rnuc(), to_fileexport_model.png, show_shapesTrue)答辩 PPT 里放这张图比贴十几行代码直观得多。一个小的提示plot_model 默认靠 pydot 和 GraphvizWindows 上装好 Graphviz 并把 bin 目录加进 PATH否则会报pydotfailed to import。3. 训练数据与主流程跑通mian.py脚本、合成条纹和TensorBoard日志3.1 合成条纹训练数据列增益与偏置模板的生成方法仓库里没有打包数据集README 只给了目录结构。训练对要自己生成。我用的方式是把条纹拆成乘性增益和加性偏置两块分别对应像元响应率不一致和暗电流不一致。下面这段工具代码放在 utilis.py 里# utilis.py —— 合成条纹噪声工具 import numpy as np def generate_stripe_noise(shape(256, 256), gain_range0.06, col_range8): h, w shape # 每列一个增益系数和偏置系数 col_gain 1.0 np.random.uniform(-gain_range, gain_range, size(1, w)) col_bias np.random.uniform(-col_range, col_range, size(1, w)) # 扩展到整幅图像的模板 gain_map np.tile(col_gain, (h, 1)) bias_map np.tile(col_bias, (h, 1)) return gain_map, bias_map def apply_stripe(clean, gain_map, bias_map): noisy clean * gain_map bias_map return np.clip(noisy, 0, 255).astype(np.float32)参数方面gain_range 控制增益起伏幅度0.03 到 0.08 比较常见。太小条纹肉眼看不清训练了也看不出效果太大网络学到的是“压暗图像”而不是“去除条纹”。col_range 控制偏置强度单位是灰度值我一般给 5 到 15。每张训练图用不同随机种子生成保证条纹位置不固定否则网络会把所有样本的条纹位置背下来换图就失效。生成完成后把成对数据存成 npypython -c import numpy as np from utilis import generate_stripe_noise, apply_stripe import cv2, glob clean_paths glob.glob(data/clean/*.png) for i, p in enumerate(clean_paths[:500]): img cv2.imread(p, cv2.IMREAD_GRAYSCALE).astype(np.float32) gain, bias generate_stripe_noise((256, 256)) noisy apply_stripe(img, gain, bias) 这里提醒一点glob 匹配到的文件要过滤扩展名白名单数据目录里混入 .DS_Store 或临时文件会直接读成 None这在后面避坑章会展开。3.2 mian.py训练脚本拆解归一化、超参与回调配置主脚本文件名叫 mian.py这是 main.py 的笔误。这个拼写错误在毕设代码里很常见不影响运行但迁移到自己工程时建议改回 main.py。脚本核心逻辑按我习惯的方式拆解如下# mian.py —— RNUC 训练主流程 import numpy as np import tensorflow as tf from tensorflow.keras.callbacks import ModelCheckpoint, TensorBoard, ReduceLROnPlateau from models import build_rnuc EPOCHS 200 BATCH_SIZE 16 LR 1e-4 # 读取已生成的成对数据 train_noisy np.load(data/train_noisy.npy) train_clean np.load(data/train_clean.npy) val_noisy np.load(data/val_noisy.npy) val_clean np.load(data/val_clean.npy) # 归一化到[0,1]输入和标签必须同尺度 train_noisy train_noisy / 255.0 train_clean train_clean / 255.0 val_noisy val_noisy / 255.0 val_clean val_clean / 255.0 # 构建模型并编译 model build_rnuc(input_shape(256, 256, 1)) model.compile( optimizertf.keras.optimizers.Adam(learning_rateLR), lossmse, metrics[mae] ) # 训练并保存日志与权重 model.fit( train_noisy, train_clean, batch_sizeBATCH_SIZE, epochsEPOCHS, validation_data(val_noisy, val_clean), callbacks[ ModelCheckpoint(ckpt/rnuc_best.h5, save_best_onlyTrue, monitorval_loss), TensorBoard(log_dirlogs), ReduceLROnPlateau(monitorval_loss, factor0.5, patience20, min_lr1e-6) ] )归一化顺序是个经典翻车点。clean 图除了noisy 图忘了除或反过来loss 一开始会大得离谱看着像不收敛其实是数据尺度不匹配。Adam 初始学习率 1e-4 是我在这个项目里的默认起点。直接上 1e-3前期 loss 跌得快中后期曲线一定震荡因为残差网络后面的损失面很平学习率太大就在最优值附近来回跳。ReduceLROnPlateau 设 patience20连续 20 轮验证 loss 不降就把学习率减半这个保护对残差类网络非常关键。还有一个版本兼容性问题。logs 目录里那个 events.out.tfevents.1531822717.DESKTOP-754RGN1 是 TensorFlow 早期日志格式这个时间戳对应 2018 年左右原始代码大概率基于 TensorFlow 1.x。如果你现在用 TensorFlow 2.x 打开模型代码里没有 tf.compat.v1 的兼容写法会报错需要把 import 调整成import tensorflow.compat.v1 as tf tf.disable_v2_behavior()或者按本文代码改成 tf.keras 高层 API就不需要 v1 兼容层。日志读取上新版 TensorBoard 对老格式一般能直接读但版本跨度太大时会空白具体的排查在第 4 章。3.3 单图推理与日志复盘events文件怎么用、模型怎么落地训练完拿到 ckpt/rnuc_best.h5单图推理的代码很简短# 加载训练好的权重并推理 import cv2 import numpy as np from models import build_rnuc model build_rnuc() model.load_weights(ckpt/rnuc_best.h5) img cv2.imread(test/striped.png, cv2.IMREAD_GRAYSCALE).astype(np.float32) / 255.0 out model.predict(img[None, ..., None])[0] cv2.imwrite(result/corrected.png, (out[..., 0] * 255).astype(np.uint8))predict 时输入要补两个维度一个 batch 维一个通道维img.shape 从 (256, 256) 变成 (1, 256, 256, 1)。输出还要把附加维度去掉再写图这是新手最容易报 shape 错误的地方。推理结果保存为 PNG不要用 JPEGJPEG 的有损压缩会引入新的条纹伪影干扰效果评估。日志复盘看 TensorBoardtensorboard --logdirlogs打开 http://localhost:6006 看 Scalars 页面。我主要看训练 loss 和验证 loss 的关系。健康形态是同步下降验证 loss 比训练略高过拟合形态是训练一路走低、验证到某轮开始反弹这时回退 epoch 或加强数据增广不收敛形态是 loss 很慢地跌到平台就完全不动多半是学习率太小或条纹生成方式太单一。logs 路径有中文用户名时 TensorBoard 可能读不到把 logdir 换成纯英文绝对路径是逐步排查时的常见解法。4. 训练避坑与常见问题排查五个高频现场逐条拆4.1 现象训练到300轮左右loss震荡验证loss不再下降原因Adam 在残差网络的平台期还用相对较高的学习率持续更新参数损失面后期很平缓学习率不降下来就会在最优值附近来回摆动loss 曲线像锯齿一样起伏。解决给优化器挂学习率衰减。我用的是 ReduceLROnPlateau监测 val_losspatience 给 20factor 给 0.5学习率在平台期自动降档。如果衰减后还震荡检查 BN 的 momentum默认 0.99 在 batch size 小于 16 时统计不稳定调到 0.995 能缓解。另一种情况是学习率降到最后仍不收敛那就不是调参问题是训练数据里的条纹模式太少回到第 3 章把 gain_range 和 col_range 范围加大重新生成。4.2 现象loss很低但校正图上还能看到竖条纹痕迹原因MSE 损失对条纹这类高频分量不敏感。网络学到的是整体亮度补偿条纹被压淡但没有被干净消除。本质原因是像素级 MSE 把“亮度差异”和“条纹结构差异”混在一个标量里条纹占整幅图的比例小梯度被平滑区域的误差稀释。解决在损失里加一个列方向平滑项惩罚校正图在横轴方向的突变。竖条纹是沿垂直方向延伸的差异发生在相邻列之间所以要沿宽x方向做差分# 额外损失项列方向平滑性约束 def column_smoothness_loss(y_true, y_pred): # 沿宽方向差分捕获相邻列之间的跳变 diff tf.abs(y_pred[:, :, :, 1:] - y_pred[:, :, :, :-1]) return tf.reduce_mean(diff) model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-4), losslambda y_true, y_pred: tf.reduce_mean(tf.square(y_true - y_pred)) 0.05 * column_smoothness_loss(y_true, y_pred) )0.05 是个起始权重我一般从 0.01 试起。每调大一档条纹更干净但图会微微变糊。如果发现边缘开始模糊把权重降回改用 L1 损失替代 MSE 作为主损失L1 对条纹轮廓的梯度响应更直接保留细节能力更强。4.3 现象TensorBoard空白events文件存在但曲线不显示原因两种最常见。一是 TensorFlow 和 TensorBoard 版本不匹配早期 tfevents 格式用最新版 TensorBoard 读取偶发空白二是 logdir 传成了具体文件路径比如 tensorboard --logdirlogs/events.out.tfevents.xxx而 TensorBoard 要求传目录目录下有以 events.out.tfevents 开头的文件才会被扫描。解决先确认传的是目录再看路径里有没有中文。原来这个仓库的机器名是 DESKTOP-754RGN1Windows 下用户名如果是中文日志路径会带上中文字符TensorBoard 扫描时会漏。把整个 logs 目录拷到 D:\tb_logs 这种纯英文绝对路径下基本能解决。还不行就在训练脚本里强制指定日志目录TensorBoard(log_dirD:/tb_logs/rnuc_run001, histogram_freq1)顺带一提histogram_freq1 会把每层权重的分布也写进日志答辩时展示网络在训练过程中权重分布的变化比单纯一条 loss 曲线更有说服力。4.4 现象换实拍红外图后校正结果发糊边缘细节丢失原因训练样本是合成条纹模型学到的噪声分布偏向训练时的条纹形态。真实 IRFPA 的条纹宽度、盲元分布、随机噪声叠加更复杂模型把真实图中的高频细节也当条纹减掉了。这是合成数据与真实数据分布不一致导致的常见失效。解决一靠数据增广训练时随机水平翻转、垂直翻转、亮度抖动让网络对条纹方向更稳健。注意条纹是列方向的水平翻转会改变条纹的左右分布垂直翻转不会改变竖条纹形态两种翻转都要做等价于扩充样本多样性。二靠推理时用重叠 patch 方法把 256×256 图切成 128×128 的块块之间重叠 8 个像素逐块推理后拼回重叠区取平均值。这会削弱拼接缝也降低边缘重影。附加好处是显存占用小batch size 可以给更大。4.5 现象batch size一调大就OOM崩溃原因红外图虽然是单通道但合并式特征提取单元同时跑 5×5、7×7 卷积合并后特征图通道数是输入通道的 3 倍显存占用并不像单通道输入看起来那么小。以 256×256 输入、filters32 计算中间特征图尺寸约 256×256×96×4 字节约 25MB 一帧叠上 batch、梯度、优化器状态16 的 batch size 已经不算保守。解决优先把训练输入尺寸从 256×256 改成 128×128模型结构和代码不用动显存占用直接降为四分之一。filters 从 32 降到 16参数量减半效果损失有限。还可以用混合精度训练在模型编译前加一句tf.keras.mixed_precision.set_global_policy(mixed_float16)显存占用大约再降三成。混合精度有个副作用BN 在 float16 下统计方差不稳训练时如果出现 loss 为 nan先关掉混合精度排查不要在 nan 状态下硬调参数。推理时如果显存也紧张把输入切成 128 块的 patch 方式同样适用并且对大图效果更友好。5. 效果验证与进阶做法残差直方图、PSNR与消融实验把住质量关5.1 残差图和PSNR双指标验证训练好的模型不能只看训练 loss。我习惯先算残差原图减去校正图得到模型预测的噪声分量。如果网络学到了条纹残差图应该是一张只包含列向条纹和极少量随机噪声的图如果残差图里出现了物体边缘轮廓说明网络把真实细节误删了校正图细节一定受损。残差直方图更直观正常残差分布接近以 0 为中心、展宽很小的高斯分布如果直方图出现双峰或多峰说明校正不彻底还有残余的结构性条纹。# 快速PSNR与SSIM评估 import numpy as np import cv2 from skimage.metrics import structural_similarity as ssim def evaluate(model, noisy, clean): pred model.predict(noisy[None, ...])[0] mse_val np.mean((pred - clean) ** 2) psnr_val 10 * np.log10(1.0 / mse_val) ssim_val ssim(pred, clean, data_range1.0) return psnr_val, ssim_val, pred这里要注意noisy 和 clean 都得是 [0,1] 的 float32PSNR 公式分子才是 1.0。如果数据还是 0 到 255分子要改成 65025。毕业论文里通常对比三组 PSNR原条纹图对干净图、校正图对干净图、两点校正图对干净图。校正图相比原图提升 3dB 以上肉眼才能明确感到“校正有效果”。5.2 控制变量消融证明两个残差块都有存在价值答辩时老师最常问“为什么用两个残差块”。提前做消融实验比临场解释可靠。把第二个残差块注释掉保持其余结构、数据、超参完全不变重新训练同一轮数在验证集上测 PSNR 和 SSIM记录成对比表。同理可以消融合并式特征提取单元——把三路拼接改成单路 3×3 卷积看 PSNR 掉多少。这种控制变量实验直接对应毕业论文的实验章节图表都有说服力而且跑起来成本极低模型小一次训练十分钟级。做过几次这类项目后我的习惯是训练完成后强制走一遍残差直方图检查如果残差里还有竖条纹分量说明损失函数权重没调到位回到第 4 章的平滑项重新调参而不是直接换网络结构。从那以后我每次跑非均匀性校正项目都会先把这条验证链路固定下来再开始训练省下大量返工时间。希望帮到你。本文还有配套的精品资源点击获取