
前阵子朋友让我帮他写一个透明塑料瓶颜色识别的脚本我最初以为这事简单得很——mask把瓶子区域抠出来转HSV求均值完事。结果瓶子放到工作台灯下一照ROI里那一片白色高光直接把颜色均值拉到离谱。试过固定阈值压高光试过换几个颜色空间做滤波折腾了一晚上都压不干净。后来才意识到问题不在于怎么“压高光”而在于我一直没搞明白物体表面的反射光到底是怎么组成的。直到认真吃透双色反射模型我才真正搞清楚这片高光从哪里来、应该怎么处理。这篇文章就把我对双色反射模型的理解、踩过的坑和落地方法完整写下来希望能帮到同样被高光、颜色失真折磨过的人。双色反射模型Dichromatic Reflection Model是1985年Shafer提出的一个反射物理模型核心观点非常直接非金属物体表面反射到观察者眼中的光可以拆成两个独立分量——接口反射镜面反射和本体反射漫反射。对于做计算机视觉、图像处理、颜色科学相关工作的朋友来说这个模型几乎是理解一切高光问题和颜色恒常性的基础。哪怕你只是偶尔写点图像处理脚本了解这个模型也能少走很多弯路。1. 先从一次高光翻车聊起传统漫反射模型为什么不够用1.1 一个看似简单实则翻车的颜色识别需求朋友那套流程今天看是很典型的入门做法。先取一帧图像用颜色阈值把瓶身区域选出来然后在这个区域里计算HSV均值。听起来顺理成章但真正跑起来就会发现灯光直射的地方塑料瓶表面会出现一大片接近纯白的高光斑块。那片区域在HSV空间里饱和度和明度全都异常直接把均值带偏。我当时采取的补救措施是什么我试着把亮度超过一定阈值的像素直接扔掉再算均值。效果比之前好一点但仍然不对。因为高光的边界不是整齐的有些过渡区域亮度没那么高却已经被高光污染了颜色。阈值设高了污染还在设低了把正常颜色区域也给误删了怎么调都不对劲。后来我又尝试把RGB转成Lab用a、b通道做统计仍然绕不开高光点在色度上的偏移。这里的问题本质是我一直在用“单分量”的思维处理“双分量”的光信号。我以为颜色是物体性质的直接反映只要避开太亮的像素就行。但真实场景中相机每个像素接收到的能量其实是镜面反射分量和漫反射分量的叠加两者的比例会随视角、光源方向、表面微观结构不断变化。你拿一个线性混合后的颜色去推物体的固有颜色当然得不到稳定结果。1.2 朗伯模型的假设盲区大家刚入门图形学和视觉时接触最多的就是朗伯模型Lambertian Model。朗伯模型假设表面是理想漫反射面反射光亮度只跟入射角和表面法线有关和观察方向无关而且反射光在所有方向均匀分布。这个假设在描述石膏像、纸张、粗糙墙面这类表面时非常好用但一旦涉及塑料、陶瓷釉面、油漆表面、光滑水果表皮这类带有明显光泽的表面模型偏差就非常大。偏差主要来自朗伯模型完全没考虑镜面反射项。现实世界大部分非金属物体都是“漫反射镜面反射”的混合体。镜面反射分量的存在不仅会让局部亮度异常高还会让局部颜色向光源颜色偏移。光源是白光高光就是白色光源是暖黄光高光就偏黄。这就带来一个很要命的问题同一个物体的同一种颜色在不同光照条件下拍出来像素值根本不是同一个数而是分布在一个由物体色和光源色共同张成的色度区间里。理解了这一点前面那个翻车脚本失败的原因就再明白不过了。只用朗伯模型思维去处理含高光的图像相当于把一个二维平面的问题强行压到一条直线上来解释必然失真。双色反射模型的价值就在于它把“反射光”明确拆成两支并且告诉你可以通过颜色空间的线性结构把它们分开后续的高光去除、光源估计、材质判断才能走上正轨。2. 双色反射模型的物理内核把反射光拆成两支2.1 接口反射决定高光颜色的并不是物体本身双色反射模型英文叫Dichromatic Reflection Model“dichromatic”意思就是“两种颜色的”。但这里说的“两种颜色”并不是指物体有两种颜色而是指反射光来自两个物理机制各自带有不同的光谱特性。第一个分量叫接口反射Interface Reflection也叫表面反射。光线从空气射到物体表面时在空气和物体材质的交界面处直接发生反射并没有进入物体内部。这部分反射遵守反射定律入射角等于反射角方向性极强所以表现为光斑形式的高光。它的光谱能量分布和光源几乎一致——光源是什么颜色高光就是什么颜色和物体本身的颜色没什么关系。这里有个很重要的知识点接口反射的强度随几何条件变化非常敏感。光源、表面法线、观察者三者之间只要角度稍微变一点反射强度就会剧烈波动。这也是为什么同一物体换个角度拍高光位置和强度差异很大。做视觉算法的人看到这种“不稳定的亮点”第一反应往往是当成噪声滤掉但它是真实物理信号不是噪声想彻底滤掉就要先知道它的数学模型。2.2 本体反射这才是物体颜色的真正来源第二个分量叫本体反射Body Reflection也叫体反射、漫反射。光线的另一部分射入物体表面之后会在物体内部的颜料粒子、微小结构之间发生多次折射和散射一部分散射光最终又从表面穿出进入人眼或相机。因为光线在物体内部经历了选择性吸收——物体吸收了一部分波长的光反射出另一部分——所以这部分光携带了物体固有的光谱特征。物体是红色是因为它对红波段散射多、对其他波段吸收多物体是蓝色则相反。这就是我们常说的“物体颜色”的真正来源。本体反射的方向性很弱在各个观察方向上分布比较均匀这就是为什么漫反射区域从不同角度看颜色都比较稳定。在计算机视觉里想要提取物体真实颜色拿到的基本就是本体反射分量。两支光线叠加在一起相机感光元件最终接收到的总反射光可以写成L(λ, i, e, g) m_i(i, e, g) · c_i(λ) m_b(i, e, g) · c_b(λ)其中i是入射角e是观察角g是入射方向和观察方向之间的相位角。m_i和m_b分别是接口反射和本体反射的几何比例因子它们只跟角度有关c_i和c_b分别是两个分量的光谱能量分布只跟波长λ有关。这个公式看着简单但它蕴含了一个非常关键的“可分离性”假设几何因子和光谱因子可以分开乘不会混在一起。也就是说无论光照角度怎么变两个分量的“颜色”保持不变变化的只是各自强度。正是这个假设才让后续从颜色空间分离两个分量成为可能。2.3 两个分量的关键差异一张表说清楚接口反射和本体反射在物理表现上差别非常大理解这些差异是后续算法设计的基础。我把它们的核心差异整理成一个表格方便对照对比维度接口反射镜面反射本体反射漫反射反射位置物体表面交界处物体内部散射后出射几何方向性强遵循反射定律弱近均匀分布光谱来源光源光谱物体固有光谱是否携带物体颜色信息基本不携带携带对观察角度敏感度非常敏感不敏感典型表现高光光斑物体本色区域提示这个表格里的“基本不携带物体颜色”对非金属材质成立对金属不成立后面讲适用范围时会单独说。实际看一张塑料瓶图像高光区域的像素值基本等于“光源色乘以一个较大的m_i再加上本体反射分量”非高光区域则主要是“本体反射分量乘以一个较小的m_b”。所以同一物体表面所有像素在RGB颜色空间里不会聚成一个点而是分布在一条折线或者一个平面上。这条折线的两个端点方向一头指向光源色一头指向物体色。这是双色反射模型在图像分析中最直观、最实用的推论。3. 在RGB空间中分离两个分量从理论到可落地的计算3.1 关键前提利用颜色分布的形状如果只停留在公式层面双色反射模型就只是个漂亮的理论没法解决实际问题。真正让它产生巨大价值的是它在RGB颜色空间里展示出的几何结构。一个表面颜色均匀的物体在现实光照下所有像素的颜色值会落在一个由两个向量张成的平面附近。第一个向量是光源颜色方向对应接口反射第二个向量是物体颜色方向对应本体反射。由于两个分量的强度比例随位置变化像素点在这个平面上沿着两个方向游走。高光的像素点离光源色方向更近漫反射区域离物体色方向更近中间区域则分布在两者之间。这句话翻译成算法语言就是如果你有一堆来自同一个颜色表面的像素你不需要事先知道光源颜色和物体颜色只要在RGB空间对这些像素做主成分分析PCA找到它们分布的两个主要方向就同时得到了光源色方向和物体色方向。这就是双色反射模型最精彩的地方——把物理问题变成了线性代数问题。当然前提条件是图像中该区域要包含足够多的高光像素和漫反射像素让两个方向都能被数据撑起来。如果整块表面都是镜面反射没有一个漫反射像素或者反过来全是漫反射看不到高光那么数据分布就退化成一条线主成分分析只能找出一个方向另一个方向要靠先验知识补上。3.2 SVD主成分分析分离双色的完整过程在实际做分离时我习惯用奇异值分解SVD而不是直接调用PCA接口因为SVD除了求出主方向外还能顺便给出奇异值奇异值的大小直接反映每个方向能解释的方差量级非常直观。流程是这样的先把目标表面区域的像素全部取出比如有N个像素每个像素是RGB三维向量组成一个N×3矩阵。然后做去均值处理——减去所有像素的平均颜色让数据云中心移到原点。注意去均值这个步骤很关键不去中心的话第一主成分会被整体亮度方向带偏反而不容易找到真正的反射分方向。之后对去中心后的矩阵做SVD分解。用numpy写的话核心代码非常短import numpy as np # pixels: (N, 3) RGB 数组已经截取到同一材质表面 mean_color pixels.mean(axis0) centered pixels - mean_color U, S, Vt np.linalg.svd(centered, full_matricesFalse) # Vt 的行向量是主方向 primary_dir Vt[0] # 最大方差方向 secondary_dir Vt[1] # 第二方差方向分解完之后Vt的第一行就是数据分布方差最大的方向在双色反射模型的理想情况下它指向物体色和光源色在平面上的某个混合方向第二行是与之正交的第二个主方向两个方向张成像素颜色分布所在的那个平面。第三行方差很小对应噪声和模型误差可以忽略。实际操作时一般会再做一个修正由于双色反射模型假设两个分量的颜色都是非负的分离出来的两个基向量理论上都应该落在RGB正值象限如果SVD给出的某个基向量带明显负成分说明数据里混入了其他表面或镜面反射像素需要回头清理ROI。3.3 一个具体数值例子手动推演分离过程为了让大家对“投影分离”有个具体概念我拿一个简化例子手动算一遍。假设某个像素的RGB值是P (200, 150, 100)。我们通过高光区域估计出的光源颜色方向是L (1.0, 0.9, 0.8)这个方向就代表接口反射分量的方向。先算这个像素在光源方向上的投影系数。点积P·L 200×1.0 150×0.9 100×0.8 200 135 80 415。然后算|L|² 1 0.81 0.64 2.45。投影系数k 415 / 2.45 ≈ 169.4。接口反射分量 k × L ≈ (169.4, 152.5, 135.5)。把像素值减去这个分量剩下的就是本体反射分量P - (169.4, 152.5, 135.5) ≈ (30.6, -2.5, -35.5)。细心的朋友会发现计算出来两个负值。这其实是个很常见的现象我在实际处理中也经常遇到。负值通常说明估计的光源颜色方向和真实方向有偏差或者像素里混入了噪声导致投影时高估了接口反射分量的幅度。遇到这种情况工程上最简单的处理是把分离后的负值裁剪到0得到(30.6, 0, 0)但这个结果相差有点大说明投影方法本身对光源颜色估计误差非常敏感。提示这个敏感性问题在真实项目里非常突出。用投影法做高光去除时光源颜色方向估计偏差5%到10%分离后的漫反射颜色就会出现明显偏差。所以实际项目中光源估计这步不能省精度直接决定后续效果。这也是为什么很多更严谨的高光分离算法会选择先在高光像素上做聚类用多个高光像素的色度分布去拟合光源色方向而不是用单个像素暴力投影。说到底双色反射模型给了明确的理论框架但工程落地时还需要配合稳健的估计手段。4. 双色反射模型的实战价值高光去除与颜色恒常4.1 高光去除不靠“阈值压”靠“分量剔除”基于上面讲的分离逻辑高光去除的正确做法就很清晰了。传统做法是用阈值把高光区域找出来然后对高光区域做像素填充或者颜色替换这种做法的毛病是边界生硬恢复出来的颜色也没有物理依据。基于双色反射模型的做法是不要试图“修”高光像素而是把每个像素里的接口反射分量剥掉只保留本体反射分量。实现上利用上文的SVD求出两个基向量后可以将每个像素颜色投影到本体反射方向上得到去除高光后的颜色。实际操作中还有一种更简单直接的近似方案适合光源颜色已知的场景对每个像素P估计接口反射的强度k max通道的比值关系或用投影系数计算然后本体反射分量P_b P - k × light_color。这种逐像素投影的做法在光滑表面上效果很不错尤其在只有一个主光源、没有复杂互反射的场景下。我记得第一次用这个思路重建高光区域的漫反射颜色时那种“高光处终于不是一片死白而是出现了真实物体颜色”的感觉非常明显。图片里的塑料瓶从灯下的一团混乱变成了稳定的颜色区域后面的颜色统计就顺理成章了。4.2 光源颜色估计给颜色恒常算法提供先验双色反射模型另一个让我印象深刻的实战用途是光源颜色估计。传统颜色恒常算法有灰色世界假设、白斑算法、灰度边缘等这些方法各有各的假设基础但都绕不开一个痛点当画面中物体本身就有大面积偏色时算法会误判光源颜色。双色反射模型提供了一个非常物理的估计途径接口反射分量的颜色就是光源颜色所以直接从高光像素就能推断光源色。只要在图像里找到一片均匀材质的镜面高光区域取其中色度最集中的像素颜色就能作为光源色估计值。这个方法在很多真实场景下比灰色世界假设可靠得多。我来举个实际工作中的例子。有一次拍一组包装盒照片背景是大面积红色卡纸整个画面红彤彤的。如果用灰色世界假设算法会认为环境光是偏绿的因为要“中和”红色的偏置这显然是错的。而我直接定位到纸盒上的高光条带取高光区域像素的高位色度很快就得到了接近真实白炽灯的光源颜色。整个过程只需要几步通过亮度阈值和色度集中度筛选出高光候选像素对候选像素的色度做直方图统计簇的中心就是光源色如果多个高光来自不同物体可以做一次聚类取面积最大的簇这个“直接从高光像素读光源色”的思路特别实用在自动白平衡流程里可以作为前置模块给后续色彩校正提供强先验。很多文献里的颜色恒常系统也把双色反射模型作为一个重要模块理由就是它的物理可解释性太强了。4.3 材质光泽度与反射分量比例除了颜色分离和光源估计双色反射模型还给材质感知提供了一个量化指标。接口反射分量和本体反射分量在总反射中的比例m_i / (m_i m_b)和表面光泽度直接相关。一个表面如果非常光滑比如抛光塑料、釉面陶瓷、光滑皮革镜面反射分量占比高高光区域小而亮。反之粗糙表面比如纸张、布料、哑光塑料漫反射分量占绝对主导高光微弱甚至看不见。基于这个比例可以做简单的材质分类特征。我把一组常见材质的反射特性整理成了一个参考表方便大家在实际项目中做粗略判断材质接口反射占比高光表现双色模型适用性哑光纸张低基本无高光适用但分离意义不大磨砂塑料中低弥散高光适用光滑塑料高锐利高光非常适用陶瓷釉面高明亮高光非常适用抛光金属极高极亮高光不适用无体反射这个表格本质上是在提醒大家双色反射模型不是一个万能的颜色解析工具它就是描述特定一类表面的模型。用之前先确认材质类型比什么都重要。5. 双色反射模型的失效边界与我的实测心得5.1 金属材质模型为何不适用双色反射模型最著名的适用范围限制就是对金属不适用。原因要从物理机制上讲金属表面有很多自由电子光线入射到金属表面时并不会进入内部发生体散射而是直接在表层和电子相互作用后反射出来。这个反射过程对不同波长的光响应不同导致金属的“镜面反射”光谱会带有金属本身的颜色特征。所以金戒指的高光是金黄色的铜器的高光是铜红色的。金属表面没有本体反射这个机制它只有“带着自身光谱特征的接口反射”。用双色反射模型去描述金属两个分量的假设直接被打破模型自然失效。这一点在工业视觉检测里特别关键。做金属表面的缺陷检测、划痕识别时千万不要套用基于双色反射模型的高光去除流程。我见过一个同行把高光分离算法用在金属工件图像上结果分离出来的“漫反射分量”完全是负值乱跳根本无法使用。后来换成基于微面元理论的渲染模型去拟合效果才正常。5.2 互反射与复杂表面线性假设被打破双色反射模型还有一个隐患经常被人忽视它假设物体表面反射的光来源于光源且光源发出的光只经过一次反射就进入相机。但真实场景中光线完全可能经过物体表面某个区域反射后又打到另一个表面再反射进入相机。这种表面之间的光线传递叫互反射inter-reflection。举一个很直观的例子一个白色的桌面旁边放一个红色杯子桌面上靠近杯子的区域往往会泛出淡淡的红色。这就是红色杯子把光散射到桌面上桌面再把混合后的光反射给相机。在这个区域相机接收到的反射光里已经混入了“物体A的体反射×物体B的体反射”这一项这种乘积项在双色反射模型的线性框架里无法被拆成单一的光源色方向和物体色方向。互反射严重的情况下颜色分布不再是一个平面而是弯成了一个曲面。这时用SVD做PCA分解第二和第三主成分都可能含有有意义的信息不再像理想情况那样第三个主成分几乎为零。我在拍室内静物时经常遇到这个问题处理手段是尽量避免把高反差的彩色物体放得太近或者只在互反射影响较小的局部区域使用双色模型。5.3 实操建议什么场景值得用什么场景果断放弃基于这些年的实践我总结了几条选择双色反射模型的实际经验供大家参考。值得用的场景往往是这几个塑料件颜色检测陶瓷、瓷砖的色差质检纸制品印刷品的高光修正手机拍照里的高光去除以及需要从图像中估计光源颜色做自动白平衡的场合。这些物体的表面以非金属电介质为主而且大多是单一均匀材质模型假设基本成立算法简单且稳定。果断放弃的场景包括金属表面检测包含大量互反射的复杂室内场景半透明材质的背光场景以及材质本身不均匀比如两种颜色塑料拼接在一起的物体。半透明材质尤其麻烦因为光线会穿透表面又在内部多次散射再穿出来表现既像体反射又像透射双色模型的“两分量”根本描述不过来。还有一点经验如果你的图像经过了重度色调映射、美颜滤镜或者不可逆的gamma变换像素值已经被非线性地重新映射过了。双色反射模型的线性关系在这种图像里是成立的还是被破坏的取决于映射函数。如果没有原始RAW数据或线性化工具我不会在压缩严重的JPG上做高光定量分析最多做做定性观察。6. 模型之外双色到多色、微面元与深度学习双色反射模型的思维方式——把复杂反射光拆成几个有物理含义的分量——延伸性很强。实际研究和工程中很多工作就是在它的基础上做扩展的。一个方向是多色反射模型。有研究者把互反射纳入模型把反射光表达成三个甚至更多分量的叠加接口反射、本体反射、以及互反射分量。这种扩展让颜色分布的几何结构从平面变成高维体分析复杂度上升但能处理更多真实场景。这个方法在计算摄影领域用来还原带有环境互反射的场景时效果确实比纯双色模型好不过参数估计难度大很多。另一个方向是微面元模型也就是图形学渲染里著名的Cook-Torrance模型、GGX模型。它们把物体表面想象成由大量朝向各异的微小镜面组成每个微面元发生镜面反射但宏观上整体呈现漫反射效果。双色反射模型关心的是光谱层面的“颜色分解”微面元模型关心的是几何层面的“能量分布”两者研究的维度不同但理念一脉相承。做渲染出身同时又在做视觉算法的人往往能把这两套思路结合得很好。这几年深度学习流行起来之后也有很多工作把双色反射模型嵌入到神经网络里。有些高光去除网络会在损失函数中加一个“分离后颜色落在两个主方向上”的正则项相当于让网络学到物理规律也有工作在生成训练数据时先用双色反射模型仿真出大量带真实物理特征的合成高光图像再用这些数据训练网络。实测下来这类融入物理先验的网络泛化性比纯黑盒网络好很多尤其在不同光照、不同材质之间迁移时劣势很小。我自己在项目里的体会是物理模型和深度学习方法不是二选一。双色反射模型提供的是一个稳定的先验框架它告诉算法“颜色分量大概应该长成什么样”而神经网络负责在复杂场景中拟合那些模型假设没办法精确描述的残余部分。这种组合逐渐成了我处理图像颜色问题的默认思路。最后再分享一个小技巧无论是做高光去除还是光源估计都建议在图像的线性色彩空间里操作。相机直出的JPG是经过gamma变换的非线性数据直接套双色反射模型会有偏差。用RAW数据或者先做sRGB转线性RGB的变换分离效果立刻上一个台阶。这个细节很少被人提到但对结果的影响非常大。