
简介一份2022年本科毕业设计源码以Python和卷积神经网络CNN解决红外图像非均匀性校正问题。该课题面向红外传感器成像中因温度、器件性能导致的亮度不一致适合计算机视觉与深度学习方向的本科生、毕业设计学生及入门工程师参考。压缩包共4个文件包含3个Python脚本和1个Markdown说明文档脚本覆盖模型结构、主执行流程与辅助工具函数文档梳理项目组织与运行逻辑整体仅5KB轻量精炼便于快速阅读与二次修改。源码完整保留RNUC-main工程骨架并结合卷积层、池化层、全连接层等典型CNN模块以及灰度化、归一化、对比度增强等预处理思路展示了模型训练、验证与结果可视化的大致流程。已有366人学习可作为课程设计或毕业设计中的直接参照帮助理解红外图像校正任务从数据准备到模型评估的落地方法。1. 红外图像非均匀性校正CNN 为什么成了无黑体定标的可行解晚上打开红外相机画面里横着几条亮带、竖着一道道条纹这不是镜头脏了而是每个探测像元的响应不一致。这种“固定图案噪声”在非制冷红外探测器上尤其明显温度一变、供电一抖条纹就跟着变。传统做法是拿黑体标定隔一会就得挡一下镜头重新定标碰上目标和场景一直在变这套流程基本没法用。这个课题做的就是把红外图像非均匀性校正交给卷积神经网络来做输入一张 raw 红外图直接输出一张条纹少、细节还在的干净图不依赖黑体、不打断观测流程。对于本科毕设来说这个方向的好处是数据集好造、网络不用太大、训练和推理都能在一张消费级显卡上跑完同时又有“能不能压住真实场景噪声”这个研究点可以深挖适合想做深度学习和图像信号处理交叉方向的人起步。2. 数据准备与预处理把单帧红外图拆成“干净图条纹噪声”的训练对做这类校正任务第一个绕不开的问题是没有成对的干净图。真红外相机拍出来就是带噪声的哪来的 ground truth所以常见做法是走合成路线找一批清晰红外图像当基底人为加上非均匀性模型来生成带噪声的图让网络去学反向映射。2.1 合成非均匀性的两种常用套路行均值模拟与环境噪声叠加非均匀性噪声在图像上主要表现为两类一是列条纹因为探测器读出电路的列增益和偏置不一致二是低速浮动的非均匀响应像云团一样叠加在背景上。合成时我会同时加这两类只用一列随机数的话网络学得很轻松实际一测就翻车。import numpy as np import cv2 from pathlib import Path def add_column_nuc_noise(img, col_offset_std15, gain_std0.03): h, w img.shape[:2] img img.astype(np.float32) / 65535.0 # 1) 列固定模式噪声每一列单独一个偏置和一个增益 offset np.random.normal(0, col_offset_std / 65535.0, size(1, w)) gain np.random.normal(1, gain_std, size(1, w)) # 2) 低速漂移噪声用正弦叠加模拟暗电流热漂移 drift 0.04 * np.sin(np.linspace(0, 12 * np.pi, w)).reshape(1, -1) # 3) 行列耦合的高斯本底噪声 h_noise np.random.normal(0, 3 / 65535.0, size(h, w)).astype(np.float32) corrupt img * gain offset drift h_noise return np.clip(corrupt, 0.0, 1.0).astype(np.float32)这里col_offset_std15表示把 15 个灰度级16bit 图的偏移加到列上这个量级贴近真实非制冷探测器在常温下的表现gain_std0.03是 3% 的增益波动再往上加图像边缘就明显发花。正弦项的幅度 0.04 决定了漂移型噪声的强度实际标定时这个值波动很大训练集里如果固定一个幅度网络会学着只消固定模式遇到真实场景就失灵。我会把它设在 0.02~0.08 之间随机采样增强鲁棒性。合成完要和原图一起导出成 numpy 数组或者直接从内存进 DataLoader。数据集规模不用太大红外图像纹理相对单一3000 对就够训练一个小网络再多容易让网络把细节也滤掉。2.2 预处理流程裁剪、归一化与数据增强拿到原始红外图以后预处理我只保留四步降采样到统一尺寸、裁剪出有效区域、归一化到 0~1、随机水平翻转。红外图像边缘经常有坏点或者探测器盲元训练时把边缘裁掉 32px避免网络去学坏点消除否则推理时边缘会出现一圈亮边。def preprocess_pair(clean_img, corrupt_img, size(256, 256)): h, w clean_img.shape[:2] clean_img clean_img[32:h-32, 32:w-32] corrupt_img corrupt_img[32:h-32, 32:w-32] clean_img cv2.resize(clean_img, size, interpolationcv2.INTER_AREA) corrupt_img cv2.resize(corrupt_img, size, interpolationcv2.INTER_LINEAR) clean_img clean_img.astype(np.float32) / 65535.0 corrupt_img corrupt_img.astype(np.float32) / 65535.0 if np.random.rand() 0.5: clean_img np.fliplr(clean_img) corrupt_img np.fliplr(corrupt_img) return corrupt_img[None, ...], clean_img[None, ...]归一化统一用 65535 而不是最大值归一化这一点很多从 python 入门开始写代码的人容易忽略。最大值归一化会把每张图的亮度拉平损失了绝对温度信息模型学到的去噪强度和亮度相关真实场景亮度变化一大就失效。固定除 65535 保持物理单位一致训练时 loss 的数值也更稳定。2.3 数据检查脚本先看噪声形态再决定训练策略建议训练前先跑一个检查脚本把合成噪声的图像按行求均值画出来确认条纹确实在数据里存在。这一步比调参更重要因为非均匀性噪声的方向决定了卷积核的宽高比例。def inspect_row_mean(img): row_mean img.mean(axis1) col_mean img.mean(axis0) print(row std:, row_mean.std(), col std:, col_mean.std())如果 col_mean 的方差远大于 row_mean说明列条纹占主导卷积核在水平方向要加宽反过来则是行条纹主导竖长卷积核更有效。我在训练第一个版本时没做这个检查默认拿了 3×3 卷积核去跑结果条纹是消了图像纹理也跟着糊。实际看红外图时列条纹居多所以后续设计网络把宽度方向优先放大。3. 卷积神经网络结构与选型从条纹噪声特性反推卷积核设计网络结构这部分直接上深层 ResNet 反而是错的。红外非均匀性校正是低频占主导的图像修复任务不需要太深的语义理解关键是感受野要覆盖噪声的横向跨度。写毕设的话把网络控制在 3 万参数左右训练快、显存小、论文里也能把结构图画得很清爽。3.1 为什么残差结构比直接回归更适合 NUC直接让网络输出校正图本质上是在回归一张 8bit 或 16bit 的灰度图网络要重建全部细节难度高容易把锐利边缘学糊。换一种思路网络只预测噪声图然后用输入减去噪声图这种残差学到的映射要简单得多。因为干净图是输入的一部分网络只需要找出“多余的那层薄膜”。这里对应到常见的残差结构网络学习的是一个高维空间的增量。在 NUC 任务里这个增量就是列偏移和增益残差量级更小、模式更固定训练收敛速度和最终效果都会好一截。3.2 卷积核形状、通道数与感受野的取舍前面说过列条纹是水平方向分布的所以第一层卷积核不应该用方形。列条纹在空间上表现为“同一列整条偏移”要让卷积核看到整列的上下文宽度方向要拉长我一般用 3×9 或 3×11。高度方向保留 3 就行因为上下相邻像素在自然图像里相关性高但条纹噪声并不是靠纵向范围识别的。通道数也值得说道。第一层 16 个通道够用加太多通道只会让网络学到更多和条纹无关的细节推理时可能过拟合到特定场景的纹理上。第二层可以缩到 12 或者保持 16 不变。整个网络就是两个卷积加一个残差连接的轻量结构。3.3 模型定义代码一个 3 万参数量的轻量网络用 PyTorch 写这个结构很简单四层卷积就够。import torch import torch.nn as nn class LightNUCNet(nn.Module): def __init__(self, in_ch1, hidden_ch16): super(LightNUCNet, self).__init__() self.block nn.Sequential( nn.Conv2d(in_ch, hidden_ch, kernel_size(3, 9), padding(1, 4)), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(hidden_ch, hidden_ch, kernel_size(3, 7), padding(1, 3)), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(hidden_ch, hidden_ch, kernel_size(1, 5), padding(0, 2)), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(hidden_ch, out_ch1, kernel_size(3, 3), padding(1, 1)) ) def forward(self, x): noise self.block(x) return x - noise两个长条卷积核负责在水平方向抓条纹中间 1×5 的卷积进一步压缩噪声估计中的高频成分最后一个 3×3 恢复到逐像素输出。整个前向只有四次卷积画卷积神经网络结构图时层次很清楚。参数说明padding在长条卷积里设成(1, 4)是保证输出尺寸不变默认hidden_ch16时模型参数量约为 2.8 万单张 256×256 输入在 1080Ti 上推理不超过 3ms。如果把 hidden_ch 提到 32去噪能力会强一点但边缘保留能力明显下降因为这个层宽开始足够记住场景纹理了。4. 训练与推理落地损失函数、训练参数与多帧时序处理网络结构定了后面拼的是训练策略。NUC 校正任务里 loss 设计比网络结构对结果影响更大只用 MSE 容易让图像变糊只用 L1 又守不住灰度均值。真实场景图像的高亮区域和暗部都需要保留所以损失函数要做组合。4.1 损失函数组合L1 保纹理、梯度差压噪声、SSIM 守边界我一般用 L1 边缘梯度差 一个轻量 SSIM 的加权和。L1 是主损失负责整体灰度逼近梯度差负责约束噪声区域不要产生新的假边缘SSIM 项只在训练后 30 个 epoch 才加入前 80 个 epoch 用不上。class NUCCombinedLoss(nn.Module): def __init__(self, alpha1.0, beta0.2, gamma0.1): super(NUCCombinedLoss, self).__init__() self.alpha alpha self.beta beta self.gamma gamma def forward(self, pred, clean, corrupt): l1_term torch.abs(pred - clean).mean() grad_pred_x torch.abs(pred[:, :, :, 1:] - pred[:, :, :, :-1]).mean() grad_clean_x torch.abs(clean[:, :, :, 1:] - clean[:, :, :, :-1]).mean() grad_term torch.abs(grad_pred_x - grad_clean_x) ssim_term 1.0 - self._ssim(pred, clean) return self.alpha * l1_term self.beta * grad_term self.gamma * ssim_term def _ssim(self, pred, clean): # 简化的 11x11 高斯 SSIM这里演示用法 from torch.nn.functional import avg_pool2d c1, c2 0.01 ** 2, 0.03 ** 2 mu_pred avg_pool2d(pred, kernel_size11, stride1, padding5) mu_clean avg_pool2d(clean, kernel_size11, stride1, padding5) sigma_pred avg_pool2d(pred * pred, 11, stride1, padding5) - mu_pred ** 2 sigma_clean avg_pool2d(clean * clean, 11, stride1, padding5) - mu_clean ** 2 sigma_cross avg_pool2d(pred * clean, 11, stride1, padding5) - mu_pred * mu_clean ssim_map ((2 * mu_pred * mu_clean c1) * (2 * sigma_cross c2)) / \ ((mu_pred ** 2 mu_clean ** 2 c1) * (sigma_pred sigma_clean c2)) return ssim_map.mean()beta0.2意味着梯度差在主损失的 20% 量级作用是在边缘结构上轻微约束不会压掉真实纹理。gamma0.1是后期微调用的SSIM 收敛慢一开始加了它反而干扰训练所以我选择在前 80 个 epoch 里设成 0。4.2 训练脚本与关键参数设置训练参数方面经过多轮对比收敛最稳的组合是 Adam 1e-3 峰值学习率 cosine 衰减 batch size 32。单卡 V100 显存完全够用批量大小不要小于 16太小 batch 的梯度方向不稳定损失曲线会来回弹。device torch.device(cuda if torch.cuda.is_available() else cpu) model LightNUCNet().to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max100, eta_min1e-6) loss_fn NUCCombinedLoss(alpha1.0, beta0.2, gamma0.0) for epoch in range(100): model.train() total_loss 0.0 for step, (corrupt, clean) in enumerate(train_loader): corrupt, clean corrupt.to(device), clean.to(device) pred model(corrupt) if epoch 80: loss_fn.gamma 0.1 loss loss_fn(pred, clean, corrupt) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm0.5) optimizer.step() total_loss loss.item() scheduler.step() if epoch % 10 0: print(fepoch {epoch}, loss {total_loss / max(step, 1):.5f})weight_decay1e-5是一个很容易被漏掉的细节对 3 万参数的网络来说正则化强度很轻但能压住偏置项在长训练里的累积漂移。clip_grad_norm_设置在 0.5主要是防个别批次的异常噪声样本把梯度顶飞红外图里有坏点时经常出现这种情况。4.3 推理脚本与帧间去闪烁处理单帧推理耦合简单把 16bit raw 图喂进网络再乘回去就行。但监控场景是连续视频流逐帧独立校正会出现一个很尴尬的视觉效果条纹消失但图像像在“水面上浮动”每帧灰度整体轻微跳变。这个问题前面描述可能不直观等你跑起来就能看到专业上叫 temporal flicker。处理这个问题的常见做法是对模型输出的列均值做时域低通。每帧算完结果后用一个滑动窗口去平滑当前帧和前面几帧的列均值。import numpy as np buffer [] def temporal_smooth_output(pred_np, window_size5): buffer.append(pred_np) if len(buffer) window_size: buffer.pop(0) col_means np.stack([frame.mean(axis0) for frame in buffer], axis0) smoothed_col_mean col_means.mean(axis0) # 只调整列均值保留网络输出的高频纹理 return pred_np - pred_np.mean(axis0, keepdimsTrue) smoothed_col_mean[None, :]这个逻辑不修改像素本身的纹理只把每列的亮度基线对齐到历史均值上闪烁感会明显下降。窗口大小 5 在 30fps 视频里对应 0.16 秒的滞后肉眼基本感知不到拖影。如果场景是快速运动的窗口要降到 3否则运动物体会拉出亮度残影。5. 复现与训练避坑五个高频失败的现查修法这个项目跑起来不难但很多人在复现时会在同一个地方反复踩坑。下面按实际频率从高到低列五个每一条都是真实发生过的问题照着排查能省一两天时间。5.1 现象一校正后图像出现横向水波状条纹训练完成后在验证集上效果很好拉去打真实视频画面出现横向波纹像水面反光一样滚动。原因是合成噪声和真实噪声分布不一致。合成时如果只用高斯偏移网络只需要学一个线性减法。真实探测器噪声里还有周期性成分尤其自带 AGC 的机器条纹是分段线性的网络没有见过这种模式就会把周期性残差放大。解决方法是把合成噪声模型改成分段线性在行方向分 8 段或 16 段随机调整 offset模拟真实探测器每个通道动态范围的扭曲。我改完这个之后现场测试的水波纹立刻减弱正常情况下你的噪声合成代码里也应保留一个分段函数入口。5.2 现象二输出比输入更亮/更暗整体灰度漂移训练 loss 已经很低但校正后的整体亮度比 raw 图低了一大截查看像素直方图发现分布整体左移。原因在于数据预处理里除的是 65535而模型输出经过了 clip合成噪声的负向偏移没有被完全补偿残差项泄漏到整体灰度上。更深层的原因是归一化时没有记录图像的“零漂移基线”。解决方法是把网络输入的均值也作为一个训练时保真的目标我通常在输出层后面加一个全局平均池化的残差连接让网络自己学习全局亮度补偿。推实时图的时候可以额外把输出图的整体均值强制拉回输入图的均值一劳永逸地保证亮度不漂。5.3 现象三训练 loss 不降但输出看起来没变化这种情况最容易让人怀疑是网络结构错了实际上网络已经学到了恒等映射。原因通常出现在损失函数设计上梯度差项和 SSIM 项如果权重过大网络发现最简单的减风险是直接复制输入因为这些项在输入和输出完全相同的时候是极小值。L1 项在图像干净时的数值也很小不足以推着网络去消除微弱条纹。解决方法是观察训练 log 中各项损失分别的变化如果 L1 在下降但梯度差项在 0 附近不动代表网络在复制输入。把beta调到 0.5 以上或者先在合成噪声很强的数据集上预训练 20 个 epoch让网络先学会压低噪声再切回正常数据。5.4 现象四换一个场景后校正效果变差在装机的现场效果很好换到另一个厂区、另一台相机上条纹回来了还有双重条纹。原因就是典型的域偏移。每台探测器的非均匀性模式不一样光照、温度、增益档位都不一致。如果你的目标是单机单场景这一条可以忽略但论文里往往会写“泛化性验证”在多个场景间交叉测试。解决方式是做“域随机化”训练。在合成噪声参数上加入随机化的增益范围和 offset 范围让网络不再执着于特定列噪声。另外可以在输入上做一个随机 0.9~1.1 的全局亮度扰动增强网络对输入动态范围的适应。真实场景测试时如果还是差我会在推理前用前 30 帧的列均值对输入图做一次粗校正把大条纹先削掉再把残差教给网络。5.5 现象五CPU 推理太慢帧率上不去模型明明只有 3 万参数在笔记本 CPU 上跑 640×512 分辨率要 80ms 一帧算下来只有 12fps达不到实时。原因是卷积核是 3×9 和 3×7 的长条结构虽然参数量少但计算量并不低。再加上没做任何优化PyTorch 在 CPU 上默认走的是通用的 GEMM 路径。解决措施有三层第一层是推理时把模型转成 TorchScript减少 Python 调度的开销第二层是打开torch.backends.mkldnn.enabled TrueMKLDNN 对长条卷积也有加速第三层是把图像先降采样到 320×256 推理然后上采样残差噪声、叠加回原图噪声本身是低频的下采样不会损失条纹信息。这一套组合下来CPU 帧率能到 40ms 左右。如果你对实时性还有更高要求就得走 NCNN 或者 ONNX Runtime 的线程优化了。6. 进阶用评价指标验证校正效果再把模型压到能跑实时模型训练完不能只看 loss 曲线人眼在条纹图像上容易被欺骗必须看客观指标。三个最实用的指标列均值曲线平滑度、噪声抑制系数NSF、边缘保留指数EPI。列均值曲线直接反映条纹是否被消除NSF 衡量输出图像粗糙度的下降幅度EPI 则用来检查网络有没有把边缘细节一并滤掉。def evaluate_nuc(clean_img, pred_img): row_clean clean_img.mean(axis1) row_pred pred_img.mean(axis1) col_clean clean_img.mean(axis0) col_pred pred_img.mean(axis0) nsf clean_img.std() / pred_img.std() epi_x np.abs(clean_img[:, 1:] - clean_img[:, :-1]).mean() / \ np.abs(pred_img[:, 1:] - pred_img[:, :-1]).mean() return { col_std_before: col_clean.std(), col_std_after: col_pred.std(), nsf: nsf, epi_x: epi_x, }col_std_after应该远小于col_std_before如果你观察到相反说明网络在制造噪声。EPI 大于 1 表示输出比输入边缘更强但并不意味着更好边缘增强过度会引入伪影0.9~1.0 之间是理想值。模型落地时我习惯把训练好的权重转成 ONNX再用 int8 量化跑在边缘设备上。量化需要注意一个问题激活值分布范围太宽会导致精度下降可以在校准集上统计输入输出范围把模型的输入输出层保持 fp32只量化中间卷积层。用这个方法能在保持视觉效果不变的前提下把推理速度推到嵌入式设备的 30fps 裸跑水平。因为我多半是在拿到新硬件时先把它的 IR 框架跑通再用量化后的模型做帧率摸底这几乎成了我在新平台上必然先做的一个验证步骤。希望这些思路能帮你在这个课题上少走一段弯路也祝你在复现时少翻几次车。本文还有配套的精品资源点击获取