DCGAN实战:低对比度红外图像增强的原理、训练与避坑指南

发布时间:2026/9/23 10:34:28
DCGAN实战:低对比度红外图像增强的原理、训练与避坑指南 简介一套面向图像处理与深度学习开发者的低对比度红外图像增强项目源码基于深度卷积生成对抗网络实现。针对红外图像对比度低、细节模糊的成像痛点通过生成器与判别器的对抗训练自动学习图像特征并生成更清晰的增强结果可支撑夜视监控、气象观测、军事侦察等应用方向。资源包共16个文件主要包含Python代码、H5权重模型、JPEG/PNG样例图像和Markdown说明文档用于网络构建、训练推理、效果比较与使用说明整体大小约21.71MB。目前已有91人学习/下载。源码中给出了生成器与判别器结构、训练流程和超参数设置还提供数据预处理、结果对比脚本以及预训练权重可直接复现整个增强流程读者既能深入了解生成对抗网络在红外图像增强中的实践细节也能基于现有代码快速调整并用于自己的数据集。1. 低对比度红外图像增强为什么偏偏是 DCGAN低对比度红外图像增强说白了就是把红外监控里那团灰蒙蒙的画面处理成人眼看得清、后续检测算法能继续工作的画面。直接用直方图均衡或伽马校正把亮度拉开噪声和伪影也会一起被放大这是传统图像算法在处理红外图时最尴尬的地方。而 DCGAN 用生成器和判别器互相博弈的方式让生成器去学习一张好的红外增强图应该长什么样而不是仅仅学一条固定的像素映射曲线。这个方案特别适合无人机夜视、工业测温、安防监控这类场景机器视觉工程师拿到项目源码后可以很快把它迁移到自己的数据上。如果你在图像算法开发或深度学习落地这条路上想找一个既有理论深度又能实际跑通的练手方向从 DCGAN 搭一套红外增强流程比一上来就上大模型要踏实得多。2. 原理与选型DCGAN 凭什么增强红外传统方法输在哪2.1 传统图像算法的三个天花板和 DCGAN 的解题思路先说说红外图像为什么难增强。红外热像仪拍出来的 14 位原始数据动态范围动辄上万而显示端往往只有 8 位 256 个灰度级。常规做法是做一次全局映射把温度范围压缩到可见灰度区间。问题就出在这里整幅图共享一条映射曲线画面里同时存在高温目标和低温背景时目标区域的对比度会被背景带跑偏大量细节挤在几个灰度级里肉眼根本分不开。这时候最容易想到的是 CLAHE也就是限制对比度的自适应直方图均衡。它在小窗口内分别做直方图均衡对红外图确实比全局映射好但天花板也很明显。第一窗口大小是个玄学参数窗口选小了平坦区域被放大出块状噪声窗口选大了又退化回全局均衡。第二CLAHE 本质是像素级重映射它对边缘是否清晰、纹理是否自然、目标区域是否合理这类语义信息完全不敏感增强出来的图经常出现对比度上去了但目标反而更难认的怪现象。DCGAN 的思路完全不一样。它不直接操作像素映射而是训练一个生成器网络让它学会把低对比度输入转换成高对比度输出。这个学习过程不是靠某个手工设计的指标而是靠一个判别器网络不断挑剔生成结果你这张增强图和我见过的高质量目标图比纹理还差在哪、边缘还假在哪、对比度分布还不对在哪。生成器被逼着一步步修正直到判别器分不清增强图和目标图。这种对抗式的学习方法把增强从像素操作升级成了语义层面的重建。所以选择 DCGAN 做红外增强核心逻辑很直接红外图像质量退化不仅仅是对比度低还叠加了噪声、模糊、动态范围压缩等多重问题这些耦合的退化用单一映射曲线解不干净。而生成对抗网络天然擅长学习一个从低质量域到高质量域的复杂映射你给它看足够多的低对比度红外图和高对比度参考图的配对样本它能自己把这条映射关系挖出来。这条路线把这些年目标检测里常用的骨干网络思路也顺势带了进来属于深度学习算法里工程落地相对顺手的一类。2.2 DCGAN 的生成器与判别器放在红外增强里怎么改DCGAN 的原版设计是输入一个随机噪声向量通过转置卷积逐级上采样生成一张图片。但用到红外增强场景时几乎所有工程实现都会做一个关键改动把生成器的输入从随机噪声换成低对比度红外图本身。这样生成器学习的就是一个条件映射输出就是对应该输入增强后的结果。很多项目源码包里写的是 DCGAN实际上跑的是这种带条件的变体这是完全正常的做法不用觉得和论文对不上。生成器的结构一般保持 DCGAN 的约定不用全连接层用卷积和转置卷积做编解码除了第一层之外都加 BatchNorm激活函数用 LeakyReLU输出层用 Tanh。为什么要守住这几个约定因为 DCGAN 训练本来就脆原论文里这些结构细节都是玩命试出来的稳定点随便改动比如把 BatchNorm 去掉生成器在红外图上特别容易陷入局部最优输出一坨不变的灰图。判别器改动更小就是把输入通道数改成和红外图一致最后输出一个判别分数。因为红外图一般是单通道灰度图所以生成器输出通道也是 1。判别器里用带步长的卷积替代池化做下采样激活函数统一 LeakyReLU。这里有个容易忽略的点增强任务里判别器不需要输出像素级的结果它只需要告诉生成器整体像不像高质量红外图所以最后还是用一个 Sigmoid 输出单一概率值就够了。关于输入尺寸常见项目里会固定成 256×256 或 512×512。256 是个比较舒服的折中显存压力小训练速度快而且红外图本身分辨率普遍不高很多热像仪输出就是 320×240 或 640×512缩到 256 并不会损失太多有效信息。如果源图宽高比不是 1:1别直接拉伸会改变物体比例后续做检测时会出问题。我一般用中心裁剪加缩放或者干脆在数据准备阶段就统一做完预处理后面训练和推理都用同一套逻辑。2.3 为什么选 DCGAN 而不是其他的 GAN 变体这地方很多人会有疑问红外增强不是有现成的 pix2pix、CycleGAN 吗为什么不直接用我把三条路线放在一起对比过结论比较明确。方案数据要求训练成本效果特点工程落地难度DCGAN带条件输入是低对比度图输出是目标图可伪配对低单卡可跑细节重建较好适合对比度拉伸低结构简单pix2pix需要严格配对数据中配合 L1 损失轮廓保留比 DCGAN 好中要多调一个 L1 权重CycleGAN不需要配对高两对生成器判别器适合风格迁移但红外增强的几何细节容易跑偏高不稳定我自己做过一轮对比实验在只有几百对低对比度和高对比度红外图的情况下DCGAN 变体的训练稳定性明显好于 CycleGAN收敛速度也更快。原因是红外增强这件事本身的域间隙不算大输入输出在结构上是强对齐的不需要 CycleGAN 那种双向循环来做未配对约束。但如果你的数据和别人的项目差异很大比如一个是大范围测温场景一个是近距离人脸测温那重训是必然的别指望直接拿别人的权重套自己的数据就能出效果。pix2pix 和 DCGAN 的差距主要在细节保留上pix2pix 额外加了一个 L1 损失约束生成图和目标图的像素距离边缘更稳。但代价是生成器结构更复杂训练时要多调一个损失权重对新手不太友好。DCGAN 虽然细节稳定性和像素级对齐弱一点但胜在结构简单、代码量小、参数少作为第一版跑通方案非常合适。等你用 DCGAN 把整个数据管线、训练流程、评估方法都跑顺了再往 pix2pix 升级那叫优化一上来就追复杂模型那叫给自己加戏。3. 准备数据与跑通训练从原始红外图到能出效果的模型3.1 数据准备红外图像的配对原则和预处理数据是所有 GAN 训练的起点红外增强也不例外。拿到一个红外增强项目源码先别急着跑 train.py第一件事是检查数据目录里的图长什么样。常见的配对方式有两种真配对和伪配对。真配对是同一场景用不同增益或不同曝光参数各拍一张一张低对比度一张高对比度这个质量最好但采集成本高。伪配对是对高质量红外图做人工退化比如降低对比度、叠加高斯噪声、模拟雾气模糊得到一个低对比度版本再把原图当目标图。伪配对是大多数人实际采用的方式因为可行性高。做退化模拟时我一般会在代码里固化几组参数让每张图退化的程度略有差异增强训练数据的多样性。下面这段是退化模拟的核心逻辑源码包里通常也会带类似的数据增强文件读一遍知道它在干什么比自己瞎猜强得多。import cv2 import numpy as np def degrade_ir_image(img): # img: 单通道或三通道红外图, 归一化到 [0, 1] # 1. 随机压暗, 模拟低对比度 scale np.random.uniform(0.3, 0.7) img_low np.clip(img * scale, 0, 1) # 2. 叠加上高斯噪声, 模拟热成像的传感器噪声 noise np.random.normal(0, np.random.uniform(0.01, 0.03), img.shape) img_low np.clip(img_low noise, 0, 1) # 3. 轻微高斯模糊, 模拟热扩散效果 blur_ksize np.random.choice([3, 5]) img_low cv2.GaussianBlur(img_low, (blur_ksize, blur_ksize), 0) return img_low这段代码里的随机范围是我自己常用的经验值不是标准答案。scale 控制在 0.3 到 0.7 之间是因为对比度压低到 30% 以下时信息损失过大生成器学到的映射很容易走向极端输出偏黑或偏亮。噪声方差 0.01 到 0.03 对应常见红外传感器的噪声水平手工输入图像本身信噪比很低时这个范围要再调小一点。需要特别注意的是退化模拟只是没有配对数据时的补救手段它的上限取决于模拟和真实退化之间的接近程度。如果你手头有机会采集同场景真配对数据优先用真配对效果比模拟好一截。预处理这步是全流程里最容易出问题也最容易被忽略的环节。红外图像在送入网络之前必须做归一化到 [-1, 1] 区间因为生成器输出层的 Tanh 激活函数输出范围就是 [-1, 1]训练时计算损失的两张图必须在同一个数值空间里。归一化的公式很简单img (img - 0.5) / 0.5前提是原始图像是 [0, 1] 浮点数据。如果你从 8 位 PNG 读进来的 uint8 数据记得先转 float 除以 255这个顺序错了训练出来的模型输出会整体偏移增强图看起来灰蒙蒙的怎么调参数都救不回来。3.2 生成器与判别器最小可跑的 DCGAN 红外增强骨架把数据处理好了接下来是模型结构。下面这份代码是我常用的最小可跑骨架生成器采用编解码结构三下采样三上采样中间保持一层特征变换适合 256×256 输入。判别器用四个卷积层逐步下采样最后输出一个判别概率。import torch import torch.nn as nn class Generator(nn.Module): def __init__(self, in_channels1, out_channels1, base64): super().__init__() # 编码器: 逐步降采样, 提取语义特征 self.down1 self._down_block(in_channels, base, normFalse) self.down2 self._down_block(base, base * 2) self.down3 self._down_block(base * 2, base * 4) # 中间层: 不改变空间尺寸 self.mid nn.Sequential( nn.Conv2d(base * 4, base * 4, kernel_size3, padding1), nn.BatchNorm2d(base * 4), nn.ReLU(inplaceTrue), ) # 解码器: 逐级恢复空间尺寸 self.up1 self._up_block(base * 4, base * 2) self.up2 self._up_block(base * 2, base) self.up3 self._up_block(base, base // 2) self.out nn.Sequential( nn.Conv2d(base // 2, out_channels, kernel_size1), nn.Tanh() ) def _down_block(self, in_c, out_c, normTrue): layers [ nn.Conv2d(in_c, out_c, kernel_size4, stride2, padding1), nn.LeakyReLU(0.2, inplaceTrue), ] if norm: layers.insert(1, nn.BatchNorm2d(out_c)) return nn.Sequential(*layers) def _up_block(self, in_c, out_c): return nn.Sequential( nn.ConvTranspose2d(in_c, out_c, kernel_size4, stride2, padding1), nn.BatchNorm2d(out_c), nn.ReLU(inplaceTrue), ) def forward(self, x): d1 self.down1(x) d2 self.down2(d1) d3 self.down3(d2) m self.mid(d3) u1 self.up1(m) u2 self.up2(u1) u3 self.up3(u2) return self.out(u3) class Discriminator(nn.Module): def __init__(self, in_channels1, base32): super().__init__() self.model nn.Sequential( nn.Conv2d(in_channels, base, kernel_size4, stride2, padding1), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(base, base * 2, kernel_size4, stride2, padding1), nn.BatchNorm2d(base * 2), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(base * 2, base * 4, kernel_size4, stride2, padding1), nn.BatchNorm2d(base * 4), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(base * 4, 1, kernel_size4, stride1, padding0), nn.Sigmoid() ) def forward(self, x): return self.model(x)代码逻辑不复杂但有几个设计细节直接关系到训练能不能收敛。生成器的编码器第一层故意不加 BatchNorm这是 DCGAN 原论文里的做法原因是第一层直接接触输入数据加 BN 反而会引入额外的统计波动干扰原始分布的学习。解码器全部用转置卷积做 2 倍上采样配合 kernel_size4 和 stride2、padding1 这一组参数输出尺寸每一步都严格翻倍不会出现尺寸对不上的问题。判别器最后没有用全局池化直接用卷积把特征压成一个单点输出这是为了保留局部判别能力判别器能同时关注到画面不同区域的纹理质量。如果你把这段代码跑起来之后发现显存不够base 从 64 降到 32 是最直接的减半手段。但要注意base 每降一半生成器的参数量大概降为原来的四分之一表达能力跟着下降增强效果可能会变糊。优先调整输入分辨率从 256 降到 192 或者 128比砍通道数对成图质量的影响更小。3.3 训练主流程与关键参数batch、lr、epoch 怎么设模型结构搭好之后训练主流程的算法流程图其实是固定的套路前向传播、计算两组损失、反向传播、交替更新参数。DCGAN 的损失函数用的是二元交叉熵判别器同时看真实目标图和生成器输出的增强图生成器则只希望自己的输出被判别器判为真实。注意训练时生成器和判别器是两个独立的优化器不能共用一个因为两个网络要往相反方向更新共用优化器会导致参数更新互相干扰这是初学最容易翻车的地方之一。def train_one_epoch(g_net, d_net, g_opt, d_opt, loader, d_loss_fn, device): for low_img, high_img in loader: low_img, high_img low_img.to(device), high_img.to(device) batch low_img.size(0) # 先训练判别器 d_opt.zero_grad() fake_img g_net(low_img).detach() # 防止梯度传到生成器 real_pred d_net(high_img) fake_pred d_net(fake_img) real_label torch.ones(batch, 1, 1, 1, devicedevice) fake_label torch.zeros(batch, 1, 1, 1, devicedevice) d_loss d_loss_fn(real_pred, real_label) d_loss_fn(fake_pred, fake_label) d_loss.backward() d_opt.step() # 再训练生成器 g_opt.zero_grad() fake_img g_net(low_img) fake_pred d_net(fake_img) g_loss d_loss_fn(fake_pred, real_label) # 目标: 骗过判别器 g_loss.backward() g_opt.step() return d_loss.item(), g_loss.item()这段训练循环里有几个细节值得展开。判别器训练时生成器输出的增强图要调用.detach()切断梯度否则反向传播会把梯度同时传进生成器和判别器判别器的更新会被生成器的梯度干扰训练稳定性和理论设计都对不上。生成器的损失里标签用的是全 1也就是希望判别器把增强图判为真实这里没有用标签平滑主要是图省事实际效果差异不大。判别器先训练、生成器后训练这个顺序也尽量不要调整现有顺序下判别器先看到的是增强图和不带梯度的生成器输出判别器更新得更稳生成器之后再用更新过的判别器计算损失相当于每轮都在和更强的对手博弈。参数设置上我建议从这组经验值起步跑通后再按实际图像数据微调。参数建议值调整方向batch size16256×256 单卡显存不够就先降到 8再考虑降输入分辨率learning rate2e-4两个优化器都用同一个值DCGAN 原论文推荐 Adam 下这个值最稳betas(0.5, 0.999)第一项必须调成 0.5默认 0.9 会让训练震荡epoch100200看验证集增强效果不是看 loss 大小损失函数BCEWithLogitsLoss数值更稳推荐替代手动加 Sigmoid 再算 BCELosslearning rate 是 DCGAN 训练里最敏感的那个旋钮。调大一点生成器很容易在几个 epoch 之内把损失打到接近零但输出图全是重复纹理调小一点训练过程特别煎熬一百个 epoch 过去增强效果还跟原图差不多让人怀疑是不是代码写错了。betas 的两个值不要一起动先保持 (0.5, 0.999)如果训练后期出现明显的振荡可以把 0.999 调成 0.9这个改动的影响比动 learning rate 温和得多。3.4 训练完你该保存什么留生成器丢判别器训练结束后的模型保存有讲究。一个常见误区是像做分类任务一样把生成器和判别器一起存下来或者只挑 loss 最低的那个 epoch 保存。实际上部署阶段只需要生成器判别器只在训练中当陪练部署时用不上。保存生成器权重时要注意连同预处理参数一起保存包括归一化的均值和标准差、图像 resize 的目标尺寸、输入数据的通道顺序。这些参数如果散落在训练脚本里等你过两个月再回来看这个项目大概率要重新翻代码才能回忆起当时的设置。源码包里常见的做法是训练脚本每隔固定 epoch 保存一次 checkpoint比如每 20 个 epoch 存一份g_net_epoch20.pth训练结束后再挑验证集上效果最好的一份转成g_net_final.pth用于推理。我个人习惯在多张验证图上做效果对比后再选单纯看训练集上的增强效果容易过拟合。模型保存格式用 PyTorch 官方的torch.save(model.state_dict(), path)就够了没必要画蛇添足存整个模型对象state_dict 的方式跨环境兼容性更好换机器继续训或者转推理都不容易出幺蛾子。4. 覆盖 DCGAN 红外增强的 5 个典型避坑点从黑图到模式崩塌4.1 现象训练了好几代增强图还是原始输入的复制品这是最常见的假收敛状态。训练 loss 一路下降看起来一切正常但把生成器单独拿出来推理输出的增强图和输入的低对比度图几乎一模一样顶多亮度稍微变了一点。原因是判别器太弱了它没能给生成器提供有效的梯度压力。判别器可能还在训练早期就因为 batch size 太小或者数据太简单很快就输了给生成器传回去的梯度接近零生成器自然懒得学。解决办法分两步走。第一步把判别器的 base 通道数加大一倍让判别器更强第二步确认训练循环里判别器每轮训练次数不比生成器少。如果发现判别器 loss 急剧下降到接近 0而生成器 loss 迟迟不降通常就是判别器过强可以反过来降低判别器容量或者给它加 Dropout。这是一场拔河两边力气要差不多大训练才能走下去。还有个小技巧把输入的 low_img 在送入生成器之前做一次轻微的对比度拉伸让生成器至少能看到一点有效信号很多 IR 原始图黑成一团网络根本学不动。4.2 现象增强图出现棋盘格纹或者结构性伪影棋盘格纹在 DCGAN 里太经典了几乎没人能躲过。现象是增强图上出现周期性排列的方块纹理尤其在图像的平坦区域特别明显比如天空、墙面、桌面。根本原因是转置卷积叠加时产生了重叠覆盖kernel_size 和 stride 组合不当导致像素贡献不均匀。解决思路有两个方向一是把转置卷积换成上采样加普通卷积的组合比如nn.Upsample(scale_factor2, modebilinear)后面接一个普通卷积层棋盘格基本消失代价是计算量略微增加二是保持转置卷积结构但仔细检查 kernel_size、stride、padding 的组合是否满足输出尺寸恰好是输入的两倍搭配不当会加剧伪影。比较讽刺的是很多项目源码里转置卷积的参数是能跑通生成 128×128 或 256×256 图但换成 512×512 或者非正方形输入棋盘格就出现了。这是因为上采样过程中尺寸变化导致的错位被放大。我一般优先选择上采样加卷积的方案虽然代码多一点但稳定性好尤其当你要把模型迁移到不同分辨率的数据时这个改动一劳永逸地消掉了棋盘格这一类伪影。4.3 现象loss 曲线看起来正常但增强图全是灰蒙蒙或黑乎乎一片loss 数值正常下降生成图却毫无精神这种灰蒙蒙现象在红外增强里非常典型。展开生成的图像看像素分布大部分值都堆在 0 附近或者某个中间灰度附近动态范围极低。此时检查两处第一处是生成器输出层的 Tanh 是否和输入数据的归一化范围对齐如果输入归一化到了 [0, 1] 而输出用 Tanh相当于强迫生成器把 [-1, 1] 的输出硬套到 [0, 1] 的目标上它只能学出一个折中的灰调第二处是目标图在训练前的预处理环节是否有误常见错误是把 uint8 数据直接当成 [0, 1] 浮点数用了数据整体偏亮或者偏暗训练出来的模型也跟着偏移。这类问题通常不是训练本身的问题而是数据管线和模型接口之间脱节。我处理这种情况的习惯是写一段十行左右的 debug 脚本随机取一批训练样本把输入、目标、模型输出三张图的灰度直方图打印出来并排对比。直方图形状接近说明数据管线没问题问题大概率在网络结构或超参数上直方图差距明显比如目标图峰值在 200 而输出峰值在 50那基本可以断定是归一化或预处理环节有 bug。4.4 现象模型只在训练场景有效换一个夜视或逆光场景就失效这是红外增强从 demo 走向工程化时最扎心的坑。训练集里全是厂房内部的红外图测试时拿去跑室外夜视监控增强出来的画面不是过曝就是出现大量假边缘。原因是训练数据缺少场景多样性。GAN 虽然叫生成对抗网络但它本质上是个数据驱动的映射函数训练集没见过的分布它不会像人一样举一反三而是会给一个自认为合理但实际上错误的结果。面对这个问题我的做法分两层。第一层是数据侧的多样性收集数据时尽量覆盖不同环境温度、不同目标距离、不同湿度条件如果实在缺数据把已有的红外图做一次随机亮度扰动、随机水平翻转、随机轻微旋转这些数据增强操作能显著提高模型的场景泛化能力。第二层是推理侧的兜底部署时把增强结果和原图做一个加权融合比如 7 分增强加 3 分原图这样即使增强在某些区域跑偏最终画面也不至于完全失真。这个技巧常在工业项目里用因为它简单可靠不需要重新训练。4.5 现象显存不够batch 一上来就 OOM训练 GAN 比训练普通分类网络更吃显存因为生成器和判别器同时驻留在显存里而且反向传播的中间激活值占用的空间比模型参数大得多。解决路径是组合式的先把 batch size 从 16 降到 8多数情况下这一步就解决了还不行就把输入分辨率从 256 降到 128注意生成器的输出尺寸也随之改变最后一层卷积的 padding 要检查是否需要调整再不行就把生成器的 base 通道数从 64 降到 32。这套组合拳打下来显存占用一般能降到原来的四分之一左右。有一点必须提醒torch.cuda.empty_cache()不能解决 OOM它只是清空缓存池里的碎片显存实际占用没有变化。真正的显存泄漏一般发生在训练循环里不小心把fake_img的梯度图保留到了下一轮迭代比如fake_img g_net(low_img)后没有在判别器训练时.detach()导致计算图一直挂着。显存越占越多最后炸掉。遇到这种问题仔细过一遍训练循环里的每个张量确保生成器输出的梯度只在生成器更新时使用其他地方该 detach 就 detach。5. 让它更可用从 demo 到工程化的三个实操技巧5.1 推理脚本要和训练脚本使用完全一致的预处理训练时你用中心裁剪加缩放推理时哪怕差一个像素的裁剪位置生成的增强图边缘都会出现一圈亮边。更隐蔽的问题出在归一化参数上训练时数据归一化到 [-1, 1]推理时如果忘了归一化或者归一化错成 [0, 1]输出图整体偏暗或偏亮肉眼很难立刻察觉但后续接目标检测时 mAP 会莫名其妙掉一截。我习惯把预处理参数单独存一个 JSON 配置文件训练、验证、推理三段代码全部读这个文件保证三者永远一致。这个习惯在模型迭代多次之后尤其重要否则你根本说不清某个版本的模型配套的是哪一组预处理参数。5.2 用对比度指标 边缘强度代替肉眼看效果训练过程里没法每时每刻盯着生成图肉眼观察又容易疲劳很容易把看着还行误判成效果不错。我一般会写一个快速评估脚本输出两个数值像素标准差用来衡量全局对比度拉普拉斯响应的均值用来衡量边缘锐利程度。增强后的图对比度应当显著高于输入图同时边缘强度不能比输入图低否则说明生成器在抹平细节。def evaluate_enhancement(img_input, img_output): std_in img_input.std() std_out img_output.std() lap_in cv2.Laplacian(img_input, cv2.CV_64F).var() lap_out cv2.Laplacian(img_output, cv2.CV_64F).var() return { contrast_ratio: std_out / (std_in 1e-8), edge_ratio: lap_out / (lap_in 1e-8), }这个评估脚本跑一遍几十张测试图的增强效果优劣就能量化排序不用再靠感觉选模型。我跑过的红外增强模型里效果最稳定的往往不是 loss 最低的那一版而是数据预处理最规范、推理时对预处理参数最较真的那版。训练时随手写的归一化常数部署时忘记对齐出来的图立刻给你颜色看。现在我把预处理参数写进配置模型权重、配置和数据版本一起归档这个细小的习惯救了我好几次。希望帮到你。本文还有配套的精品资源点击获取