简介这份2022年本科毕业设计源码面向红外成像与深度学习方向的毕设学生及图像处理入门者围绕Python与卷积神经网络实现红外图像非均匀性校正覆盖从图像预处理、CNN特征提取、模型训练与优化到验证测试与结果可视化的完整流程可帮助读者理解“端到端”解决红外图像亮度不均问题的技术路线。压缩包共4个文件以3个Python脚本和1个Markdown说明文档为主整体约5KB脚本分别承担模型定义、主流程实现与工具函数等角色说明文档则用于梳理项目结构与使用方式。已有366人学习下载特别适合需要参考毕业设计项目框架、复现校正算法或快速上手CNN图像处理任务的读者也可作为毕设选题、课程设计与工程实训的算法基线便于快速定位代码并开展二次开发。1. 红外图像非均匀性校正毕设源码里最值得复用的是这条技术路线打开一个名为“Python基于卷积神经网络的红外图像非均匀性校正”的毕设源码包第一反应通常是先找main.py按顺序跑一遍。但这类源码包的共同特点是以“能出图、能过答辩”为目标真正可复用的不是那一整套工程而是“把非均匀性校正当作图像去噪任务用 CNN 在合成数据上学条纹噪声映射”这条思路。红外焦平面阵列每个像元的响应不一致导致画面出现固定的条纹和块状图案温度越低越明显。传统两点校正需要挡片基于场景的统计方法对运动场景敏感而卷积神经网络可以直接学习退化图到干净图的映射不需要额外硬件。这篇笔记会按“原理—数据—网络—训练—踩坑—验证”的顺序把这条路线拆成可以照着抄的步骤适合正在做毕设选型的学生也适合刚接触红外图像算法的工程师。如果你已经装好了 Python 和 PyTorch最快半小时能把一个最小可训练的模型跑起来。2. 先立住思路非均匀性噪声模型与卷积神经网络的对应关系2.1 非均匀性校正的三种路线与毕设方案的选型理由红外焦平面阵列IRFPA的每个像元在相同辐照下输出的电压不完全相同这种响应差异在画面上表现为固定的条纹或网格状图案。工程上解决这个问题有三类路线路线典型方法前提条件主要问题标定类两点校正、多点分段校正需要黑体或挡片提供均匀辐射源标定设备贵探测器老化后校正参数失效基于场景类时域高通滤波、恒定统计法场景灰度分布相对稳定需要连续帧静止场景会“鬼影”运动缓慢时校正收敛慢学习类CNN 回归、GAN 配对去条纹训练阶段需要配对数据依赖数据合成质量实际场景泛化要看数据多样性毕设源码包选择 CNN 路线核心原因是它省掉了挡片和黑体这类额外硬件把校正问题变成“从退化图像到干净图像”的监督学习问题。训练阶段用合成方式制造带条纹的退化图推理阶段直接把单帧红外图交给网络输出。这个思路能过答辩同时工作量容易拆数据集构造、网络设计、训练调参、指标对比每一块都有明确产出。需要说清楚的是CNN 校正并不能替代探测器出厂前的非均匀性标定它做的是“残余条纹的进一步抑制”。如果把两点校正比作粗调CNN 校正更像是细调。源码包里如果能直接读取原始 14bit 或 16bit 红外数据效果会明显好于先压缩成 8bit 再做校正——这一点后面避坑章节会展开。2.2 从退化模型到网络设计为什么用残差结构红外图像的非均匀性可以建模成I(x, y) g(x, y) · S(x, y) b(x, y) n其中 S(x, y) 是理想场景辐射g(x, y) 和 b(x, y) 分别是每个像元的增益和偏置n 是随机噪声。条纹感主要来自读出电路逐列/逐行扫描引入的列方向增益差异所以退化函数在空间上是低频的但在列方向上呈现规律性。CNN 要学习的是 g 和 b 的组合效果也就是从退化图 I 估计条纹分量再得到校正图Ŝ I − f(I, θ)这个思路对应的是残差学习而不是直接回归输出完整图像。原因是校正前后图像的主体结构高度相似网络只需学习一个低幅度、高结构化的条纹分量比学习整张图更容易收敛。把网络设计成残差形式也方便在现有模型上叠加更深的模块而不用担心梯度消失。网络结构图上能看到典型的编码器—解码器骨架。考虑到非均匀性条纹在空间上是稠密且全局分布的单层卷积要覆盖完整条纹宽度需要很大的卷积核不如用多层小卷积核叠加来扩大感受野。一个标准的残差块设计包括两次 3×3 卷积、BatchNorm 和 ReLU中间用跳跃连接把输入加到输出上。在毕设这个体量下不需要引入注意力机制或 Transformer 这些复杂结构基础卷积堆叠加残差连接已经能把合成条纹压下去同时训练稳定、显存占用低。下面给一个可复用的残差块实现这也是源码包里最常见的基础组件写法import torch import torch.nn as nn class ResBlock(nn.Module): def __init__(self, in_channels, out_channels, stride1): super().__init__() self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.shortcut nn.Sequential() if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels) ) def forward(self, x): out self.conv1(x) out self.bn1(out) out self.relu(out) out self.conv2(out) out self.bn2(out) out self.shortcut(x) out self.relu(out) return out这段代码的要点在跳跃连接。shortcut是空的时候输入输出通道数一致恒等映射直接相加当特征图尺寸或通道数变化时用 1×1 卷积做投影保证相加的两个张量形状完全一致。biasFalse BatchNorm 是常见搭配因为 BatchNorm 层自带偏置项卷积层再设 bias 会造成冗余。实际训练时如果发现验证集损失一直不降优先查这里是不是通道数没对上——很多毕设源码的翻车点就出在 shortcut 分支的维度错误上。整个网络结构可以按“3 层卷积下采样 4 个残差块 转置卷积上采样”来搭输入输出都是单通道灰度图。这个结构谈不上新颖但胜在稳PyTorch 官方文档里的图像分类模型也都是同样的构建思路。源码包里的网络如果编成了 50 层以上先不要急着复现——红外条纹校正不是 ImageNet 分类参数越多反而越容易在小数据集上过拟合把条纹“背下来”。2.3 感受野与条纹宽度的对应关系非均匀性条纹在红外图里的宽度通常在 1 到 10 个像素之间取决于探测器的读出电路和坏点簇大小。网络每一层卷积的有效感受野是叠加的3×3 卷积堆叠三层感受野是 7×7堆到五层是 11×11。合成数据时要统计出条纹的平均宽度反推网络深度这是一个很实用的选型依据。如果合成数据里的条纹是 8 像素宽的带状噪声而网络感受野只有 5 像素会出现训练损失很低、但输出图里仍有隐约条纹的情况——因为网络只学到了条纹的边缘没学到条纹内部完整的灰度偏移。解决方式有两种扩大卷积核到 5×5或者增加卷积层数。优先选择增加层数而不是加大卷积核因为 5×5 卷积的参数量是 3×3 的 2.78 倍在只有一两千张训练图的数据集上更容易过拟合。3. 构造训练数据自制红外成对数据集的关键是让模拟噪声足够真3.1 真实红外图做底图滑窗完整切块与集外验证CNN 训练需要配对数据退化图带条纹和干净图不带条纹。现实中没有仪器能同时拍一帧“带条纹”和一帧“不带条纹”的红外图所以几乎所有的毕设方案都是用干净的红外图加合成噪声来生成退化图。先说干净图从哪来。推荐用公开的红外数据集或自己用红外相机采集只要原始图是 16bit不要经过强压缩。最忌直接用网上找的 8bit JPEG 红外照片因为 JPEG 压缩本身会引入块效应模型会把压缩噪声当成条纹的一部分学进去。拿到干净图后滑动窗口切成小块每块作为一个干净样本。下面这段代码是滑窗切块的实现也是整个数据管线的基础import cv2 import numpy as np import os from glob import glob def extract_patches(image_path, save_dir, patch_size256, stride128): os.makedirs(save_dir, exist_okTrue) img cv2.imread(image_path, cv2.IMREAD_UNCHANGED) if img.dtype np.uint16: img (img.astype(np.float32) / 65535.0) elif img.dtype np.uint8: img img.astype(np.float32) / 255.0 h, w img.shape[:2] idx 0 for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): patch img[y:ypatch_size, x:xpatch_size] if patch.shape[0] ! patch_size or patch.shape[1] ! patch_size: continue np.save(os.path.join(save_dir, fclean_{idx:05d}.npy), patch) idx 1 print(f{os.path.basename(image_path)}: {idx} patches) if __name__ __main__: for p in glob(raw_images/*.tiff): extract_patches(p, patches, patch_size256, stride128)这段代码的逻辑是用IMREAD_UNCHANGED读入 16bit 原始图归一化到 [0,1] 区间再保存成.npy避免每次训练时重复做归一化。patch_size256, stride128意味着相邻 patch 有 50% 重叠一张 640×480 的红外图大约能切出 18 个 patch。如果图只有 320×240建议把 patch_size 降到 128否则一张图切不出几个 patch数据集规模不够。这里保留的是 16bit 归一化流程对应实际红外设备常见的 14bit/16bit 输出。有一点容易被忽略滑窗切块时要记录每个 patch 来自哪张原图。训练集和验证集的划分要按“原图”划分而不是按“patch”划分否则同一张图的相邻 patch 会同时出现在训练集和验证集里验证集指标会虚高。后面训练部分会看到这个细节直接影响你对模型泛化能力的判断。3.2 生成模拟条纹噪声列/行增益偏差与像元噪声分开加合成噪声的方式直接决定模型能不能学到物理上真实的条纹。看源码包时第一件事就是检查它的退化模型是“只看列方向”还是“列 行 随机像元”都覆盖。只加列方向的乘性噪声训练出的模型拿到真实探测器图像上往往会“去不干净”因为真实器件的条纹既有列方向增益差也有像元级偏置差。推荐的噪声模型分三层列方向乘性增益、行方向乘性增益、随机像元加性偏置。列噪声用某一列的增益系数整体偏大或偏小来模拟行噪声同理像元噪声则独立随机。写一段可以复用的噪声注入代码import numpy as np def generate_degraded(clean, noise_level0.1, stripe_width1): h, w clean.shape[:2] degraded clean.copy() col_gain 1.0 np.random.uniform(-noise_level, noise_level, size(1, w, 1)) row_gain 1.0 np.random.uniform(-noise_level * 0.3, noise_level * 0.3, size(h, 1, 1)) pix_bias np.random.uniform(-noise_level * 0.02, noise_level * 0.02, size(h, w, 1)) # 对单通道灰度图做广播乘法 degraded degraded * col_gain * row_gain pix_bias # 模拟条纹宽度对列增益做邻域平滑产生带状条纹 if stripe_width 1: kernel np.ones((1, stripe_width, 1)) / stripe_width col_gain_smooth col_gain # 实际使用时可用 scipy.ndimage.uniform_filter 替代 degraded clean * col_gain_smooth * row_gain pix_bias return degraded.clip(0, 1).astype(np.float32)代码里col_gain是列方向的乘性系数row_gain幅度只有列方向的 30%因为真实读出电路中列方向条纹更明显。pix_bias控制在 2% 以内主要模拟像元偏置不一致。noise_level0.1意味着列增益最大偏差 10%这是一个视觉上明显但还能看清细节的强度。如果噪声加得太轻网络轻轻松松就能去干净指标很好看但拿到真实图像上完全不是回事加得太重场景纹理会被条纹淹没网络学不到有用的场景结构。3.3 数据处理管线训练集/验证集划分与标签组织数据准备的最后一步是把干净图和退化图配对存好。为了避免每次训练时临时算噪声导致实验不可复现推荐的做法是在数据集准备阶段一次性生成退化图保存成input.npy干净图保持原名target.npy后续训练直接读取。一个典型的数据目录组织方式data/ ├── train/ │ ├── input/ │ │ ├── 0000.npy │ │ └── ... │ └── target/ │ ├── 0000.npy │ └── ... └── val/ ├── input/ └── target/生成数据时注意两个参数噪声强度和随机种子。噪声强度最好做一点“难度扰动”比如 50% 的样本用noise_level0.05~0.1的随机值50% 用0.1~0.15这样训练集里既有样本量充足、又覆盖不同退化程度。随机种子固定下来每次生成的退化图完全一致方便复现。如果你的训练集要生成 2000 对 256×256 的 patch大约需要 20 张 640×480 的干净底图按 128 步长切块后数量是够的。4. 从网络到权重让毕设模型在两组数据上都能收敛的落地细节4.1 网络结构落地残差块通道数与卷积核的设置有了残差块之后把网络完整搭出来结构上参考常见 CNN 去噪网络的做法输入层一个 3×3 卷积、四组残差块、输出层一个 3×3 卷积。通道数用 32 起步而不是上来就 64 或 128。红外图像是单通道256×256 的 patch 输入下32 通道的参数量在一个可控的范围一张入门级显卡也能训练。完整网络的 PyTorch 实现import torch.nn as nn class StripeRemovalNet(nn.Module): def __init__(self, in_ch1, base_ch32): super().__init__() self.head nn.Conv2d(in_ch, base_ch, kernel_size3, padding1) self.body nn.Sequential( ResBlock(base_ch, base_ch), ResBlock(base_ch, base_ch), ResBlock(base_ch, base_ch), ResBlock(base_ch, base_ch), ) self.tail nn.Conv2d(base_ch, in_ch, kernel_size3, padding1) self.relu nn.ReLU(inplaceTrue) def forward(self, x): identity x # 输入原图用于残差相加 x self.relu(self.head(x)) x self.body(x) residual self.tail(x) return identity - residual # 校正结果 原图 - 预测条纹关键设计在 forward 的返回值上。tail输出的不是校正图而是预测出的条纹分量所以最终结果用identity - residual而不是直接输出x。这样设计有几层好处网络学习目标从“生成一张完整图像”变成了“预测一张低频条纹图”后者空间结构更简单、更容易收敛同时这个写法保留了信息通路输入图像的结构信息直接通过恒等路径传到输出网络不需要从头重建场景细节。训练时先检查 forward 的输出张量形状是否和输入完全一致。一个常见的毕设翻车点是在tail卷积里漏了padding1导致输出 H、W 各少 2 个像素运行时误差到了损失函数那里才暴露来回查很久。写模型时先打印model(torch.randn(1,1,256,256)).shape确认一下再往下走。4.2 损失函数与评价指标PSNR 只是门槛条纹抑制要看粗糙度训练损失函数的选择上L1 损失和 L2 损失是最常见的两个选项。L2均方误差对大误差样本惩罚更重收敛更平稳但生成的图像会偏模糊L1平均绝对误差对离群点更鲁棒边缘保持更好。红外条纹校正这种场景建议用 L1 作为主损失配合一个可选的频域约束。只依赖 L1 有一个问题数值上 PSNR 提升明显但条纹可能还有一点点残余因为平均像素误差已经很小。工程上常用粗糙度Roughness来衡量校正后的条纹残留公式是ρ ||f * I||₁ / ||I||₁其中 f 是拉普拉斯算子或高通滤波器。ρ 越小说明图像高频成分越少——但这里要区分“高频被去掉了”和“条纹被抹平了”。如果在校正图上 ρ 显著下降而边缘细节还清晰说明去条纹有效。把粗糙度加进损失函数做联合优化是提升视觉质量的直接手段。一个简单的实现是把拉普拉斯滤波响应加到损失里import torch import torch.nn.functional as F def roughness_loss(x): laplacian_kernel torch.tensor([[0, -1, 0], [-1, 4, -1], [0, -1, 0]], dtypex.dtype, devicex.device) laplacian_kernel laplacian_kernel.view(1, 1, 3, 3) lap F.conv2d(x, laplacian_kernel, padding1) return torch.mean(torch.abs(lap)) def total_loss(pred, target): l1 F.l1_loss(pred, target) rough roughness_loss(pred) return l1 0.1 * rough粗糙度损失的权重别设太大0.05 到 0.1 之间比较合适。权重太大会导致整个输出图被过度平滑细节纹理全丢拿到答辩现场被老师一张原图对比就看出问题了。PSNR 当参考指标粗糙度当辅助指标两者一起看才算完整。4.3 训练参数批次、学习率、patch 数与收敛判断训练超参按毕设常用的配置来设定就行不需要花哨的调参策略。一个稳定能收敛的配置如下表参数推荐值说明输入尺寸256×256 或 128×128显存不够时降尺寸比降 batch 更好batch_size8 或 16256×256 单通道8 更稳妥训练更快初始学习率1e-3Adam 优化器常用配置学习率调度StepLR每 20 epoch 乘 0.5后期用小学习率做精细收敛训练轮数60 到 80 epoch数据量小100 epoch 容易过拟合优化器AdamW 或 Adam默认 beta 参数即可对应训练脚本的核心循环import torch from torch.utils.data import DataLoader, TensorDataset train_in torch.from_numpy(train_inputs).float().unsqueeze(1) train_tg torch.from_numpy(train_targets).float().unsqueeze(1) dataset TensorDataset(train_in, train_tg) loader DataLoader(dataset, batch_size8, shuffleTrue, num_workers2) model StripeRemovalNet() optimizer torch.optim.AdamW(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.5) for epoch in range(80): model.train() total 0.0 for inp, tg in loader: pred model(inp) loss total_loss(pred, tg) optimizer.zero_grad() loss.backward() optimizer.step() total loss.item() * inp.size(0) scheduler.step() print(fEpoch {epoch1}: loss{total/len(dataset):.6f})这里的DataLoader用num_workers2开两个子进程读数据。如果训练中途报 “DataLoader worker 意外退出”先检查数据文件是不是全部读取成功再考虑把num_workers改为 0 排查。还有一点经验当 patch 数只有几千时不需要每个 epoch 都去 shuffle 一次大数据集shuffleTrue保持默认即可。判断收敛的标准是验证集损失不再下降且小幅波动。如果 60 个 epoch 里验证损失先降后升就是过拟合的典型信号回到数据生成部分加大噪声强度或做数据增强而不是继续调网络宽度。5. 避坑清单训练崩掉、条纹加重、测试好看但整图发闷5.1 16bit 位深不统一预处理就引入新噪声现象从网上下载的红外数据集有的图在训练时效果很好换到自己的图像上全糊了。原因数据集中混了 8bit 和 16bit 图归一化到 [0,1] 的映射关系不一致。8bit 图是 0~255 的量化16bit 是 0~65535如果混着读、混着归一化模型看到的“灰度分布”根本不是同一个物理量纲。解决训练前统计每张图的 dtype 和灰度直方图写一个统一的前处理函数。16bit 图一律除以 655358bit 图先转成 uint16左移 8 位或统一成 float32 再按各自范围归一化。建议在源码包里加一个preprocess.py单独处理位深转换不要把这个逻辑散落在各段代码里。5.2 加了噪声反而让训练不收敛噪声参数范围必须按 DN 体现现象生成退化图时noise_level设成 0.5训练十几个 epoch 后损失纹丝不动。原因噪声加太猛退化图信噪比极低网络无法从条纹里分辨场景结构。红外图的非均匀性噪声在视觉上虽然显眼但实际幅度不会超过信号本身的 50%。让网络学一个被噪声完全淹没的图像等于让它凭猜测去恢复。解决把噪声强度控制在 0.1 到 0.2 之间。如果数据来源是实验室红外相机可以先用两点校正后的残差图统计噪声标准差按实际噪声分布去设置noise_level。合成数据的噪声分布和真实数据越接近训练出来的网络越实用。5.3 测试指标好看但整图发闷只用了 PSNR 做评判现象验证集 PSNR 从 28dB 涨到 34dB但把校正图放大看纹理细节像被磨皮了一样边缘全糊了。原因PSNR 对全局灰度误差敏感但对局部结构不敏感。L1 损失在平滑区域和边缘区域等权重模型会倾向于牺牲少数边缘像素来换取大面积的灰度准确输出图自然发“肉”。解决训练损失里把粗糙度权重调小或者换成 Charbonnier 损失、SSIM 损失这类对结构敏感的损失函数。同时评价指标加上 SSIM、边缘保持指数EPI。我自己做这一块时的血泪经验是任何只报 PSNR 不报 SSIM 的毕设都存在把图像磨平来刷分的嫌疑。5.4 显存没炸但训练中 OOMDataLoader 多进程与内存现象训练到中途报RuntimeError: CUDA out of memory但显卡显存明明只用了 50%。原因PyTorch 的num_workers会把数据从磁盘读到内存再传给 GPU如果 patch 文件是.npy每个 worker 都持有整批数据的副本内存先爆了连带影响 CUDA 缓存。解决先调num_workers0跑一个 epoch 验证数据和模型本身没问题再逐步增加 worker 数量。同时确保数据集加载函数里np.load后立即转成torch.Tensor避免在内存里堆积过多的 Python 对象。另一个做法是把数据打包成一个大的.pt文件训练时用torch.load一次性加载省掉文件 IO。5.5 条纹宽了模型就不灵数据增强里忘了随机条纹宽度现象合成条纹宽度固定为 1 像素训练出的模型在真实图上碰到 3~5 像素宽的条纹就校正不干净。原因固定条纹宽度让网络学会了“只去 1 像素宽的纵向噪声”而真实探测器的条纹宽度跟像元尺寸、坏点簇有关不是一个固定值。解决生成退化图时让条纹宽度在 1~4 像素之间随机取值。可以用scipy.ndimage.uniform_filter对列增益图做不同尺度的平滑模拟不同宽度的条纹。这个增强做一次模型在真实数据上的鲁棒性会明显提升。6. 收敛之后的事用真实条纹图做验证并让模型的“去条纹能力”可对比6.1 一个验证脚本输出校正前后粗糙度与 PSNR 对比训练完结后最怕只保存训练曲线就完事。至少写一个验证脚本加载训练好的权重跑一批真实条纹图输出量化对比结果。推荐一个最简单的评测实现import torch import numpy as np from skimage.metrics import peak_signal_noise_ratio model.eval() with torch.no_grad(): for idx, (inp, tg) in enumerate(val_loader): pred model(inp) pred_np pred.squeeze().cpu().numpy() tg_np tg.squeeze().cpu().numpy() inp_np inp.squeeze().cpu().numpy() psnr peak_signal_noise_ratio(tg_np, pred_np, data_range1.0) rough_in roughness_np(inp_np) rough_pred roughness_np(pred_np) print(fSample {idx}: PSNR{psnr:.2f}dB, Roughness {rough_in:.4f}-{rough_pred:.4f})每条样本打印一行跑完就能看出模型在哪些样本上失效。如果你有时间把每张图整理成“原图 / 带条纹图 / 校正图”三栏并排的对比图这是毕设答辩时最直观的展示材料。6.2 与经典方法对比的三个展示维度这里值得给读者一条验证校准的建议也是这个方向做得好的项目普遍会做的一件事CNN 校正方法必须和经典两点校正、时域高通滤波做一个可视化对比。对比时固定三个展示维度并排大图、某一行像素的灰度剖面曲线、局部细节放大。剖面曲线是最能说明问题的——去看校正后的剖面曲线是变平了还是只是整段平移如果只是平移说明网络做的是灰度拉伸而不是条纹抑制。另一个维度是“时序稳定性”取一段连续帧序列逐帧算粗糙度看曲线是否平稳剧烈抖动说明单帧校正不稳定。做这个对比时建议把模型在校准过的数据上和未校准的数据上各测一遍。两点校正后的残余条纹幅度小、结构简单CNN 的表现通常更好直接拿原始数据测条纹幅度大模型输出可能带微笑失真。这个结果如实写进报告里比你硬吹模型能替代硬件标定要可信得多。最后分享一个我在这个方向上的习惯每次训练都在一个完全没参与训练的“真实条纹图”集上做冒烟测试不看任何合成指标只看人眼观感。因为合成数据学到的去条纹能力在真实探测器上衰减得很快。这个习惯帮你守住了毕设的最后一道关口——评委不看你训练损失多好看而是直接看你现场跑出来的图。希望这篇笔记能帮你避开那些只有做过的人才知道的坑把这个方向做扎实。本文还有配套的精品资源点击获取