图像增强赛道在近年计算机视觉挑战中热度很高,低光增强则是其中最能体现“从坏输入到好输出”的一类任务。NTIRE 2026 Low-light Enhancement 赛道以 “Twilight Cowboy Challenge” 为名,直接指向黄昏低照度、高动态范围、复杂光源混合的真实拍摄场景。对参加这类挑战的选手来说,难点不在于把图像调亮,而在于如何在提升亮度的同时抑制噪声、恢复色彩、保留局部细节,并保证结果在评价指标和人眼观感上都稳定。
这篇文章会围绕低光增强挑战的完整工程链路展开:先理解任务和评价机制,再搭好环境与基线模型,接着跑通训练和验证,然后讨论针对“Twilight Cowboy”这类暗光场景的改进方向,最后给出常见问题排查和备赛检查清单。如果你是第一次参加 NTIRE 这样的竞赛,或者正准备把低光增强算法落到自己的项目里,这条路径可以直接复用。
1. 先理解“Twilight Cowboy”到底在考验什么
1.1 低光增强要解决的不是“调亮”这一个动作
低光图像的退化是多维度的。照明不足只是最直观的表象,实际上还伴随传感器噪声、颜色偏色、对比度下降、暗部细节丢失和动态范围压缩。如果只把像素值乘一个增益,亮部很快溢出,暗部噪声被同步放大,结果会是一张更亮但更脏的图。
低光增强任务通常希望输出一张接近正常光照条件下的清晰图像,所以算法需要同时完成几个动作:
- 估计场景中的光照分布,而不是使用全局单一增益。
- 抑制亮度放大后更容易暴露的噪声。
- 修正由白平衡偏差造成的色彩偏移。
- 在暗部增强和亮部保持之间做动态范围平衡。
这也是为什么低光增强经常和去噪、去马赛克、白平衡、色调映射等任务绑在一起。单独的卷积网络可以在配对数据上学到这些行为,但需要合理的损失函数和训练策略,否则很容易在指标上好看、视觉上失败。
1.2 “Twilight Cowboy”场景的特殊性
从名称看,“Twilight Cowboy”指向的是黄昏和入夜后的户外场景。这类场景有几个很典型的视觉特征:
- 天空和光源附近仍有较高亮度,地面、阴影区域很暗,形成明显的动态范围差异。
- 整体色温偏暖,局部可能出现夕阳、路灯、车灯等强光源,导致色彩分割复杂。
- 暗部区域噪声明显,尤其是在手机或普通消费级相机拍摄时。
- 人物或物体轮廓经常处于逆光位置,边缘细节容易淹没在暗部中。
这些特征决定了算法不能只在一个固定亮度范围内做变换。对“Twilight”一类图像,模型需要具备根据局部内容自适应调节的能力。例如对天空只做轻微增强,对暗部阴影做更强的提升,同时始终限制噪声放大。这也是为什么许多参赛方案会引入注意力机制、光照估计分支或者递归调制结构。
1.3 NTIRE挑战的一般评价机制
NTIRE 全称是 New Trends in Image Restoration and Enhancement,是计算机视觉领域经常在 CVPR 会议期间组织的图像复原与增强系列挑战活动。低光增强通常作为其中一个独立赛道出现。具体到 2026 年的赛事规则,需要在官网确认,但历届挑战的一些通用做法可以提前参考:
- 官方会提供成对的训练数据,低光图像作为输入,对应的高质量正常光图像作为参考。
- 验证集和测试集不公开标签,参赛者跑推理后上传结果,由主办方统一评估。
- 常用评价指标包括 PSNR、SSIM、LPIPS,部分赛道会加入面向感知质量的 NIQE、PI 或用户研究。
- 赛道可能分成 sRGB 输入和 RAW 输入,也可以分成“有参考”和“无参考”两个子任务。
不管规则怎么变,参赛者最好准备两套评估管线:一套计算 PSNR/SSIM/LPIPS,便于自己验证时排序;另一套严格按照官方要求生成提交文件。两套管线的数据范围、图像边界、命名方式必须完全一致,否则容易出现本地指标不错、提交后分数对不上的问题。
2. 环境准备与基线搭建,先从能跑通开始
2.1 硬件和软件环境
低光增强模型不需要像大语言模型那样消耗巨额显存,但为了快速迭代,一张支持 CUDA 的显卡仍然是必要条件。最小可行配置可以按这个标准准备:
| 项目 | 建议配置 |
|---|---|
| GPU | NVIDIA RTX 3060 或更高,显存 8GB 以上 |
| 内存 | 16GB 以上 |
| 操作系统 | Linux 优先,Windows 也可运行 |
| Python | 3.8 到 3.11 均可 |
| 深度学习框架 | PyTorch 2.x |
| 图像库 | OpenCV、scikit-image、Pillow |
| 辅助库 | lpips、tqdm、tensorboard 或 wandb |
安装环境的命令通常如下:
conda create -n llie python=3.9 conda activate llie pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python scikit-image lpips tqdm tensorboard这里要注意,PyTorch 的安装地址需要和本机 CUDA 驱动版本匹配。如果驱动版本较新,也可以使用默认官方源安装。训练前先用一个小脚本确认 GPU 可用:
import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU only")2.2 低光数据集的目录设计与读取方式
挑战赛提供的完整数据集通常以图像对的形式出现。即使官方数据还没发布,也可以先用公开低光数据集验证流程,或者自己构造小型训练集。推荐的数据目录结构如下:
data/ ├── train/ │ ├── low/ │ │ ├── 0001.png │ │ ├── 0002.png │ │ └── ... │ └── high/ │ ├── 0001.png │ ├── 0002.png │ └── ... ├── val/ │ ├── low/ │ └── high/ └── test/ └── low/读取时建议先写一个 Dataset 类,统一处理图像路径、归一化范围和数据增强。一个最小实现如下:
import os import cv2 import numpy as np from torch.utils.data import Dataset class LowLightDataset(Dataset): def __init__(self, root, split="train", crop_size=256, train_mode=True): self.low_dir = os.path.join(root, split, "low") self.high_dir = os.path.join(root, split, "high") self.names = sorted(os.listdir(self.low_dir)) self.crop_size = crop_size self.train_mode = train_mode def __len__(self): return len(self.names) def __getitem__(self, idx): name = self.names[idx] low = cv2.imread(os.path.join(self.low_dir, name)).astype(np.float32) / 255.0 high = cv2.imread(os.path.join(self.high_dir, name)).astype(np.float32) / 255.0 # OpenCV 默认 BGR,这里保持一致;若用 PIL 则注意通道顺序 if self.train_mode: low, high = self._crop(low, high, self.crop_size) if np.random.rand() > 0.5: low = low[:, ::-1, :] high = high[:, ::-1, :] low = np.ascontiguousarray(low.transpose(2, 0, 1)) high = np.ascontiguousarray(high.transpose(2, 0, 1)) return low, high def _crop(self, low, high, size): h, w, _ = low.shape top = np.random.randint(0, h - size + 1) if h > size else 0 left = np.random.randint(0, w - size + 1) if w > size else 0 low = low[top:top + size, left:left + size, :] high = high[top:top + size, left:left + size, :] return low, high这一段代码里有几个容易出错的地方。第一个是图像缩放范围,必须保证输入和标签都在 0 到 1 之间。第二个是通道顺序,OpenCV 读取出来是 BGR,如果后续训练用的预训练模型是按 RGB 初始化,需要在进入网络前转换,或者在读取时直接使用cv2.cvtColor。第三个是随机裁剪,挑战赛图像尺寸可能有大有小,不能假设所有图像都超过裁剪尺寸。
2.3 评估指标的实现与口径
PSNR 和 SSIM 是低光增强最常用的两个指标。PSNR 衡量重建像素误差,SSIM 衡量结构相似性。LPIPS 则更接近人的感知,数值越低表示感知差异越小。
PSNR 的计算用 scikit-image 很方便:
from skimage.metrics import peak_signal_noise_ratio, structural_similarity import lpips def compute_psnr_ssim(pred, target): pred = pred.clip(0, 1) target = target.clip(0, 1) psnr = peak_signal_noise_ratio(target, pred, data_range=1.0) ssim = structural_similarity( target, pred, data_range=1.0, channel_axis=2, gaussian_weights=True, sigma=1.5, use_sample_covariance=False, ) return psnr, ssim使用 LPIPS 时需要初始化一个预训练网络:
lpips_fn = lpips.LPIPS(net="vgg") def compute_lpips(pred, target): pred_t = torch.from_numpy(pred).permute(2, 0, 1).unsqueeze(0).float() target_t = torch.from_numpy(target).permute(2, 0, 1).unsqueeze(0).float() return lpips_fn(pred_t * 2 - 1, target_t * 2 - 1).item()计算指标时有一个常见口径问题:是否去掉边缘像素。许多图像复原任务会在评估前裁剪掉 4 到 8 像素,因为卷积网络边缘存在 padding 伪影。是否做这种处理,要以官方评估代码为准。如果自己建立验证流程,最好先固定一种口径,并和官方样例结果比对。
3. 从零实现一个最小可运行的低光增强模型
3.1 模型选择:从轻量全卷积网络开始
低光增强并没有必须使用的网络结构。刚起步时,不建议直接搭建复杂 Transformer,而应该先用一个简单、稳定、容易调试的卷积网络跑通流程。这里给出一个带残差连接的小网络,输入 3 通道图像,输出 3 通道增强结果,结构类似浅层 U-Net 的简化版。
import torch import torch.nn as nn import torch.nn.functional as F class SimpleEnhanceNet(nn.Module): def __init__(self, in_channels=3, base=32): super().__init__() self.encoder = nn.Sequential( nn.Conv2d(in_channels, base, 3, padding=1), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(base, base * 2, 3, stride=2, padding=1), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(base * 2, base * 2, 3, padding=1), nn.LeakyReLU(0.2, inplace=True), ) self.decoder = nn.Sequential( nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False), nn.Conv2d(base * 2, base, 3, padding=1), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(base, in_channels, 3, padding=1), ) def forward(self, x): identity = x x = self.encoder(x) x = self.decoder(x) x = F.interpolate(x, size=identity.shape[2:], mode="bilinear", align_corners=False) return identity + x这个网络的关键在于最后的残差连接。模型学习的是低光图像到正常图像的增量,而不是直接生成完整图像。实验表明,残差结构比直接输出更稳定,尤其是暗部区域,模型只需要学习修正量,优化难度更低。
3.2 损失函数组合:L1 是稳定下限,SSIM 和感知损失帮助视觉质量
基线阶段可以只使用 L1 损失。L1 对每个像素的误差惩罚是线性的,不会像 L2 那样被少数过亮或过暗像素主导,在图像复原任务中往往收敛更平稳。
但要提升视觉质量,可以加入 SSIM 损失,公式如下:
class SSIMLoss(nn.Module): def __init__(self, window_size=11): super().__init__() self.window_size = window_size def forward(self, pred, target): pred = pred.clamp(0, 1) target = target.clamp(0, 1) # 简化实现,实际可复用 pytorch_msssim from pytorch_msssim import ssim return 1 - ssim(pred, target, data_range=1.0, size_average=True)如果希望进一步贴近人眼感知,可以加入 LPIPS 损失。训练时损失可以写成:
loss = l1_loss(pred, target) + 0.1 * ssim_loss(pred, target) + 0.01 * lpips_loss(pred, target)不同损失权重的选择取决于数据集。如果只追求 PSNR,L1 和 MSE 权重可以更大;如果追求观感,SSIM 和 LPIPS 权重应该提高。这里给出的 1、0.1、0.01 只是常见起点,需要小规模实验验证。
3.3 训练脚本主体:把数据、模型、损失串起来
一个完整训练脚本至少包含数据加载、模型初始化、优化器、训练循环、验证循环和 checkpoint 保存。下面是一个精简但可运行的骨架:
import torch from torch.utils.data import DataLoader from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR def train_one_epoch(model, loader, opt, criterion, device): model.train() total_loss = 0 for low, high in loader: low = low.to(device) high = high.to(device) pred = model(low) loss = criterion(pred, high) opt.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) opt.step() total_loss += loss.item() * low.size(0) return total_loss / len(loader.dataset) model = SimpleEnhanceNet().cuda() dataset = LowLightDataset("data", split="train", crop_size=256) loader = DataLoader(dataset, batch_size=8, shuffle=True, num_workers=4) optimizer = AdamW(model.parameters(), lr=1e-4, weight_decay=1e-5) scheduler = CosineAnnealingLR(optimizer, T_max=50) criterion = torch.nn.L1Loss() for epoch in range(50): loss = train_one_epoch(model, loader, optimizer, criterion, torch.device("cuda")) scheduler.step() print(f"epoch {epoch+1}, loss {loss:.4f}")这段代码里最有必要强调的是梯度裁剪。低光增强任务的输入像素差异大,偶尔会出现损失突增,梯度裁剪能避免模型参数被异常样本带偏。clip_grad_norm_中的max_norm一般设置在 0.5 到 5.0 之间,这里取 1.0。
4. 对标挑战主题,改进算法的四个方向
4.1 从全局映射到曲线估计:Zero-DCE 类方法
简单卷积网络虽然能跑通,但在“Twilight Cowboy”这类光照分布复杂的图像上,很容易出现局部过曝或增强不足。一个被广泛验证的思路是估计逐像素光照曲线,而不是直接输出最终图像。Zero-DCE 的思路很有参考价值:网络学习一组曲线参数,对输入图像做多次迭代映射,从而实现 self-calibrated 的亮度调整。
曲线映射的最小表达如下:
def apply_curve(image, alpha, beta, gamma): # image: [B, 3, H, W] x = image for _ in range(8): x = x + alpha * (x - x * x) return x这种方法的好处是输出范围有界,不会超过 0 到 1,天然稳定。缺点是曲线表达能力有限,对噪声和颜色偏差的处理需要其他模块配合。
4.2 联合去噪:暗部放大后的噪声是最大敌人
低光图像的噪声在亮度提升后会非常明显。提高亮度相当于放大信号和噪声,如果网络没有去噪能力,最终输出会出现大量彩色噪点,即使 PSNR 还行,视觉上仍然不可接受。
处理思路有两类。第一类是显式加一个去噪分支,先对低光图去噪,再做亮度增强。第二类是隐式通过损失函数和网络容量学习去噪。参加挑战时,可以在网络中间层加入一个去噪监督损失,或者在训练时对标签添加噪声扰动,提升鲁棒性。
对 RAW 输入数据,还可以利用 Bayer 域的噪声模型进行仿真。如果官方赛道提供 RAW 数据,优先在 RAW 域训练,因为原始数据的线性关系更容易建模噪声,sRGB 域经过非线性变换后噪声更难分离。
4.3 动态范围与曝光区域自适应
Twilight 场景经常同时存在高光天空和极暗地面,单一曲线很难兼顾。一个实用做法是引入亮度注意力图:
brightness = low.mean(dim=1, keepdim=True) attention = 1 - brightness # 暗部权重高用这张注意力图作为辅助信息,让网络知道哪些区域需要强增强,哪些区域应该保持。可以在输入中拼接这个灰度图,也可以在损失函数中对暗部像素分配更高权重:
loss = ((pred - high).abs() * (1 + attention)).mean()这么做的好处是让模型更关注暗部细节恢复,而不是把所有像素一视同仁。缺点是需要调权重,暗部权重过高容易导致噪声敏化,需要配合正则化使用。
4.4 数据增强与配对数据生成
竞赛数据集可能较小,而低光增强模型对数据分布的依赖很强。一种有效策略是从正常光图像合成低光图像,从而获得大量配对样本。常见合成流程:
- 随机 gamma 暗化。
- 加入高斯噪声和泊松噪声。
- 降低饱和度或改变色温。
- 用 JPEG 压缩引入伪影。
- 随机调整亮度和对比度。
合成样本的公开代码如下:
import numpy as np import cv2 def synthesize_low_light(high, gamma_range=(1.5, 3.5), noise_std=0.02): high = high.astype(np.float32) / 255.0 gamma = np.random.uniform(*gamma_range) low = np.power(high, gamma) noise = np.random.normal(0, noise_std, low.shape).astype(np.float32) low = low + noise low = np.clip(low, 0, 1) return low合成低光图像不能完全替代真实数据,因为真实传感器的噪声和颜色退化更复杂。但它可以作为预训练阶段的数据来源,让模型先学会基本增强能力,再用官方数据微调,这样通常比直接从零训练更稳定。
5. 训练、验证与推理的完整流程
5.1 训练配置建议
训练配置不应该一开始就用大 batch 和大学习率,而应该先做小规模实验。建议从以下配置起步:
| 参数 | 建议值 | 说明 |
|---|---|---|
| 输入尺寸 | 256x256 | 太大显存容易不足,太小丢失细节 |
| Batch size | 8 或 16 | 按显存调整 |
| 优化器 | AdamW | 比 SGD 收敛更稳定 |
| 学习率 | 1e-4 | 作为初始值,观察 Loss 曲线调整 |
| 学习率调度 | CosineAnnealing | 后期衰减更平稳 |
| Epoch | 30 到 60 | 视数据量而定 |
| 混合精度 | torch.cuda.amp | 可减少显存并加速 |
启用混合精度训练时,代码要包在torch.cuda.amp.autocast()中,并对 loss 调用scaler.scale:
scaler = torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): pred = model(low) loss = criterion(pred, high) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()使用混合精度时注意,有些损失函数可能在 fp16 下不稳定,尤其是涉及动态范围较大的操作。如果发现 Loss 出现 NaN,可以关闭混合精度,或者把部分计算转为 fp32 后再进行。
5.2 验证时应该怎么比较模型
训练过程中要定期在验证集上计算 PSNR、SSIM 和 LPIPS。不要只保存最后一个 epoch,而应该保存验证指标最优的 checkpoint。建议按下面的逻辑记录:
- 每一个 epoch 结束时,在固定验证集上跑一次推理。
- 计算 PSNR、SSIM、LPIPS。
- 将结果写入 CSV 文件,方便后续排序。
- 同时保存一张可视化的对比图,包括低光图、预测图、标签图。
可视化比指标更能暴露问题。模型可能 PSNR 很高,但颜色发灰;也可能 SSIM 不错,但暗部一团黑。只有看图像才能判断是否存在偏色、过曝、噪点残留等问题。
5.3 推理和提交文件生成
推理阶段要和训练阶段保持完全一致的数据处理流程。一个常见错误是训练时使用 BGR 读图,推理时换成了 PIL 的 RGB,导致结果偏色。推理脚本建议固定使用相同库和相同路径。
def inference(model, low_path, out_path, device): low = cv2.imread(low_path).astype(np.float32) / 255.0 low_t = torch.from_numpy(low.transpose(2, 0, 1)).unsqueeze(0).float().to(device) with torch.no_grad(): pred = model(low_t) pred = pred.squeeze(0).permute(1, 2, 0).clamp(0, 1).cpu().numpy() pred = np.uint8(pred * 255.0) cv2.imwrite(out_path, pred)生成提交文件时要检查:
- 输出图像尺寸是否和输入保持一致。
- 输出是否被正确裁剪到 0 到 255 的整数范围。
- 文件名和目录结构是否符合官方要求。
- 是否包含 alpha 通道或额外信息。
- 是否使用无损 PNG,而不是质量压缩过的 JPG。
6. 常见问题排查:从现象定位到修复
6.1 Loss 下降但 PSNR 不涨
表现:训练 loss 持续降低,但验证集 PSNR 在某个数值附近不动,甚至下降。
可能原因:
- 模型容量不够,无法表达更复杂的增强映射。
- 损失函数与评价指标不一致,L1 优化方向与 PSNR 并不完全相同。
- 验证集和训练集分布差异大。
- 数据增强太强,模型在验证集上泛化不足。
排查方式:
- 先去掉所有数据增强复跑,观察 PSNR 是否恢复。
- 在训练集上计算 PSNR,如果训练集指标也很低,说明模型容量或训练策略有问题。
- 如果训练集指标高、验证集指标低,优先考虑过拟合和数据分布问题。
6.2 输出发灰、偏色或过曝
表现:预测图像整体发灰,对比度不足;或者暗部变亮但颜色发青、发黄;亮部严重过曝。
可能原因:
- 模型输出被直接加在输入上,但残差学习将大量像素推向中间灰。
- 训练数据中正常光图像白平衡不统一。
- 亮部区域在损失函数中被一视同仁,导致过曝。
- 通道顺序不一致,造成颜色通道错乱。
排查方式:
- 先检查输入和标签的通道顺序是否一致。
- 对输出做直方图统计,看是不是集中在 0.4 到 0.7。
- 在少量样本上观察预测和标签的差值图,判断是否出现系统性偏移。
解决方向:
- 加入对比度损失或 SSIM 损失。
- 对图像高光区域做权重下降。
- 使用全局特征调制或颜色校正模块。
6.3 训练到一半显存不足
表现:前几个 epoch 正常,随着 training 进行,显存占用持续增长,最后抛出CUDA out of memory。
可能原因:
- 验证阶段累积了计算图。
- DataLoader 的 num_workers 设置不当。
- 没有定时清理验证用的中间变量。
- 图像尺寸变大或 batch size 设置过大。
排查方式:
- 在验证代码里使用
with torch.no_grad()。 - 在循环里使用
torch.cuda.empty_cache(),但这只是缓解手段。 - 使用
nvidia-smi观察显存占用曲线。
推荐做法:
- 训练和验证共用一个 GPU 时,先在验证阶段释放显存。
- 如果使用
torch.cuda.amp,确认参数更新逻辑正确。 - 不要在每个 iteration 里创建新的 loss 函数实例或保存过多历史 tensor。
6.4 本地指标和官方提交结果不一致
表现:本地验证 PSNR 很高,但官方测试集分数明显偏低,或者相反。
可能原因:
- 本地验证集和官方测试集来源不同,分布不一致。
- 图像预处理不一致,例如缩放方式、边界填充、通道顺序。
- 官方评估时对图像范围有特殊处理,比如要求输出 uint8 而本地直接比较浮点图像。
- 提交格式错误导致官方读图失败。
排查方式:
- 对照官方示例代码,重建评估流程。
- 用官方提供的 sample submission 跑一遍,确认输出完全符合要求。
- 将本地保存的图像下载后重新读回,再计算指标,确认没有写入伪影。
解决方向:
- 统一使用无损 PNG 保存。
- 在提交前写一个自检脚本,校验文件名、数量和图像尺寸。
7. 备赛最佳实践与最终检查清单
7.1 先跑通固定随机种子的小规模实验
竞赛中最大的时间浪费是流程不稳定。建议在正式训练前,固定所有随机种子,用 10 张训练图、1 张验证图做 2 个 epoch 的小实验。这个阶段不看效果,只看是否能正常前向、反向、保存 checkpoint、加载推理。小实验确认没问题后,再扩展到完整数据和更大模型。
固定随机种子示例:
import random import numpy as np import torch def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)固定随机种子不能保证跨设备完全一致,但至少可以排除大部分数据加载和参数初始化问题。
7.2 建立实验记录表
参加 NTIRE 这类挑战,会做大量实验。建议用表格记录每一组实验的可复现信息:
| 实验编号 | 模型结构 | 损失组合 | 输入尺寸 | Batch | 学习率 | 数据增强 | PSNR | SSIM | LPIPS | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|
| 001 | SimpleEnhanceNet | L1 | 256 | 8 | 1e-4 | 无 | 18.2 | 0.71 | 0.23 | 基线 |
| 002 | SimpleEnhanceNet | L1+SSIM | 256 | 8 | 1e-4 | 翻转裁剪 | 19.1 | 0.78 | 0.19 | 加入 SSIM 后更好 |
| 003 | CurveNet | L1+LPIPS | 320 | 4 | 5e-5 | 合成低光 | 19.8 | 0.81 | 0.16 | 候选方案 |
记录表不只是给自己看,也是为了在调试时快速定位哪一次改动影响了指标。没有记录表的竞赛实验,很容易陷入“改了很多但不知道为什么变好”的状态。
7.3 最终提交前的检查清单
提交前至少花一个小时做完整检查,而不是直接上传。下面这张清单可以复用:
| 检查项 | 操作 | 是否通过 |
|---|---|---|
| 推理脚本路径 | 确认使用最终 checkpoint,不是中间临时版本 | 待确认 |
| 预处理一致性 | 确认训练和推理使用相同读图库、通道顺序、范围 | 待确认 |
| 输出格式 | 确认输出尺寸、位深、通道数、文件名符合要求 | 待确认 |
| 图像范围 | 确认输出在 0 到 255 内,类型为 uint8 | 待确认 |
| 测试集覆盖 | 确认所有测试图都成功推理,没有遗漏 | 待确认 |
| 本地评估复跑 | 用官方样例提交跑通本地评估,确认分数稳定 | 待确认 |
| 异常样本检查 | 随机抽查 10 张结果,观察是否出现过曝或偏色 | 待确认 |
| 硬件稳定性 | 推理全程记录显存和日志,确认没有中途崩溃 | 待确认 |
| 结果备份 | 将最终结果压缩备份,记录 SHA256 | 待确认 |
7.4 扩展方向与实际项目落地建议
如果已经跑通基线并提升了指标,可以再考虑三个扩展方向。
第一,使用先进的恢复骨干网络。比如结合 Swin Transformer 局部窗口注意力的结构,或使用 Restormer 中基于通道注意力的设计,能够更好地建模全局光照关系。但这需要足够数据和显存,建议在基线上验证收益后再集成。
第二,引入 RAW 域信息。RAW 图保留了更多暗部信息,低光增强在 RAW 域处理通常比 sRGB 域更容易获得真实细节。如果挑战提供 RAW 数据,值得投入人力做 RAW 到 RGB 的完整 pipeline。
第三,做模型集成和测试时增强。多个模型预测取平均,或在推理时对输入做翻转和颜色抖动,再做反变换,通常能稳定提升 PSNR 和 SSIM。代价是推理时间增加,如果挑战对速度没有硬性要求,这是很直接的提分手段。
落到实际项目里,最需要注意的是模型不能只在竞赛指标上表现好,还要考虑落地时的运行环境。轻量网络、半精度推理、ONNX 导出、批处理接口,都是低光增强产品化的常见要求。竞赛结束后,把验证集、训练配置、最佳模型和推理脚本一起整理成文档,方便后续复现和迁移到真实场景。
低光增强是典型的“输入质量差,算法上限依赖数据和处理细节”的任务。把数据读取、指标计算、损失函数、训练验证流程全部跑通,再针对 Twilight 场景的暗部细节、噪声和动态范围做定向优化,才能在 NTIRE 2026 这样竞争激烈的挑战中拿到稳定的成绩。如果你现在还在准备阶段,先从 2.2 节的数据读取开始,一步一步把基线搭起来,后面所有实验都会顺利得多。