从亮度预测色度:一种解构颜色恒常性问题的深度学习新思路 第一次看到这个论文标题时我愣了一下。“Color Constancy by Learning to Predict Chromaticity from Luminance”——颜色恒常性这个方向这几年被深度学习翻来覆去地做大家习惯性地把整张RGB图像扔给网络让它直接回归光照颜色。而这篇文章偏偏绕了个弯先把亮度Luminance单独拎出来然后让网络从亮度图预测色度Chromaticity。这个操作初听有点反直觉亮度里没有颜色信息怎么能预测出颜色但仔细想光照变化在亮度通道里留下的痕迹恰恰是颜色恒常性任务里最稳定的线索之一。这篇文章讲的不是“又堆了一个新网络”而是换了一个解构问题的角度把“估计光照颜色”拆成“从亮度推断色度”这一步用回归的思路把看似无解的耦合关系拉成可学习的映射。如果你做过白平衡算法或者被自动白平衡在复杂光源下的偏色问题折磨过这篇文章的思路会给你一个很不一样的参考。下面我把整个思路从头拆开边读边补实现细节最后给出一套可以照着跑的复现方案和排坑记录。1. 颜色恒常性到底在解决什么问题1.1 人眼和相机在“看颜色”上的根本差异先说个每个人都经历过的现象在暖黄色的灯泡下看书白纸看起来还是白的走到室外阴天同一张白纸看起来也还是白的。人眼会自动把光源的颜色“扣除”让我们感知物体本身的颜色。这种能力叫颜色恒常性是视觉系统的一种底层自动校正机制。相机就没有这种机制。传感器记录的是场景反射光进入镜头后的物理数值暖光下拍出来的白纸就是发黄的阴天拍出来就会发蓝。所以相机需要额外的算法来做“白平衡”也就是用软件去模拟人眼的自动校正能力。颜色恒常性Color Constancy这个任务本质上就是给定一张图像计算拍摄时的光照颜色然后把偏色掰回来。这里面有一个很经典的形式化表达。把图像上每个点的像素值看作物体表面反射率与光照光谱的乘积去掉复杂的高阶反射可以写成I_c(x) R_c(x) * L_c其中 I_c 是第 c 个颜色通道R、G、B的像素值R_c(x) 是那个位置的表面反射率L_c 是全局光照在三个通道上的强度。这个式子当然是简化过的没有考虑互反射、镜面反射、空间变化光照但它已经抓住了问题核心我们只拿到 I想要把 L 和 R 拆开。只要 L 估计得够准就能通过一个对角矩阵把图像校正到标准光照下这一步就是白平衡里最常用的 Von Kries 对角校正。所以颜色恒常性问题不管前面的网络怎么搭最终几乎都会落成“估计全局光照颜色 L”这个回归任务。1.2 一个看起来简单、做起来扎心的数学问题为什么这个任务这么多年都没被彻底解决因为从数学上看它本质上是一个病态问题。给定单个像素点RGB的值你根本无法判断这到底是“红色物体在白色光下”还是“白色物体在红色光下”。同一个像素值可以对应无穷多种“反射率-光照”组合约束严重不足。这也是经典方法全部依赖全局统计假设的原因。灰度世界假设Gray World认为场景里所有颜色的平均值是灰色所以把图像RGB均值当作光照的反色白斑算法White Patch假设场景里存在纯白色区域于是取每个通道的最大值。这些假设在特定场景下有效一旦场景里大面积是单一颜色或者光源比较复杂假设就崩了。深度学习方法之所以能后来居上是因为它通过学习大量数据中隐含的统计规律隐式地给这个病态问题加上了“先验约束”。但哪怕是这样深度学习也需要回答一个问题输入图像里到底用哪些信息来做光照估计最有效大多数方法给出的答案是直接用整张图的RGB像素。这在逻辑上没毛病因为颜色信息全在RGB里。但问题也在这里——RGB图像里同时包含了物体颜色和光照颜色网络要在一个耦合信号里把光照分量“拎”出来学习难度很高。这篇文章选择把RGB拆成亮度色度只让网络从亮度出发实际上是主动把“物体颜色信息”这个干扰源给屏蔽了一部分逼迫网络去学习光照在亮度通道里留下的那种更本质的模式。1.3 一个被低估的中间层能力颜色恒常性不只是学术圈自嗨。手机相机拍夜景、室内氛围灯下的人像、工业视觉里的颜色检测、自动驾驶在日落时对交通灯和路面的判断全都依赖这一步。算法偏色直接导致画面整体泛黄、泛蓝后续无论做人脸检测还是语义分割效果都会跟着崩。所以这个方向每年都有大量论文在做工业界需求也一直没断过。这篇文章真正有意思的地方不在于它又刷高了多少个点的指标而在于它提出来的“用亮度预测色度”这个中间层表示让网络不再直接输出一个三通道光照向量而是先学习把亮度和色度之间的映射关系建模出来。这个思路在工程上也有实用价值很多ISP管线上已经有很好的亮度信号可以直接复用不用临时改硬件或重新拉一套RGB统计模块。2. 核心思路拆解为什么从亮度预测色度是行得通的2.1 传统方法的基本盘统计假设与物理先验先回顾一下传统方法因为不理解传统方法就很难理解这篇论文的动机。灰度世界家族的方法核心就是假设场景平均反射率是灰色。这个假设对色彩丰富的户外场景表现不错但当你拍一面纯红色的墙或者一个篮球算法就会把红色误当成光照偏红然后在校正阶段疯狂加青色。这是均值假设的硬伤。灰边缘Grey Edge这一系引入了更高阶的信息它假设场景中边缘处的颜色导数是灰色的也就是说颜色突变处的通道差服从灰度分布。这个方法在纹理丰富的场景里表现更好但在平滑表面或强噪声下也不稳定。物理派方法会用光照几何模型做约束比如考虑镜面高光的分布或者利用双色反射模型来分离漫反射和镜面反射分量。这类方法对场景要求较高在受控工业环境里效果不错但拿到日常用户随手拍的图像上鲁棒性很难保证。这些方法的共同点是用某种固定统计量直接计算光照无法根据场景自适应调整。图片里出现一只橙色猫和一片橙色墙壁对这些方法来说没有本质区别—它们都在做“统计→猜测”的推导。2.2 深度学习方法的主流进路到了深度学习阶段大家的做法基本变成了把图像缩放成固定尺寸比如 224×224或512×512直接输入卷积网络输出一个三通道向量作为光照估计然后计算这个向量与真实光照的角度误差作为损失函数。这种端到端的学习方式靠大量成对数据把“图像→光照颜色”的映射学出来效果确实比传统方法强不少尤其是对复杂光源场景的适应能力更好了。这个路线本身没什么问题但有一个可以琢磨的细节输入的是RGB三通道输出是光照RGB中间所有特征都在做同一件事——在颜色维度上做解耦。网络需要学的是“去掉物体颜色留下光照颜色”这个任务在RGB空间里表达比较绕因为RGB三个通道高度相关且对光照变化的响应是非线性的。于是就有一些工作尝试改变输入表示比如把图像转成色彩直方图或者用色度分布作为输入特征。这篇文章选择了一种更极致的解构方式——直接把两个色度通道去掉只留亮度然后要求网络从亮度中预测出色度。这意味着网络必须从“空间结构和亮度分布的模式”里找到光照颜色的线索而不是从“像素本身的颜色”里找。2.3 “亮度预测色度”的三个直觉依据为什么亮度里藏着光照色度的信息这是全篇最核心的问题结合成像原理和实验观察我认为有三个层面的依据支撑。第一光照颜色会影响各个通道的响应比例而这种影响会改变亮度的统计分布结构。假设场景里有多种反射率的表面在不同色温光照下虽然物体的可见颜色在变但亮度值的变化模式会带有光照色度的痕迹。比如偏蓝光照会导致蓝色通道响应被整体放大这对亮度的梯度分布和通道间比值会有可观测的影响。第二场景的几何结构其实包含了光源信息。高光、阴影、边缘方向这些特征在亮度图上保留得比较完整而光照方向与颜色某种程度上是耦合的。网络完全可以从亮度图的边缘分布、高光位置、阴影软硬等线索中推断出光源的冷暖倾向。第三亮度图的信噪比更高。RGB三个通道在弱光下噪声水平不同单独使用某个通道容易放大噪声而亮度通道是三个通道的加权融合等效信噪比更高。用亮度作为输入网络学到的特征更稳定对传感器噪声和JPEG压缩伪影的鲁棒性也更好。这三点不是论文里明确列出来的推导而是我结合成像模型和复现经验补出来的解释。理解到这一层后面的网络结构设计和训练技巧就有方向了。3. 关键细节解析网络结构设计背后的逻辑3.1 输入表示与色度监督先明确输入输出。输入是单通道亮度图输出是两个通道的色度坐标或加一个归一化操作后的二维向量。色度的定义一般取r R / (R G B) g G / (R G B)这样色度坐标天然满足 r g b 1只不过 b 可以由前两个推出来所以回归两个通道就够了。从RGB图算亮度常用做法是加权求和L 0.2126 * R 0.7152 * G 0.0722 * B这组系数是标准亮度公式符合人对亮度的感知权重。复现时可以直接用 OpenCV 的 cvtColor 转灰度也可以自己用矩阵乘法做效果差别不大。要注意的是图像必须先用线性化RGB也就是去掉伽马校正否则亮度关系不线性后面学出来的映射会有偏差。3.2 为什么输出色度而不是直接输出光照RGB选择输出色度而非完整光照向量是一个很实用的设计决策。原因有二一是色度天然与亮度尺度无关。两个光照颜色向量如果只是亮度大小不同色度坐标是一样的。这等于把“光照有多亮”这个信息主动扔掉网络只需要关心“光照是什么颜色”。也就是说网络不需要同时回归亮度和颜色两个维度回归目标更简单收敛压力更小。二是便于做归一化和度量。色度坐标都在[0,1]范围输出层加一个 Sigmoid 直接限制范围比回归三通道任意实数稳定得多。而最终评估用的角度误差本质上也只关心光照方向不关心大小所以色度监督与角度误差指标是自洽的。实际实现时可以再加一层归一化例如把网络输出的二维向量看成二维角度通过三角函数映射或直接归一化模长来约束方向这比单纯回归两个独立数值更符合色度的几何意义。3.3 损失函数的选择与坑如果你直接对这个任务用 MSE 损失训练过程会非常不稳定。原因在于色度坐标的数值范围虽然只有0到1之间但角度误差对颜色方向非常敏感一点微小的色度偏差在低色度区域会被放大成很大的角度差。我更推荐的训练方案是组合损失L L_angular λ * L_mse其中角度误差损失为L_angular arccos( (gt · pred) / (|gt| * |pred|) )MSE 损失用来稳定训练早期避免梯度振荡角度损失负责让预测方向精确对齐。λ 取0.1到0.5之间先要跑几个小实验确定。还有一个容易踩的坑反三角函数的定义域。如果 gt · pred / (|gt| * |pred|) 因为数值问题跑到1.0000001arccos 会返回NaN。一般做法是在计算前做一次 clip 操作把值裁剪到[-1, 1]。我在第一次复现训练的时候就被这个NaN坑过整整半天loss曲线都是空的后来才发现是clip忘写了。3.4 网络结构选型不是越深越好这类任务跟图像分类不一样不需要从头到尾把语义信息层层抽象而是要保留空间统计信息。我的建议是用一个中等深度的卷积网络全连接层不要太多。一种比较稳的基线结构是输入224×224×1 的亮度图卷积块164通道3×3卷积步长为2后接BN和ReLU卷积块2128通道3×3卷积步长为2卷积块3256通道3×3卷积步长为2全局平均池化全连接1512维ReLU输出层2维Sigmoid这个结构参数量不大单卡就能训练关键是全局平均池化之前特征图的空间信息保留得够好。如果你要刷高指标可以换用现成的轻量分类网络去掉分类头只取特征提取部分前面加一个灰度输入适配层就行。还有一个值得尝试的小技巧在卷积特征提取阶段使用 DenseNet 风格的拼接结构把浅层亮度边缘特征和深层统计特征连接起来。虽然文章标题没有明确指出这一点但在复现这类“小输入信息量”任务时密集连接能有效缓解梯度消失也更利于网络同时利用局部边缘和全局统计两类线索。4. 复现实操从数据准备到训练评估4.1 数据集与预处理流程公开可用的带真实光照标签的数据集就那么几个复现时建议选一个规模适中的作为主实验集。数据格式一般是原始图像加一个光照颜色向量基准测试时常用角度误差的中位数、平均值、最大值作为指标。拿到数据后要按顺序处理线性化很多公开数据提供的是RAW线性图但如果拿到的是sRGB图先做反伽马变换。黑电平扣除与归一化把像素值归一化到 0~1。下采样分辨率降到 224×224这个尺寸已经够用。灰度变换用标准亮度公式把RGB转成单通道亮度图。标签变换把光照RGB向量转成色度坐标rg同时保留原始光照向量用于最终评估。这里有一个细节很容易被忽略训练时输入的是灰度亮度图但评估时必须在原始RGB空间计算角度误差。也就是网络输出的色度要先还原回三维光照向量b1-r-g再做角度误差计算。数据增强方面水平翻转是必须的因为颜色恒常性任务中左右翻转不会改变光照颜色但会翻转向量中某些空间线索的方向。随机裁剪也可以加但要注意裁剪不能裁掉太多边界本来全局统计信息就不多再裁就没了。4.2 模型实现与训练配置下面给出一份可以直接跑的 PyTorch 代码骨架结构是上面说的基线网络。import torch import torch.nn as nn class LuminanceColorNet(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 64, 3, stride2, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 128, 3, stride2, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 256, 3, stride2, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), ) self.pool nn.AdaptiveAvgPool2d(1) self.fc nn.Sequential( nn.Linear(256, 512), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(512, 2), nn.Sigmoid() ) def forward(self, x): x self.features(x) x self.pool(x).flatten(1) x self.fc(x) return x训练配置我实测下来比较稳定的一组参数优化器Adam初始学习率 1e-4权重衰减1e-5Batch size16Epoch80~120早停阈值设10个epoch学习率策略每30个epoch乘0.1关键一点在第一个epoch结束后打印一次验证集的角度误差中位数如果明显低于随机猜测水平大概55度到65度说明管线没问题可以继续往下跑。如果还是接近随机不要盲目调学习率先检查数据预处理和标签配准。4.3 评估与对比实验评估代码里最核心的部分是角度误差计算def angular_error(pred_light, gt_light): pred_light pred_light / torch.norm(pred_light, dim-1, keepdimTrue) gt_light gt_light / torch.norm(gt_light, dim-1, keepdimTrue) cos_theta (pred_light * gt_light).sum(dim-1).clamp(-1.0, 1.0) return torch.acos(cos_theta) * 180.0 / torch.pi这里要再强调一次 clip 的必要性。矩阵乘完以后浮点误差很容易让 cos 值跑到 1.0000001不 clamp 直接 acos 结果全是 NaN。做对比实验时至少要跑这几组输入用RGB三通道直接回归光照常见基线检验灰度输入的信息丢失程度输入用亮度单通道输出用色度本篇完整方法输入用亮度单通道输出用三通道光照向量验证输出色度的优势去掉角度损失只用MSE验证损失组合的必要性这样的消融设计基本能把文章核心贡献的每一项都拆出来验证。实测中第2组通常比第1组在泛化性上略好尤其在跨数据集测试时灰度输入对场景颜色变化的过拟合更少。4.4 跨数据集泛化测试颜色恒常性有一个很尴尬的现实在同一个数据集上训练再测试指标很好看但换一个场景分布差异大的数据集性能会明显下降。这是因为不同数据集使用的相机传感器光谱响应不同而且场景内容偏好不同比如室内场景和室外场景的统计特征差别很大。我建议训练好模型后一定要做一次跨数据集测试。如果发现性能掉很多可以尝试一种很有效的补救方式测试时做简单的输入适配也就是把测试图的亮度直方图匹配到训练集亮度分布。这个操作不改变光照颜色信息但能够减小传感器差异带来的亮度统计漂移通常能把跨数据集角度误差降低10%~20%。另一个更彻底的做法是训练时加入随机光照增强在训练图像上随机乘一组光照扰动向量让模型见过更多光照条件。这个增强对亮度预测色度的任务特别有效因为扰动光照会直接改变亮度图迫使网络学到对光照变化更鲁棒的特征。5. 复现中常见问题与排查技巧实录5.1 灰度输入导致的信息瓶颈与对策第一次跑这个方案我遇到的最大困惑是训练loss不断下降但验证集指标就是不动。仔细分析后发现灰度图丢失了颜色对比信息网络很容易陷入一种“偷懒”的解——不管输入是什么都预测一个接近数据集平均色度的值。应对策略是强化空间信息的作用。具体有两招很管用第一输入上叠加位置编码或梯度图。在亮度图上叠加一个由Sobel算子计算得到的边缘强度通道或者叠加一个坐标网格通道都能给网络提供额外的结构线索让它更容易利用空间模式而不是简单求均值。第二在损失函数中加入场景级约束。比如强制预测结果与场景中高光区域的色度建立一个弱一致性。这个不一定要做得很复杂可以用一个辅助分支在全局平均池化前对高光区域特征做一个加权池化并把加权特征直接接到最后的色度输出上。这个方法有点“开小灶”的意思但确实能打破僵局。5.2 数据划分的坑颜色恒常性数据集普遍存在一个特殊问题同一个场景可能在不同光源下被拍摄多次。如果训练集和测试集划分时没有检查场景重叠那么测试集里会出现与训练集高度相似的场景评估结果会虚高很多。正确做法是按场景ID划分数据集保证同一个场景的图片全部落在同一侧。我在复现时因为这个吃了大亏一开始按文件名随机划分验证集中位数误差只有3度左右感觉模型效果非常好后来改成按场景划分指标立刻跳到7度多。这个差别足以让一篇论文的结论完全反转所以评估协议一定要严格。5.3 训练震荡与角度损失数值问题另一个高频问题是训练早期loss出现剧烈震荡甚至某个batch之后直接变为NaN。排查顺序依次是输入数据里有没有NaN或Inf个别数据集的标注文件偶尔会有缺失值。标签有没有归一化如果真实光照向量里混入了一个零向量归一化会除零。角度损失有没有clip前面已经说过。学习率是否过大Adam在早期容易因为温启动参数导致大梯度可以把学习率先降到1e-5跑10个epoch再调回去。如果以上都排除了还有一个容易被忽视的点Sigmoid输出在两端饱和区梯度非常小如果标签恰好接近0或1刚开始几个epoch网络几乎学不动。解决办法是在Sigmoid之前把logits乘以一个系数比如5迫使输出在训练早期不进入饱和区。5.4 问题排查速查表现象可能原因解决方法Loss持续下降但验证集不动模型退化为预测均值叠加边缘/位置通道引入高光约束训练早期Loss变成NaN角度计算未clip在acos前用clamp限制余弦值到[-1,1]跨数据集测试掉点严重传感器差异、场景分布漂移直方图匹配测试图亮度训练时加光照增强验证集指标虚高同场景图像泄漏到测试集按场景ID严格划分数据输出色度趋于0或1Sigmoid饱和导致梯度消失在Sigmoid前放大logits或改用atan-style输出灯光偏暖的测试集上表现异常训练数据冷暖不平衡对训练集做光照扰动增强均衡冷暖分布5.5 从论文方法到工程落地的一点点心得复现到后期我越来越觉得这篇文章最大的价值不在于指标本身而在于它提供了一个很轻量的改造思路如果你的ISP管线里已经有稳定的亮度信号白平衡模块完全可以利用这个亮度通道做一次快速的色度估计不需要依赖专门的RGB统计硬件模块。这一点对手机端和嵌入式端非常友好因为亮度图的计算成本远低于RGB直方图统计也更方便硬件加速。另外这种方法的一个扩展方向是视频序列。视频里相邻帧的亮度变化具有时间连续性用亮度序列预测色度会更稳定。我在实验中发现把视频连续帧的亮度图拼成一个多通道输入预测出的光照色度比单帧预测噪声小很多。这个方向目前论文里没有展开但工程价值很实在。如果你正打算复现这篇论文我的建议是先把评估协议定清楚再开始调网络结构。这个方向最核心的问题永远是“评估是否公正”其次是“输入表示是否合理”结构反而是最不敏感的部分。记住这点你能省掉大量无效调参的时间。