☰
IAT光照自适应Transformer:极端光照下的信号灯识别与轨道分割实战
2026/10/27 20:13:04 网站建设 项目流程

1. 项目缘起与整体设计思路拆解

信号灯颜色识别和轨道线分割这两个任务,放在常规光照条件下都不算难事,现成的检测网络和分割网络一抓一大把。但一旦把场景挪到隧道口逆光、夜间弱光、对向远光灯直射、站台强补光这些极端光照环境里,传统方案的掉点速度会让人怀疑人生。我最初接触这类需求时,用的是一个常规的语义分割网络加一个颜色分类头,白天测试集上mAP能到0.9以上,到了夜间弱光场景直接跌到0.5出头,强光过曝区域更是惨不忍睹,红灯和灭灯几乎分不开。这个项目要解决的核心问题就一个:让模型在光照剧烈变化的条件下,依然稳定地完成信号灯颜色判别和轨道线、墙壁、障碍物的分割。

标题里提到的IAT(Illumination-Adaptive-Transformer)是整条技术路线的锚点。它的核心思想并不复杂——与其让网络在暗光和强光之间"硬扛",不如显式地把光照适应这件事拆出来,用一套轻量的参数预测机制去动态调整特征表达。这和传统图像增强预处理的路子有本质区别:预处理增强是"先修图再识别",修图环节本身就会引入信息损失和伪影,而且增强参数是全局固定的,遇到画面里同时存在强光区和阴影区的情况就彻底失效。IAT走的是特征层面的自适应路线,让网络自己学会"哪里该亮一点、哪里该压一压"。

整体方案我拆成了三个模块来设计。第一个是光照自适应特征提取主干,负责在多个尺度上对输入特征做动态调制;第二个是双任务解码头,一个分支做信号灯的颜色分类与位置回归,另一个分支做轨道线、墙壁、障碍物的逐像素分割;第三个是光照一致性约束模块,在训练阶段引入光照鲁棒性约束,让同一场景在不同曝光条件下的特征表达尽量靠近。这三个模块不是简单堆叠,而是共享主干特征、各自轻量化,保证推理速度能撑住实时性要求。

为什么选Transformer而不是纯CNN?这里有个很实际的考量。CNN的局部感受野在弱光区域容易把噪声当成纹理学进去,而Transformer的自注意力机制在全局建模上更有优势,能把画面中较亮区域的信息"借"给暗区做参考。IAT的做法是在Transformer块里嵌入光照自适应调制,具体来说,它会对特征图做全局池化得到光照统计量,再通过一个小型MLP预测一组通道级的缩放和偏移参数,对特征做仿射变换。这个操作的计算量很小,但效果立竿见影——相当于给每个通道配了一个"亮度旋钮",网络自己决定拧多少。

方案选型上我还对比过几条路线。一条是直接用Retinex分解做预处理再送进分割网络,实测下来在单一光照条件下还行,但多光源混合场景下分解结果不稳定,而且分解本身耗时不小。另一条是用GAN做光照风格迁移,把暗光图迁到正常光照域再识别,问题是GAN的训练不稳定,生成图像容易引入伪影,反而干扰分割边界。IAT这条路线的优势在于端到端、无预处理、特征级自适应,训练虽然也需要多光照数据,但对数据增强的依赖没那么苛刻。最终选它,核心就是看中了自适应发生在特征层而非像素层这一点,信息损失最小。

还有一点值得说清楚:这个项目里信号灯颜色识别和轨道线分割虽然共享主干,但两个任务的损失权重和训练策略是分开调的。颜色识别对光照变化更敏感,需要更强的自适应调制;分割任务对边界精度要求高,需要保留更多高频细节。如果强行用一套超参跑两个任务,必然有一边被牺牲。我的做法是给两个解码头分别设置学习率倍率,分割头用较小的学习率保证边界稳定,分类头用稍大的学习率加快颜色判别收敛。这个细节在后面实操部分会展开讲。

2. 光照自适应机制的核心细节与实操要点

2.1 光照自适应调制到底在调什么

