NTIRE 2026低光增强挑战:从零构建U-Net Baseline方案

发布时间:2026/8/29 3:44:28
NTIRE 2026低光增强挑战:从零构建U-Net Baseline方案 低光增强在很多人眼里是“把暗图调亮”的简单任务但真正上手做过一次就会明白它其实是图像复原里最难啃的骨头之一。暗光环境下传感器捕捉到的不只是亮度不足还包括信噪比急剧下降、色彩偏移、动态范围受限、暗部噪点被后续处理放大等一系列连锁问题。这也是为什么 NTIRE 等顶级图像复原比赛几乎每届都会保留低光增强赛道因为这个问题远没有“调亮”那么简单。NTIRE 2026 的低光增强挑战赛以“Twilight Cowboy Challenge”为代号从命名上就能看出组委会想强调的场景方向晨昏交界、自然光复杂、高动态范围的户外摄影环境。这篇文章会围绕这个比赛主题把低光增强从任务定义、数据构造、网络设计、训练策略到评估方法完整拆开讲一遍。文章的重点不是追求刷榜而是帮助读者建立一套从零开始、可复现、可迭代的 baseline 方案。如果你正准备参加 NTIRE 2026或者工作中需要处理夜拍、监控、车载视觉等低光场景这篇文章可以作为起点。1. 为什么低光增强值得单独作为一个任务低光增强和普通图像处理中的“亮度调整”有一个本质区别后者是在信息完整的前提下做像素变换而前者要在信息已经受损的情况下做信息恢复。一帧暗光图像除了亮度值偏低还伴随着量子噪声、读出噪声、热噪声等多种噪声叠加以及因动态范围不足而丢失的暗部细节。直接套用直方图均衡化或 Gamma 校正往往会让噪声同步放大图像看起来反而更脏。从信号处理的角度看低光增强试图完成的是从一个低信噪比观测中估计出高信噪比干净图像。这个过程本质上是病态逆问题因为单帧图像里真实信号与噪声在频域、空间域存在大量重叠不能简单用滤波器分离。这也是低光增强区别于超分辨率、去噪等单一任务的原因低光增强往往要同时处理亮度提升、去噪、色彩校正、细节重建多个目标而这些目标之间还有相互制约关系。具体到 NTIRE 这类比赛任务通常还会引入“参考图”的概念。参赛模型需要将弱光输入映射到一张或一组正常曝光参考图评价指标则计算模型输出与参考图之间的像素级相似度与感知相似度。这意味着模型的优化目标不仅是人眼“看着舒服”还要求能够在 PSNR峰值信噪比和 SSIM结构相似性等指标上稳定提升。许多在观感上讨巧的处理方式在指标层面可能并不占优。理解这个区别是后续所有方案设计的前提。2. NTIRE 与 Low-light Challenge任务定义与评估体系2.1 NTIRE 比赛背景NTIRENew Trends in Image Restoration and Enhancement workshop是图像复原与增强领域的重要学术竞赛和研讨会每年与 CVPR 的 workshop 同期举办。它覆盖超分辨率、去噪、去模糊、去雨、低光增强等多个赛道很多赛道既提供固定训练集和验证集也保留开放赛道供团队自由使用外部数据。NTIRE 的比赛结果往往是当年该领域技术进展的风向标许多后来被广泛引用的网络结构和训练策略最早都是在这里出现。低光增强赛道在 NTIRE 中的定位是解决“极弱光环境下的图像恢复”这一实际痛点。这类任务常见于安防监控、自动驾驶夜间感知、手机夜景摄影、医学内窥镜成像等场景。相比学术数据集比赛提供的图像对往往更接近真实拍摄环境光线分布更不均匀噪声类型也更多样不会刻意规避雨雾、路灯、车窗反光等干扰因素。2.2 Twilight Cowboy Challenge 的题解从“Twilight Cowboy”这个代号来推断“Twilight”指向晨昏时段也就是黎明和黄昏这两个自然光快速变化的窗口“Cowboy”则带有户外、开阔地形、远距离拍摄的意象。综合来看这一赛道很可能更强调自然场景、复杂光照和远距离小目标的低光增强表现。这类场景对模型有两个特殊挑战一是天空与地面亮度跨度大需要模型具备良好的动态范围处理能力二是远景细节在暗光下容易完全淹没在噪声中模型需要学会从极低信噪比区域中重建结构信息。当然在没有官方详细说明之前以上只能算合理推测。参加比赛时最稳妥的做法是先等赛道规则和数据说明落地再根据训练集的统计特征确定技术路线。但从历届 NTIRE 低光赛道经验看无论具体场景如何变化几项核心能力是通用的大范围亮度映射、稳健的暗部去噪、色彩恒常性保持、细节纹理重建。2.3 评估指标与提交机制NTIRE 类比赛通常使用 PSNR、SSIM、LPIPS 作为主要评估指标。这三个指标各有侧重指标侧重点局限PSNR像素级绝对误差对感知质量不敏感SSIM结构相似性对纹理细节区分度不够LPIPS感知特征距离依赖预训练网络的分布从历届经验看最终排名往往综合使用多个指标。对参赛者来说一个常见的误区是只盯着 PSNR 调模型结果输出图像亮度接近参考图但纹理显得“平滑过头”反而在 LPIPS 上吃亏。比较合理的做法是训练阶段在多个指标间寻找平衡或者训练多个模型分别优化像素级指标和感知指标最终做模型融合。此外比赛通常要求输出 PNG 格式的 RGB 图像且禁止在测试阶段使用外部模型对测试图做针对性微调。这类规则需要在参赛前仔细阅读不要在最后提交阶段因为格式问题被取消成绩。3. 从 Retinex 到深度学习技术路线演进3.1 传统 Retinex 思想的局限低光增强最早被广泛采用的理论框架是 Retinex 理论。Retinex 将一张图像分解为光照分量和反射分量认为反射分量代表物体固有的颜色和纹理信息光照分量决定人眼看到的明暗程度。基于这个假设传统算法通过估计光照分量再对反射分量做增强来恢复图像。这类方法实现简单、计算开销低但问题也很明显光照估计本身是病态问题在复杂场景下容易出现光晕、色彩异常和噪声放大。更本质的局限在于Retinex 理论假设反射分量是“干净”的但在真实暗光图像中反射分量已经和噪声混合在一起。单纯分离光照无法去掉噪声反而可能因为乘性增强把噪声也放大。因此传统 Retinex 方法在真实低光图片上的表现往往不如在合成数据上理想。3.2 深度学习方法的主流路线深度学习方法大体沿三个方向发展。第一种是端到端有监督学习直接用卷积网络或 Transformer 将低光图像映射到正常曝光图像。代表工作包括基于 U-Net 结构的各类网络这类方法依赖成对数据训练得当可以获得很高的 PSNR 和 SSIM。第二种是 Retinex 启发的深度分解方法网络先学习分解图像为光照图和反射图再分别处理。这类方法在可解释性上更强也容易引入先验约束但训练过程通常更复杂需要设计额外的损失函数来约束分解结果的合理性。第三种是零样本或弱监督方法不依赖完整的成对数据而是利用图像本身的统计特性来做增强例如 Zero-DCE 一类的亮度曲线估计方法。这类方法在真实数据上泛化性好但指标上限通常不如有监督方法更适合没有成对数据时的工程方案。3.3 为什么比赛首选有监督 Baseline对 NTIRE 这类比赛而言有监督方法是默认起点。原因是比赛会提供完整的成对训练数据有监督学习能够稳定地利用这个信息。选择 Baseline 网络时不需要追求结构新颖反而应该优先选择训练稳定、显存友好、已有大量开源实现的网络结构。一个常见的错误是在拿到数据的第一天就尝试复现最新的顶会论文。实际上先把一个成熟的 Baseline 跑通确认数据流和评估流程正确再用消融实验逐步替换模块效率要高得多。比赛的第一个目标永远是建立正确的评测闭环其次才是提高分数。4. 环境准备与数据构造4.1 基础环境与依赖本文示例代码基于 Python 与 PyTorch 编写。版本号请以实际环境为准但建议使用 2.x 版本的 PyTorch。安装依赖的命令大致如下python -m pip install torch torchvision pytorch-lightning1.9.5 opencv-python tqdm numpy pillow这里选择 pytorch-lightning 主要是为了简化训练循环和验证逻辑减少模板代码。如果你更习惯原生 PyTorch完全可以不用这个库核心训练逻辑差别不大。硬件方面建议至少具备一块 12GB 显存的 GPU。低光增强的图像通常不会裁剪得太小因为小图上的噪声分布和真实场景有差异推理时直接应用到全图容易出现不一致。如果显存有限可以先从 256x256 训练再在推理阶段使用分块滑动窗口。4.2 数据接口设计低光增强数据集通常提供成对的暗光图和参考图。一个健壮的数据接口至少需要做以下几件事读取图像对统一通道和尺寸。在训练阶段做随机裁剪增加样本多样性。做随机翻转控制过拟合。将像素归一化到 [0, 1] 区间。可选地做色彩抖动提升模型的色彩鲁棒性。以下是一个自定义 Dataset 的参考实现# 文件路径dataset/lowlighter_dataset.py import os import cv2 import numpy as np import torch from torch.utils.data import Dataset class LowLightDataset(Dataset): def __init__(self, root_dir, input_dirinput, target_dirtarget, patch_size256, is_trainTrue): self.root_dir root_dir self.input_dir os.path.join(root_dir, input_dir) self.target_dir os.path.join(root_dir, target_dir) self.patch_size patch_size self.is_train is_train self.input_names sorted(os.listdir(self.input_dir)) self.target_names sorted(os.listdir(self.target_dir)) def __len__(self): return len(self.input_names) def __getitem__(self, idx): in_path os.path.join(self.input_dir, self.input_names[idx]) tar_path os.path.join(self.target_dir, self.target_names[idx]) input_img cv2.imread(in_path) target_img cv2.imread(tar_path) input_img cv2.cvtColor(input_img, cv2.COLOR_BGR2RGB) target_img cv2.cvtColor(target_img, cv2.COLOR_BGR2RGB) input_img input_img.astype(np.float32) / 255.0 target_img target_img.astype(np.float32) / 255.0 if self.is_train: input_img, target_img self._random_crop(input_img, target_img) if np.random.rand() 0.5: input_img input_img[:, ::-1, :] target_img target_img[:, ::-1, :] if np.random.rand() 0.5: input_img input_img[::-1, :, :] target_img target_img[::-1, :, :] input_tensor torch.from_numpy(input_img.transpose(2, 0, 1)).float() target_tensor torch.from_numpy(target_img.transpose(2, 0, 1)).float() return input_tensor, target_tensor def _random_crop(self, input_img, target_img): h, w input_img.shape[:2] ph, pw self.patch_size, self.patch_size if h ph and w pw: top np.random.randint(0, h - ph 1) left np.random.randint(0, w - pw 1) else: top, left 0, 0 return (input_img[top:topph, left:leftpw, :], target_img[top:topph, left:leftpw, :])代码中有几个细节值得说明。随机裁剪的 patch 大小需要根据数据集分辨率调整分辨率偏小的数据集可以适当降低 patch 大小。色彩抖动没有写进这个类里是因为它未必适用于所有比赛数据。有些比赛数据的参考图本身由人工标注调色色彩抖动反而会破坏对应关系。是否使用这类强数据增强建议通过验证集指标来判断不要盲目堆叠。4.3 数据检查清单拿到数据后建议先执行下面几个步骤统计所有输入图的平均亮度分布确认是否存在极端欠曝样本。检查图片对数量是否一一对应避免文件名排序错位。随机可视化 20 组图像对确认参考图没有明显错位或合成痕迹。统计图像的宽高分布确定合适的训练分辨率。第 2 点是新手最常见的坑。很多数据集的 input 和 target 文件名并不完全一致直接用 index 对应可能在量大的时候错位。稳妥做法是根据文件名中的公共 ID 做映射而不是依赖 sorted 目录顺序。5. 可复现 Baseline一个轻量增强网络5.1 网络结构选型低光增强任务有一个特殊点输入和输出都是 RGB 图像空间分辨率完全一致本质上是像素到像素的映射。U-Net 类结构天然适合这类任务因为它通过下采样增加感受野又通过上采样恢复空间细节还通过跳跃连接保留原始边缘信息。相比近年出现的复杂注意力结构U-Net 在稳定性和显存开销上有明显优势。下面的示例使用一个简化的 U-Net 结构卷积通道数保持在适中水平便于在单卡上跑通# 文件路径models/unet_enhance.py import torch import torch.nn as nn class ConvBlock(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv1 nn.Conv2d(in_ch, out_ch, 3, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_ch) self.conv2 nn.Conv2d(out_ch, out_ch, 3, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_ch) self.relu nn.ReLU(inplaceTrue) def forward(self, x): x self.relu(self.bn1(self.conv1(x))) x self.relu(self.bn2(self.conv2(x))) return x class Encoder(nn.Module): def __init__(self, in_ch3, base_ch32): super().__init__() self.enc1 ConvBlock(in_ch, base_ch) self.enc2 ConvBlock(base_ch, base_ch * 2) self.enc3 ConvBlock(base_ch * 2, base_ch * 4) self.pool nn.MaxPool2d(2) def forward(self, x): f1 self.enc1(x) f2 self.enc2(self.pool(f1)) f3 self.enc3(self.pool(f2)) return f1, f2, f3 class Decoder(nn.Module): def __init__(self, base_ch32, out_ch3): super().__init__() self.up1 nn.ConvTranspose2d(base_ch * 4, base_ch * 2, 2, stride2) self.dec2 ConvBlock(base_ch * 4, base_ch * 2) self.up2 nn.ConvTranspose2d(base_ch * 2, base_ch, 2, stride2) self.dec1 ConvBlock(base_ch * 2, base_ch) self.final nn.Conv2d(base_ch, out_ch, 3, padding1) def forward(self, f1, f2, f3): x self.up1(f3) x torch.cat([x, f2], dim1) x self.dec2(x) x self.up2(x) x torch.cat([x, f1], dim1) x self.dec1(x) return self.final(x) class UNetEnhance(nn.Module): def __init__(self, in_ch3, base_ch32, out_ch3): super().__init__() self.encoder Encoder(in_ch, base_ch) self.decoder Decoder(base_ch, out_ch) def forward(self, x): f1, f2, f3 self.encoder(x) out self.decoder(f1, f2, f3) return out这个网络结构非常基础但它具备低光增强模型需要的最核心能力通过多层下采样扩大感受野通过跳跃连接保留细节通过卷积堆叠拟合非线性映射。在拿到数据后先用这个结构跑通全流程是完全足够的。5.2 损失函数组合低光增强常用的损失函数有 L1 Loss、L2 Loss、感知损失Perceptual Loss和 SSIM Loss。L1 Loss 比 L2 Loss 在细节保持上更好因为 L2 Loss 对大误差惩罚过大会让模型偏向输出更平滑的图像。感知损失使用预训练网络的中间特征计算距离能提升肉眼观感但也会增加训练开销。在 Baseline 阶段推荐使用 L1 Loss 可选的 SSIM Loss 组合# 文件路径losses/combined_loss.py import torch import torch.nn as nn import torch.nn.functional as F class L1SSIMLoss(nn.Module): def __init__(self, alpha0.8): super().__init__() self.alpha alpha def forward(self, pred, target): l1 F.l1_loss(pred, target) ssim_loss self._ssim_loss(pred, target) return self.alpha * l1 (1 - self.alpha) * ssim_loss def _ssim_loss(self, pred, target, window_size11, sigma1.5): channel pred.shape[1] grid torch.arange(window_size, dtypetorch.float32, devicepred.device) grid grid - window_size // 2 gauss torch.exp(-grid ** 2 / (2 * sigma ** 2)) gauss gauss / gauss.sum() kernel gauss.outer(gauss).view(1, 1, window_size, window_size) kernel kernel.repeat(channel, 1, 1, 1) pad window_size // 2 mu_pred F.conv2d(pred, kernel, paddingpad, groupschannel) mu_target F.conv2d(target, kernel, paddingpad, groupschannel) mu_pred_sq mu_pred * mu_pred mu_target_sq mu_target * mu_target mu_pred_target mu_pred * mu_target sigma_pred_sq F.conv2d(pred * pred, kernel, paddingpad, groupschannel) - mu_pred_sq sigma_target_sq F.conv2d(target * target, kernel, paddingpad, groupschannel) - mu_target_sq sigma_pred_target F.conv2d(pred * target, kernel, paddingpad, groupschannel) - mu_pred_target C1 0.01 ** 2 C2 0.03 ** 2 ssim_map ((2 * mu_pred_target C1) * (2 * sigma_pred_target C2)) / \ ((mu_pred_sq mu_target_sq C1) * (sigma_pred_sq sigma_target_sq C2)) return 1 - ssim_map.mean()这里实现的 SSIM Loss 是标准公式没有使用可学习的 sigma 和窗口尺寸在 Baseline 阶段足够。alpha 的取值可以调整建议在 0.7 到 0.9 之间。这个组合的好处是 L1 保证像素级收敛SSIM 保证局部结构一致性两者互补。5.3 训练循环下面给出一个完整的训练脚本逻辑上覆盖了模型初始化、数据加载、优化器设置、训练和验证# 文件路径train.py import os import argparse import torch import torch.optim as optim from torch.utils.data import DataLoader from torch.utils.tensorboard import SummaryWriter from dataset.lowlighter_dataset import LowLightDataset from models.unet_enhance import UNetEnhance from losses.combined_loss import L1SSIMLoss from torchvision.utils import make_grid def psnr(pred, target): mse torch.mean((pred - target) ** 2) return 10 * torch.log10(1.0 / (mse 1e-8)) def main(): parser argparse.ArgumentParser() parser.add_argument(--data_root, typestr, requiredTrue) parser.add_argument(--epochs, typeint, default100) parser.add_argument(--batch_size, typeint, default8) parser.add_argument(--lr, typefloat, default1e-4) parser.add_argument(--patch_size, typeint, default256) parser.add_argument(--checkpoint_dir, typestr, defaultcheckpoints) args parser.parse_args() os.makedirs(args.checkpoint_dir, exist_okTrue) writer SummaryWriter() train_ds LowLightDataset(args.data_root, patch_sizeargs.patch_size, is_trainTrue) val_ds LowLightDataset(args.data_root, patch_sizeargs.patch_size, is_trainFalse) train_loader DataLoader(train_ds, batch_sizeargs.batch_size, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size1, shuffleFalse) model UNetEnhance(in_ch3, base_ch32, out_ch3).cuda() criterion L1SSIMLoss(alpha0.8) optimizer optim.AdamW(model.parameters(), lrargs.lr) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxargs.epochs) best_psnr 0.0 for epoch in range(1, args.epochs 1): model.train() train_loss 0.0 for inputs, targets in train_loader: inputs inputs.cuda() targets targets.cuda() outputs model(inputs) loss criterion(outputs, targets) optimizer.zero_grad() loss.backward() optimizer.step() train_loss loss.item() * inputs.size(0) train_loss / len(train_ds) writer.add_scalar(train_loss, train_loss, epoch) if epoch % 5 0: model.eval() val_psnr 0.0 sample_inputs None sample_outputs None with torch.no_grad(): for inputs, targets in val_loader: inputs inputs.cuda() targets targets.cuda() outputs model(inputs) val_psnr psnr(outputs, targets).item() if sample_inputs is None: sample_inputs inputs sample_outputs outputs val_psnr / len(val_loader) writer.add_scalar(val_psnr, val_psnr, epoch) if val_psnr best_psnr: best_psnr val_psnr torch.save(model.state_dict(), os.path.join(args.checkpoint_dir, unet_enhance_best.pth)) grid_in make_grid(sample_inputs, normalizeTrue) grid_out make_grid(sample_outputs, normalizeTrue) writer.add_image(input, grid_in, epoch) writer.add_image(output, grid_out, epoch) print(fEpoch {epoch}: loss{train_loss:.6f}, val_psnr{val_psnr:.2f} dB) scheduler.step() writer.close() if __name__ __main__: main()在这个训练循环里验证集在每次评估时都使用原始分辨率这在显存足够时更接近真实推理表现。如果显存不足也可以在验证时裁剪中心区域。6. 训练流程与效果验证6.1 启动训练假设数据集目录结构如下data_root/ input/ 00001.png 00002.png target/ 00001.png 00002.png启动训练的命令为python train.py --data_root /path/to/data_root --epochs 100 --batch_size 8训练过程中需要重点关注训练损失是否在下降、验证 PSNR 是否持续提升。如果验证 PSNR 在某个 epoch 后开始下降而训练损失还在下降说明模型开始过拟合这时应该考虑更强的正则化或数据增强而不是继续加大训练轮数。6.2 推理与结果可视化训练结束后推理脚本可以独立运行# 文件路径inference.py import argparse import cv2 import torch import numpy as np from models.unet_enhance import UNetEnhance def main(): parser argparse.ArgumentParser() parser.add_argument(--checkpoint, typestr, requiredTrue) parser.add_argument(--input_path, typestr, requiredTrue) parser.add_argument(--output_path, typestr, requiredTrue) args parser.parse_args() model UNetEnhance(in_ch3, base_ch32, out_ch3) model.load_state_dict(torch.load(args.checkpoint, map_locationcpu)) model.eval() img cv2.imread(args.input_path) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w img_rgb.shape[:2] # 保持 16 的倍数统一缩放到推理尺寸 new_h (h // 16) * 16 new_w (w // 16) * 16 img_rgb cv2.resize(img_rgb, (new_w, new_h)) tensor torch.from_numpy(img_rgb.astype(np.float32) / 255.0).permute(2, 0, 1).unsqueeze(0) with torch.no_grad(): output model(tensor) output_np output.squeeze(0).permute(1, 2, 0).numpy() output_np np.clip(output_np, 0, 1) output_bgr cv2.cvtColor((output_np * 255).astype(np.uint8), cv2.COLOR_RGB2BGR) cv2.imwrite(args.output_path, output_bgr) if __name__ __main__: main()推理时要注意一个容易出错的地方训练时输入经过了归一化到 [0,1]推理也必须做相同处理不能在图像数据上漏掉除法操作。另一个是图像尺寸大多数卷积网络对尺寸没有严格限制但如果网络包含 4 层下采样输入尺寸需要能被 16 整除否则会在上采样拼接时出现形状不匹配。上面的代码先将尺寸调整到 16 的倍数就是为了规避这个问题。6.3 效果判断低光增强的效果不能只看一张图是否“变亮了”。需要从三个维度检查亮度是否自然是否出现过度提亮导致高光区域过曝。暗部区域的纹理是否真实恢复是否出现边缘重影或涂抹感。色彩是否保持稳是否出现整体偏绿、偏紫等颜色偏移。在比赛场景中最终评判以官方指标为准但如果你发现自己的输出在 PSNR 上很高、人眼却明显过度平滑就需要考虑在损失函数中加入感知约束。相反如果 PSNR 一直提不上去优先检查数据对齐和训练稳定性不要一开始就换大网络。7. 常见问题与排查思路低光增强训练中遇到的问题相对集中下面表格整理了最常见的情况问题现象可能原因排查方式解决方案训练损失不下降学习率过大或过小观察 loss 曲线震荡幅度调整初始学习率建议 1e-4 起步验证 PSNR 很低输入输出归一化不一致检查推理脚本是否做了 /255统一训练和推理预处理输出图像整体偏灰BatchNorm 在推理统计偏差检查 train/eval 模式切换确认 model.eval() 被调用暗部出现彩色噪点损失函数只关注 L2/L1可视化噪声分布增加感知损失或去噪约束高光区域过曝严重模型对高动态范围拟合不足查看直方图使用 gamma 校正或 HDR 损失显存不足输入尺寸过大或 batch 太大查看 GPU 显存占用降低 batch_size 或 patch_size训练结果反复波动数据顺序固定导致震荡检查 DataLoader shuffle确保 shuffleTrue第一个问题最常见也最迷惑。如果 loss 一点不下降通常不是模型问题而是数据处理问题比如输入全为 0、归一化错误、图像读取失败但没有报错。建议在训练前打印一批输入 tensor 的均值和方差确认数据流正常。第二个问题的典型场景是训练用 [0,1] 归一化推理时输入 [0,255] 的原始数据模型输出会被整体放大导致 PSNR 极低。这种错误在代码上很难一眼看出建议在推理脚本里输出输入张量的 max/min 值与训练数据进行比较。8. 工程化最佳实践8.1 从比赛到工程落地的差异比赛模型追求指标上限工程系统追求稳定性、速度、内存可控。如果你在比赛中训练了一个多层 Transformer 模型直接部署到真实业务里往往会发现推理延迟不可接受。从工程视角看建议训练一个“大模型”用于离线研究再通过蒸馏或量化压缩出一个线上版本。这比在业务初期就追求统一模型更现实。低光增强在手机端还有一个额外挑战算力受限。移动端 CPU 或 NPU 能接受的模型大小通常在几十 MB 以内U-Net 这类全卷积结构需要在通道数上做精简或者改用 MobileNet 风格的可分离卷积。8.2 数据与标注管理低光增强模型的效果高度依赖训练数据的质量。真实场景中成对数据往往难以获取。当前常用做法是使用长短曝光图像对同一场景下用短曝光拍暗图用长曝光或包围曝光拍参考图。但这种方法只适用于静态场景运动物体上会产生重影。更复杂的方案是使用多帧合成和光流对齐不过实现成本会高很多。在数据管理上建议做到三点原始数据只读、预处理结果可复现、数据版本可追踪。第一次训练时把用到的数据分布、预处理方式、随机种子都记录下来后续即使换网络结构也能准确对比效果避免因为数据处理不一致导致误判。8.3 合理使用外部数据与预训练模型参加比赛时可以合理使用外部数据吗这取决于比赛规则。有的赛道允许使用 ImageNet 预训练权重有的允许使用外部数据集有的明确禁止。在动手之前先把规则读两遍不要因为使用外部数据而被取消资格。一个相对稳妥的方式是使用 ImageNet 预训练权重做初始化因为 ImageNet 是通用视觉基础资源大部分比赛规则不会禁止。如果使用特定领域的预训练模型比如用某个夜间数据集训练过的权重作为初始化就需要确认比赛规则是否允许。8.4 模型融合与 TTA比赛排名靠前的方案几乎都会使用模型融合和测试时增强TTA。模型融合可以简单理解为集成多个训练阶段的 checkpoint或融合多个不同结构的模型。TTA 则是对输入做翻转、旋转、缩放等变换对输出做反变换后平均。这个技巧不需修改训练代码只改推理阶段通常能稳定提升 0.1 到 0.3 dB 的 PSNR。TTA 也有成本代价时间和显存开销都会成倍增加。比赛阶段可以用 TTA 提升排名工程落地时一般不启用优先保证延迟稳定。9. 总结与后续学习方向这篇文章围绕 NTIRE 2026 Low-light Enhancement 挑战赛从低光增强的任务界定出发讲清楚了为什么它不是简单的“调亮”而是涉及信噪比恢复、色彩校正、动态范围重建的复杂逆问题。在此基础上给出了一个基于 U-Net 的完整 Baseline 方案包括数据接口、网络结构、损失函数、训练循环、推理脚本和常见问题排查方法。整个流程从数据读取到最终 PNG 输出是完整的可以在拿到比赛数据后直接复用。如果你接下来准备认真打这场比赛建议按这个顺序继续深入先跑通 Baseline记录第一版 PSNR然后做一次数据可视化分析搞清楚训练集的主要难点接着逐个替换网络结构、损失函数、数据增强用控制变量法观察指标变化。不要一开始就上太复杂的结构Baseline 的意义是建立可靠的评价基准而不是追求分数上限。低光增强近年来还有一个重要趋势值得关注从 sRGB 域向 RAW 域延展。RAW 数据保留了更完整的传感器信息在极暗光场景下恢复潜力更大但对数据量和计算资源的要求也更高。如果你在 NTIRE 比赛中发现 sRGB 域的指标已经接近天花板可以考虑往 RAW 域方向找突破点。再提醒一句比赛结果不只是模型决定的。数据加载、尺寸对齐、归一化、评估脚本任何一环出错都会让分数偏差巨大。先把评价闭环做稳固再谈调模型。