IAT光照自适应Transformer:极端光照下的信号灯识别与轨道分割实战 1. 项目缘起与整体设计思路拆解信号灯颜色识别和轨道线分割这两个任务放在常规光照条件下都不算难事现成的检测网络和分割网络一抓一大把。但一旦把场景挪到隧道口逆光、夜间弱光、对向远光灯直射、站台强补光这些极端光照环境里传统方案的掉点速度会让人怀疑人生。我最初接触这类需求时用的是一个常规的语义分割网络加一个颜色分类头白天测试集上mAP能到0.9以上到了夜间弱光场景直接跌到0.5出头强光过曝区域更是惨不忍睹红灯和灭灯几乎分不开。这个项目要解决的核心问题就一个让模型在光照剧烈变化的条件下依然稳定地完成信号灯颜色判别和轨道线、墙壁、障碍物的分割。标题里提到的IATIllumination-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-python4.8.1.78 pip install albumentations1.3.1 pip install tensorboard3.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, reduction4): 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, mean0, std0.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], dim1) 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, reductionnone) return (ce * weight_map).mean()3.5 训练流程与显存优化训练用AdamW优化器初始学习率1e-4权重衰减0.01。学习率调度用余弦退火配warmupwarmup跑5个epoch让调制模块先稳定下来。batch size受显存限制我用的是8配梯度累积4步等效batch 32。如果显存更紧张可以把梯度累积步数加大但要注意BatchNorm的统计量在累积模式下会不准所以主干里我尽量用LayerNorm而非BatchNormTransformer结构本身也适合LayerNorm。混合精度训练开了amp但前面提过调制模块的池化和MLP要保持fp32。实现方式是用torch.cuda.amp.autocast包住主干调制模块内部用with autocast(enabledFalse)单独关掉。这个细节不注意的话训练后期会出现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作用范围调制模块强制fp324.6 几个容易被忽略的实操细节第一个是验证集的光照分布要单独统计。很多人验证集是随机划分的光照分布和训练集一致指标好看但说明不了鲁棒性。我建议按光照条件分层划分验证集确保每种光照条件都有足够样本这样指标才能反映真实场景表现。第二个是模型导出时的算子兼容性。光照调制模块里的自适应池化在某些推理框架上支持不好导出前要确认目标框架的算子支持列表。如果遇到不支持的情况可以把自适应池化替换成固定尺寸的池化加插值效果几乎无损。第三个是多帧投票的窗口大小。时序平滑的窗口不是越大越好窗口太大响应延迟高信号灯切换时会有滞后。我实测窗口取5帧比较平衡既能抑制单帧误判又不会明显延迟。第四个是标注一致性检查。双任务标注涉及框和mask两种形式不同标注人员对边界的理解可能有偏差。训练前要做一轮交叉检查把边界标注差异大的样本挑出来重新标否则网络会学到矛盾的监督信号。这个工作费时但值得我这边检查后发现大约百分之五的样本存在明显标注偏差修正后指标有可感知的提升。5. 光照自适应思路的延展与个人体会这套光照自适应调制的思路其实不局限于信号灯和轨道线这个场景。我在其他几个项目里做过迁移尝试比如夜间车牌识别、逆光人脸检测把调制模块插到现有主干里配合对应的数据增强指标都有稳定提升。核心原因在于光照变化是一个跨领域的共性问题特征级的自适应调制比像素级的预处理更通用因为它不依赖具体的图像内容假设。不过迁移时要注意一点调制模块的插入位置和数量要根据任务调整。分类任务插在深层就够了分割任务需要在浅层也插因为浅层的高频信息对边界更重要。插太多会增加计算量插太少覆盖不到多尺度光照变化一般主干每个stage插一个是比较平衡的选择。我个人在实际操作中的体会是这类光照鲁棒性问题数据质量的重要性不亚于模型结构。我见过不少团队把精力全花在改网络上数据增强随便搞搞结果指标卡在瓶颈上不去。实际上把同一场景的多光照样本配对做好配合合理的一致性约束往往比换一个更复杂的网络更有效。模型结构决定上限数据决定能不能摸到上限这个顺序不能搞反。最后分享一个小技巧调试光照自适应模块时可以把调制参数可视化出来看。把缩放因子按通道画成曲线正常情况下不同光照条件下曲线形状应该有明显差异如果几乎重合说明调制没起作用得回头检查初始化或者学习率设置。这个可视化手段比盯着loss曲线直观得多能快速定位问题出在调制模块还是主干上。