很多人第一次看IAT的结构图会觉得"不就是加了个注意力吗",其实关键不在注意力本身,而在调制参数的生成方式。我把它拆开讲:输入特征图先经过一个全局平均池化,得到一个通道维度的向量,这个向量可以理解为"当前画面在每个特征通道上的平均响应强度"。然后这个向量过两层全连接,第一层降维到原来的四分之一,第二层再升回原维度,中间夹一个GELU激活。输出的两组参数分别作为缩放因子和偏移量,对原特征做逐通道的仿射变换。

这里有个容易踩的坑:缩放因子的初始化。如果按常规做法初始化成接近1的值,训练初期调制几乎不起作用,网络会先学到一个"无视光照"的退化解,后期很难纠正过来。我的经验是把缩放因子初始化为1附近但带一点随机扰动,偏移量初始化为0,同时在训练前几个epoch对调制模块用稍高的学习率,让它先"动起来"。实测下来这样收敛更稳,最终指标也比默认初始化高出一两个点。

另一个细节是池化的粒度。全局平均池化会丢失空间分布信息,对于画面里同时有强光区和暗区的情况,一个全局统计量是不够的。我的改进是在全局池化之外并联一个局部池化分支,把特征图分成四块分别统计,再拼接后送进MLP。这样调制参数就带上了粗略的空间位置信息,暗区对应的特征块和亮区对应的特征块能得到不同的调制强度。代价是参数量增加了一点,但推理耗时几乎没变,因为池化和MLP的计算量相对主干可以忽略。

2.2 多尺度特征融合的取舍

主干网络输出的是多尺度特征,浅层分辨率高但语义弱,深层语义强但分辨率低。信号灯颜色识别主要靠深层语义,轨道线分割则需要浅层的高频边界信息。常规做法是FPN式的自顶向下融合,但在这个项目里我做了调整:融合之前先对每个尺度的特征单独做光照自适应调制,而不是融合后再统一调制。

理由很直接。不同尺度特征对光照的敏感程度不一样,浅层特征保留了大量像素级亮度信息,受光照影响大;深层特征经过多次下采样,亮度信息已经被稀释,更多是语义层面的响应。如果融合后再调制,等于用一套参数去适配两种截然不同的分布,效果会打折扣。分开调制再融合,每个尺度都能得到适合自己的调整强度。这个改动在消融实验里贡献了大约两个点的分割IoU提升,尤其是在强光过曝导致边界模糊的场景下改善明显。

融合方式上我用的是带权重的逐元素相加而非拼接。拼接会让通道数翻倍,后续卷积的计算量上去,而且实测拼接带来的增益很小。加权相加的权重是可学习的,初始化为等权,让网络自己决定各尺度的贡献比例。这里要注意权重要做softmax归一化,否则训练中容易出现某个尺度权重爆炸、其他尺度被压制的情况。

2.3 双任务解码头的结构设计

分割头用的是轻量的逐级上采样结构,每一级上采样后与对应尺度的主干特征做跳跃连接,连接方式是先拼接再过一个3x3卷积压缩通道。分类头则是在最深层的特征上做全局池化,接两层全连接输出颜色类别,同时用一个小的回归分支输出信号灯的边界框。两个头共享主干但参数独立,这一点前面提过,是为了让两个任务各自收敛到合适的节奏。

分类头的颜色类别我设了五类:红、黄、绿、灭灯、未知。加"未知"这一类很有必要,实际场景里信号灯可能被遮挡、可能处于闪烁的中间态、也可能是反光造成的假阳性,强行归到四类里会拉低整体准确率。未知类的样本在训练时用难例挖掘的方式补充,把那些分类置信度低但又确实不属于明确颜色的样本归进去。

分割头的类别设置上,轨道线、墙壁、障碍物是三个主类,外加背景类。这里有个实操经验:墙壁和障碍物的边界在弱光下极易混淆,因为两者在灰度上接近。我的处理是在分割头的损失函数里对墙壁和障碍物之间的边界像素加大权重,用边界加权的交叉熵配合Dice损失。边界像素的提取方式是对标注图做形态学梯度,膨胀几个像素得到边界带。这个技巧让墙壁和障碍物的混淆率降了不少。

注意:边界加权不能加得太狠,权重系数超过5之后训练会变得不稳定,边界附近的预测会过度敏感,反而产生锯齿。我实测下来权重在2到3之间比较合适。

2.4 训练数据的组织策略

光照自适应网络对数据的多样性要求比普通网络更高。我的数据组织原则是:同一场景、同一信号灯状态,至少要有三种以上不同光照条件的样本。比如同一个路口的红灯,要有白天正常光、傍晚逆光、夜间弱光三个版本。这样网络才能学到"光照变了但语义没变"的不变性。

数据增强上我没有用常规的随机亮度对比度抖动,而是用了基于物理的光照模拟增强:随机调整gamma曲线、模拟过曝的高光裁剪、模拟暗部的噪声注入。gamma调整的范围控制在0.5到2.0之间,超出这个范围生成的图像太失真,反而有害。高光裁剪是先把图像转到HSV空间,对V通道做上限截断再转回,模拟强光过曝的效果。噪声注入是在暗部区域按比例叠加高斯噪声,模拟低照度传感器的噪声特性。

这里有个反直觉的经验:不要用太强的增强。我一开始把gamma范围开到0.3到3.0,想着增强越狠鲁棒性越好,结果模型在正常光照下的指标反而掉了。原因是过强的增强让训练分布偏离了真实分布,网络把大量容量花在了处理极端失真上。后来收窄到0.5到2.0,正常光照指标回来了,极端光照指标也没怎么降。增强的目的是覆盖真实可能遇到的光照范围,不是越极端越好。

3. 完整实操流程与关键环节实现

3.1 环境搭建与依赖配置

整个项目基于PyTorch实现,我用的版本是2.0以上,CUDA按显卡型号对应配置。依赖不算多,核心是torch、torchvision、opencv-python、numpy、albumentations(做数据增强)、tensorboard(训练监控)。albumentations在这里比torchvision自带的transform好用,因为它对分割标注的同步变换支持更完善,做几何增强时图像和mask不会错位。

环境配置上有个小坑:albumentations的版本更新较快,某些版本对多通道mask的处理有变化,建议锁定一个稳定版本。我用的组合是albumentations 1.3.x配opencv-python 4.8.x,实测稳定。另外如果要用混合精度训练,注意把光照调制模块里的池化和MLP部分保持在fp32,否则统计量精度不够会导致调制参数抖动。

pip install torch torchvision --index-url 对应源 pip install opencv-python==4.8.1.78 pip install albumentations==1.3.1 pip install tensorboard

3.2 数据标注与预处理规范

标注这块要单独说,因为双任务的标注质量直接决定上限。信号灯的标注用矩形框加颜色标签,框要贴紧灯体,不要框到灯罩外的光晕。轨道线的标注用多边形勾勒,沿着线的中心走,宽度按实际线宽标注,不要为了省事画成细线。墙壁和障碍物用语义分割的mask标注,边界要精确到像素级,弱光下看不清的地方宁可标成"忽略区域"也不要瞎猜。

预处理流程我固定为:先按长边缩放到固定尺寸(我用的是1024),短边等比缩放后padding到固定值,这样不破坏长宽比。归一化用ImageNet的均值和方差,虽然输入不是自然图像,但实测这个归一化参数迁移过来效果不差,没必要单独统计。信号灯的框坐标在缩放时要同步变换,这个用albumentations的bbox_params自动处理。

标注数据里我特意保留了一部分"困难样本"不参与训练、只做验证,包括强光直射导致信号灯颜色偏移的、雨雾天气下轨道线几乎不可见的、障碍物和墙壁颜色完全一致的。这部分数据用来做最终的鲁棒性评估,避免训练集和验证集同分布导致的虚高指标。

3.3 模型搭建的关键代码结构

光照自适应模块的实现我写成独立的一个类,方便插到主干的各个阶段。核心逻辑是池化、MLP、仿射变换三步,代码不长但要注意维度的对齐。

import torch import torch.nn as nn class IlluminationAdaptiveModulation(nn.Module): def __init__(self, channels, reduction=4): super().__init__() self.global_pool = nn.AdaptiveAvgPool2d(1) self.local_pool = nn.AdaptiveAvgPool2d(2) hidden = max(channels // reduction, 8) self.mlp = nn.Sequential( nn.Linear(channels * 5, hidden), nn.GELU(), nn.Linear(hidden, channels * 2) ) # 缩放因子初始化带扰动,偏移量初始化为0 nn.init.normal_(self.mlp[-1].weight, mean=0, std=0.01) nn.init.constant_(self.mlp[-1].bias, 0) def forward(self, x): b, c, h, w = x.shape g = self.global_pool(x).view(b, c) l = self.local_pool(x).view(b, c * 4) stat = torch.cat([g, l], dim=1) params = self.mlp(stat) scale, shift = params[:, :c], params[:, c:] scale = 1.0 + scale.view(b, c, 1, 1) shift = shift.view(b, c, 1, 1) return x * scale + shift

这段代码里channels * 5是因为全局池化给c维、局部池化给4c维,拼起来是5c。局部池化用2x2的输出,相当于把特征图分成四块统计,这就是前面说的空间位置信息的来源。缩放因子用1.0 + scale的形式,保证初始状态接近恒等映射,训练从稳定点出发。

主干我用的是一个轻量Transformer的变体,patch embedding之后堆叠若干带调制模块的Transformer块。这里不展开完整主干代码,重点是把调制模块插在每个Transformer块的FFN之前,而不是之后。放在FFN之前的原因是FFN本身有非线性变换能力,先调制再变换,调制效果能被FFN进一步整合;放在之后的话调制结果直接输出到下一层,整合不充分。这个位置选择在消融里也有影响,放前面的指标略好。

3.4 损失函数设计与权重调参

总损失是三个部分的加权和:颜色分类损失、分割损失、光照一致性损失。分类损失用带标签平滑的交叉熵,平滑系数0.1,缓解过拟合。分割损失用交叉熵加Dice的组合,交叉熵管像素级分类,Dice管区域重叠,两者互补。光照一致性损失是我额外加的,思路是取同一场景不同光照的两张图,经过主干后对深层特征做L2距离约束,让它们的特征尽量接近。

权重设置上,分类损失权重1.0,分割损失权重1.0,光照一致性损失权重0.1。一致性损失权重不能高,高了会压制特征的判别性,网络为了"让不同光照特征接近"会把所有特征都拉向均值,分类和分割都受损。0.1这个值是试出来的,从0.05到0.5扫了一遍,0.1附近综合指标最好。

分割损失里边界加权那部分,实现方式是对损失图乘一个权重图,边界带权重2.5,非边界区域权重1.0。权重图在数据加载时根据mask的形态学梯度预计算好,训练时直接查用,避免每个batch重复计算。

def boundary_weighted_ce(pred, target, weight_map): ce = nn.functional.cross_entropy(pred, target, reduction='none') return (ce * weight_map).mean()

3.5 训练流程与显存优化

训练用AdamW优化器,初始学习率1e-4,权重衰减0.01。学习率调度用余弦退火配warmup,warmup跑5个epoch,让调制模块先稳定下来。batch size受显存限制,我用的是8,配梯度累积4步等效batch 32。如果显存更紧张,可以把梯度累积步数加大,但要注意BatchNorm的统计量在累积模式下会不准,所以主干里我尽量用LayerNorm而非BatchNorm,Transformer结构本身也适合LayerNorm。

混合精度训练开了amp,但前面提过调制模块的池化和MLP要保持fp32。实现方式是用torch.cuda.amp.autocast包住主干,调制模块内部用with autocast(enabled=False)单独关掉。这个细节不注意的话,训练后期会出现loss突然跳变,排查半天发现是调制参数精度不够导致的。

训练轮数我跑了120个epoch,前80个epoch正常训练,后40个epoch对困难样本做重采样,提高困难样本的出现频率。困难样本的判定标准是当前模型预测置信度低于阈值的样本,每个epoch结束后重新评估一遍。这个策略让最终模型在极端光照验证集上的指标提升了三到四个点。

提示:困难样本重采样不要一开始就用,模型还没学好的时候困难样本的定义不准,容易把简单样本误判成困难样本,导致训练分布偏移。等模型指标进入平台期再开这个策略比较稳妥。

3.6 推理部署与后处理

推理阶段我把两个任务的头合并到一个前向流程里,一次前向同时输出分类结果和分割mask。后处理上,分类结果做非极大值抑制去掉重叠框,分割mask做连通域分析去掉面积过小的碎片区域。信号灯颜色判别加了一个时序平滑:连续多帧的判别结果做投票,避免单帧误判。这个在视频流场景下很实用,单帧因为反光或遮挡误判的情况能被有效抑制。

推理速度上,1024分辨率输入在单卡上能跑到30FPS以上,满足实时要求。如果硬件更弱,可以把输入降到768,指标掉得不多但速度提升明显。分割mask的输出分辨率我保持和输入一致,不做降采样再上采样,因为轨道线的细线结构降采样后容易断。

4. 常见问题排查与避坑经验实录

4.1 训练不收敛或loss震荡

这是最常见的问题,我遇到过两次。第一次是调制模块的缩放因子初始化不当,前面提过,改成带扰动初始化后解决。第二次是学习率对调制模块来说偏大,调制参数在训练初期剧烈跳动,连带主干特征也不稳定。解决办法是给调制模块单独设一个较小的学习率倍率,我用的是0.5倍,等训练稳定后再放开。

还有一种情况是loss震荡但不发散,指标在某个区间来回跳。这通常是batch size太小导致的统计量不稳定,加大梯度累积步数或者换LayerNorm能缓解。如果用的是BatchNorm,检查一下是不是某个batch里全是暗光样本或全是强光样本,分布太偏会让BN统计量失真。

4.2 强光过曝区域分割边界丢失

强光过曝会把轨道线和墙壁的边界"烧"掉,像素值全部饱和到255,边界信息在像素层面已经不存在了。这种情况下单靠像素特征确实无能为力,我的应对是在光照调制模块里让网络学会从周边未过曝区域推断边界走向。具体做法是在训练时对过曝区域做随机遮挡增强,强迫网络利用上下文信息补全边界。这个增强配合Transformer的全局注意力效果不错,过曝区域的边界IoU从0.4出头提到了0.6以上。

另一个辅助手段是在分割后处理里加边界平滑,对分割mask做一次导向滤波,用原图的亮度梯度做引导。过曝区域亮度梯度为零,滤波会倾向于用周边区域的边界方向做平滑,视觉上边界更连贯。这个后处理不改变整体指标太多,但主观质量提升明显。

4.3 信号灯颜色在弱光下判错

弱光下红灯和灭灯的区分是老大难,红灯在极暗环境下传感器响应很弱,和灭灯的像素值差距很小。我的经验是不要只依赖颜色特征,要结合信号灯的位置和形状。红灯亮的时候灯体轮廓清晰、中心亮边缘暗,灭灯时整个灯体灰度均匀。我在分类头里额外加了一个形状特征分支,用灯体区域的灰度梯度直方图作为输入,和颜色特征拼接后一起分类。这个改动让弱光下红灯和灭灯的混淆率降了一半。

还有一个容易忽略的点是白平衡。不同摄像头的白平衡设置不一样,同一个红灯在不同设备上拍出来的色调有偏差。训练数据要覆盖多种设备来源,或者在预处理里做一次简单的白平衡校正。我用的是灰度世界假设做校正,虽然不完美但能拉齐大部分设备差异。

4.4 墙壁与障碍物混淆

前面提过边界加权能缓解,但如果混淆严重到区域级,光靠加权不够。我的补充手段是在分割头里加一个材质感知的辅助分支,用一个小卷积网络对每个像素预测一个粗糙的材质类别(光滑、粗糙、金属、植被等),这个辅助分支的损失权重设得较低,主要作用是给主分割头提供额外的特征线索。墙壁通常是光滑或粗糙的平面材质,障碍物可能是金属或植被,材质线索能帮助区分。

这个辅助分支在推理时可以保留也可以去掉,保留的话增加一点计算量但指标更稳,去掉的话速度快一点但边界附近容易出错。我最终部署时保留了,因为增加的计算量在可接受范围内。

4.5 问题速查表

问题现象可能原因排查方向解决手段
训练loss震荡不收敛调制模块学习率过大检查调制模块梯度范数调制模块学习率倍率降到0.5
强光区边界IoU低过曝导致像素信息丢失统计过曝区域占比过曝遮挡增强+导向滤波后处理
弱光红灯误判为灭灯颜色特征区分度不足看混淆矩阵加形状特征分支+白平衡校正
墙壁障碍物区域级混淆材质特征未利用看错误区域分布加材质感知辅助分支
推理速度不达标输入分辨率过高profile各模块耗时降到768分辨率或裁剪主干层数
混合精度训练后期loss跳变调制模块精度不足检查amp作用范围调制模块强制fp32

4.6 几个容易被忽略的实操细节

第一个是验证集的光照分布要单独统计。很多人验证集是随机划分的,光照分布和训练集一致,指标好看但说明不了鲁棒性。我建议按光照条件分层划分验证集,确保每种光照条件都有足够样本,这样指标才能反映真实场景表现。

第二个是模型导出时的算子兼容性。光照调制模块里的自适应池化在某些推理框架上支持不好,导出前要确认目标框架的算子支持列表。如果遇到不支持的情况,可以把自适应池化替换成固定尺寸的池化加插值,效果几乎无损。

第三个是多帧投票的窗口大小。时序平滑的窗口不是越大越好,窗口太大响应延迟高,信号灯切换时会有滞后。我实测窗口取5帧比较平衡,既能抑制单帧误判,又不会明显延迟。

第四个是标注一致性检查。双任务标注涉及框和mask两种形式,不同标注人员对边界的理解可能有偏差。训练前要做一轮交叉检查,把边界标注差异大的样本挑出来重新标,否则网络会学到矛盾的监督信号。这个工作费时但值得,我这边检查后发现大约百分之五的样本存在明显标注偏差,修正后指标有可感知的提升。

5. 光照自适应思路的延展与个人体会

这套光照自适应调制的思路其实不局限于信号灯和轨道线这个场景。我在其他几个项目里做过迁移尝试,比如夜间车牌识别、逆光人脸检测,把调制模块插到现有主干里,配合对应的数据增强,指标都有稳定提升。核心原因在于光照变化是一个跨领域的共性问题,特征级的自适应调制比像素级的预处理更通用,因为它不依赖具体的图像内容假设。

不过迁移时要注意一点:调制模块的插入位置和数量要根据任务调整。分类任务插在深层就够了,分割任务需要在浅层也插,因为浅层的高频信息对边界更重要。插太多会增加计算量,插太少覆盖不到多尺度光照变化,一般主干每个stage插一个是比较平衡的选择。

我个人在实际操作中的体会是,这类光照鲁棒性问题,数据质量的重要性不亚于模型结构。我见过不少团队把精力全花在改网络上,数据增强随便搞搞,结果指标卡在瓶颈上不去。实际上把同一场景的多光照样本配对做好,配合合理的一致性约束,往往比换一个更复杂的网络更有效。模型结构决定上限,数据决定能不能摸到上限,这个顺序不能搞反。

最后分享一个小技巧:调试光照自适应模块时,可以把调制参数可视化出来看。把缩放因子按通道画成曲线,正常情况下不同光照条件下曲线形状应该有明显差异,如果几乎重合说明调制没起作用,得回头检查初始化或者学习率设置。这个可视化手段比盯着loss曲线直观得多,能快速定位问题出在调制模块还是主干上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询