如果只看名字,“计算照明”很容易被当成“怎么把场景照亮”的打光技巧。但把它放进成像这条线里重新理解,你会发现完全不同的东西:光源在这里不是帮相机照明的配角,而是把未知量主动编码进光场、再由相机解码的主角。光度立体、格雷码、相移,恰好就是三条最经典的编码路径——分别从光的方向、空间图案和正弦相位上做文章。这篇文章对应计算成像系列第17讲,核心就是把这三项技术放在同一条逻辑线下拆开,讲清楚它们分别解决什么问题、公式为什么那样写、组合系统应该怎么搭。适合正在做三维重建、机器视觉测量、结构光系统研发的读者,也适合刚入门计算成像、想建立整体认知的同学。
1. 从“照亮”到“编码”:三项技术共享的底层逻辑
1.1 光不再是能量,而是可编程的信息载体
传统成像系统里,照明通常被视为外部条件:环境光够不够、色温对不对、有没有反光。相机只是被动记录场景辐射度,光源本身不携带任何“设计过的信息”。计算成像的核心转变,就是把光源变成系统里一个可控的自由度——你可以在不同帧之间改变光的强度、方向、颜色、偏振、空间分布,然后让这些已知变化成为求解未知量的约束。
这就像与人交流:被动观察是听一个人自言自语,线索有限;而计算照明是你主动向场景提问,每次改变光源条件,就等于问了一个精心设计的问题。光度立体问的是“从左边打光时你长什么样”,相移问的是“这条正弦条纹在你表面偏移了多少”,格雷码问的是“你位于第几条编码区间”。场景的响应被图像传感器记录下来,剩下的工作就是解码。
1.2 调制维度:它们分别在光的哪个属性上做文章
理解这三项技术的差异,最直接的方式是看它们调制了什么:
| 技术 | 调制维度 | 核心观测 | 典型输出 |
|---|---|---|---|
| 光度立体 | 光源方向(时间序列) | 同一视点下不同方向光的反射强度 | 表面法向、反照率 |
| 相移 | 正弦条纹的相位偏移 | 条纹在物体表面形变后的相位 | 包裹相位、相对高度 |
| 格雷码 | 二值条纹的码字位置 | 每个像素落在第几条带 | 条纹级数、绝对相位 |
这三者并不是竞争关系。光度立体更关心表面的朝向和材质属性,对深度不敏感;相移和格雷码则更像是结构光测量中负责“精确测量坐标”的两级齿轮——格雷码负责周期级定位,相移负责周期内插值。实际系统里经常把它们串起来用。
1.3 病态变良态:计算照明真正带来的红利
单张图像在很多重建问题里是病态的。比如经典的明暗恢复形状(shape-from-shading),只给定一幅灰度图,光照、法向、反照率全都未知,方程数永远少于未知数,只能靠强先验硬猜。但如果你能控制光源方向,情况立刻变了:已知方向、已知辐照度,未知量只剩反照率和法向,而且方程从偏微分方程变成了逐像素的代数方程。
这就是计算照明的本质价值——它通过主动调制,把不适定问题变成良态问题。不是算法变聪明了,而是我们主动给系统提供了更多已知约束。后面几个章节的所有公式,本质都是在对同一句话做不同形式的展开:“因为光源是我控制的,所以我能从图像强度里解出我想要的几何量。”
2. 光度立体:光源方向里藏着的表面法向
2.1 朗伯模型:把反射方程改写成线性方程组
光度立体的前提假设是朗伯反射:表面把光均匀散射到所有方向,观测强度与视线无关,只与表面法向和入射光方向的夹角余弦成正比。若第 i 帧光源方向为向量 l_i,表面法向为 n,反照率为 ρ,那么像素灰度满足:
E_i = ρ · max(0, n · l_i)
把 ρn 看作一个三维向量 g,上式就是线性方程 E_i = l_i · g。光源方向是已知的,像素灰度是观测值,未知量 g 只有三个分量。理论上只要三帧不同方向的光源,就能解出 g,再对 g 取模长得到反照率、归一化得到法向。
这里有个细节容易被新手忽略:“点乘”要求光源方向已经归一化,而且你假设的是无穷远处平行光。近场光源如果距离表面太近,方向会随像素位置变化,必须按入射方向逐像素计算,否则重建出来的法向图会出现大范围弯曲畸变。
2.2 为什么实际系统都要打四路甚至更多光源
三方向是数学上最少需求量,但它非常不稳健。真实场景里至少有三个问题:光源方向一旦标定有误差,三方程系统没有冗余,误差会直接进入法向解;某个方向的入射光被物体自身遮挡时,该像素在这帧数据基本失效;纹理很平的区域,单帧噪声占比会被放大。
所以实际系统普遍用四路、六路甚至更多光源,把方程写成超定形式,再走最小二乘。比如经典的“四象限”布置:左上、右上、左下、右下对称放四盏灯。这种布局的好处是,阴影或高光区域通常只出现在特定方向,四个方向之间可以互相投票,配合一个鲁棒损失函数就能把异常像素剔除掉。
2.3 光源方向标定:镜面球的低成本方案
光度立体对光源方向的误差极其敏感。你如果直接用尺子量灯的位置、手工计算方向,最终法向图一定歪。工程里最简单的标定方法是用一个镜面球:球的半径已知,放一个高光点,镜面反射的高光点对应球面某位置,反射方向指向光源,从而反推出光源入射方向。实际操作时让球占据画面一定面积,对高光点做椭圆拟合,把球心位置、高光点位置和反射定律结合起来,就能得到光源方向,精度远高于手工测量。
阴影剔除也是必须做的一步。理想朗伯模型里,n · l_i 小于 0 的像素应该输出 0,但真实相机会有环境光、黑电平偏置,导致本应全黑的区域出现微弱灰度。这时可以设定一个低阈值:某帧灰度低于该值时,该像素不参与该帧的最小二乘。更稳的做法是先用中值或者最大灰度做一个粗糙的置信度图,再对残差使用 Huber 损失迭代求解。
2.4 代码示例:Python 三行实现最小二乘法向求解
如果光源方向矩阵 L 是 m×3,图像序列拉平后是 m×N,可以直接用最小二乘闭式解:
import numpy as np def photometric_stereo(images, L): """ images: (m, h, w) 灰度图序列 L: (m, 3) 归一化光源方向 返回 normals: (h, w, 3) 法向图, albedo: (h, w) 反照率 """ m, h, w = images.shape im_flat = images.reshape(m, -1) # (m, N) G = np.linalg.pinv(L) @ im_flat # 最小二乘解 g = (3, N) albedo = np.linalg.norm(G, axis=0, keepdims=True) normals = G / (albedo + 1e-12) return normals.T.reshape(h, w, 3), albedo.reshape(h, w)这个实现没有做阴影剔除,直接对全部像素求了伪逆。实际项目里建议在循环内用置信度掩膜截掉低灰度帧,或者用一个 scipy 的 least_squares 逐像素迭代,效果会好很多。我自己跑工业表面检测时,四方向最小二乘已经能稳定出法向图,但从四方向换到六方向后,边缘和细划痕的法向残差明显下降——多两个方向带来的冗余,比换更复杂的算法有用得多。
3. 相移轮廓术:正弦条纹为什么能量到亚像素
3.1 正弦条纹的核心优势:单频、连续、可解析
比格雷码更进一步的是相移法。投射一组正弦条纹到物体表面,相机拍到的条纹会因为物体高度发生相位偏移。物体表面每一点的深度,都体现在该点正弦强度序列的相位里。
选择正弦而不用方波,最关键的原因是:对焦模糊只影响正弦图案的幅度对比度,不影响其相位位置。你可以想象一根正弦波被高斯模糊卷积——它依然是同频率、同相位的正弦波,只是振幅变小了。这使得相位测量对离焦、投影模糊、表面微凹凸都保持稳健,而这正是二维定位类结构光代码难以做到的。工业上很多高精度面形测量,宁可用更多帧数也坚持投影正弦条纹,就是因为这个性质。
3.2 N步相移:三步公式、四步公式与取舍逻辑
让投影仪连续投射 N 帧相位偏移的正弦条纹,强度模型写作:
I_i(x) = I'(x) + I''(x) · cos(φ(x) - δ_i),δ_i = 2πi / N
其中 I' 是背景光强,I'' 是调制幅度,φ 就是待求的相位。因为三个未知量(I'、I''、φ)至少需要三个方程,所以三步相移是最少帧数。三步公式是:
φ = atan2(√3(I2 - I3), 2I1 - I2 - I3)
四步相移则更常用,因为它的解算公式特别干净:
φ = atan2(I4 - I2, I1 - I3)
四步好在两点:I1 与 I3 相减、I2 与 I4 相减,直接把背景光强这一项消掉了;同时对偶次谐波畸变天然不敏感。投影仪实际输出的亮度响应并不是理想线性,而是带 gamma 幂律的变形,会产生谐波分量。三步相移会被这些谐波干扰成周期性波纹,四步相移能压住相当一部分。
帧数越多,抗噪性越好,但会牺牲采集时间。测量光滑连续表面、物体静止不动时,我推荐直接上七步或九步相移,相位噪声能低一个量级;动态场景或生产线在线检测,才需要用三步/四步去换速度。
3.3 atan2 与相位包裹:真正的分水岭在这里
不管几步相移,最后都用 atan2 解相位,这意味着输出值被限制在 (-π, π] 的区间里。真实的相位沿着条纹方向不断累积,可能超过 2π 好几圈,从 atan2 出来却会突然跳回 -π。这就是“相位包裹”:明明是一条连续斜坡,解出来却像锯齿一样一截一截地断裂。
解开包裹是整套系统最关键的一步。如果物体表面连续、没有大台阶,可以用空间解包裹算法,对相邻像素相位做积分。可一旦表面有断裂、遮挡、深凹槽,相位差超过 π 的边界就会把整个积分拉飞——一条错误传播出去,一整行数据全废。这也是我会在后一节专门讲格雷码的原因:空间展开不可靠时,就上时间展开,用额外帧把每个像素的周期编号硬编码出来。
4. 格雷码解码:为什么二进制不够、相邻码距必须为1
4.1 从相位解包裹的需求切入
空间解包裹最怕的是“跳变超过 π”的像素。时间展开思路则是:不靠邻居,靠额外的投影帧来告诉每个像素“你现在属于第几条条纹区间”。给每个条纹区间编号,从 0 到 2^N - 1,然后逐位投影黑白条纹图案,每个像素在 N 帧序列中读出一串 0/1,这个二进制串就是它的绝对周期编号。
如果直接用自然二进制编码,相邻编号可能差很多个 bit。比如从 0111 变到 1000,五个 bit 全翻转。投影和成像系统的散焦、噪声、曝光波动会导致某个 bit 判错,哪怕只错一位,在自然二进制里可能直接跳到相隔好几个周期的位置上,绝对相位错了一大截。
4.2 格雷码的性质:相邻码字只有一位不同
格雷码就是为此设计的。相邻两个十进制数对应的格雷码,保证只翻转一个 bit。这样即使边界处有一像素判错,解码结果最多偏差一个周期,配合中值滤波和区域生长,误差可以控制在可接受范围内。
十进制转格雷码的公式非常简单:
def decimal_to_gray(n): return n ^ (n >> 1)反向恢复也很直观:格雷码最高位等于二进制最高位,之后每一位由前一位与当前格雷码位异或得到。换个角度理解,就是“每一位二进制码 = 它前面所有格雷码位的累积异或”。边界判错的影响不会再被二进制编码放大,这是格雷码能在结构光里存在几十年的根本原因。
4.3 投影散焦与边界像素:解码的大坑
格雷码理论与工程实现之间隔着一层现实:投影仪镜头必然散焦,黑白条纹在物体表面的过渡不是阶跃而是斜坡,边界附近会有多个像素读出的码值不确定。如果不处理,你会看到物体表面周期性出现一行一行的伪条纹。
工程上通常从两个方向下手:一是在解码时把边界置信度低的位置标为无效,典型做法是用“黑白反转图案”做二次判断:同一 bit 再投一帧反色图,两声明的绝对差值超过阈值就标记为不可靠;另一种做法是投影时故意比相移条纹粗,只让格雷码负责粗等级定位,把亚周期细分完全交给相位——即便边界附近个别像素错了,相位场会因为区域生长和滤波被修复一大半。这一点在组合系统里尤其重要。
5. 组队方案:格雷码加相移的高精度结构光链路
5.1 绝对相位 = 条纹级数 + 包裹相位
单独使用相移,最大问题是没有绝对周期信息,只能测相对形变;单独使用格雷码,精度只有条纹宽度级别,远远不够。把两者合起来才是工业结构光最经典的姿势:先用格雷码解出每个像素的周期编号 k,再用相移解出周期内的精细相位 φ,绝对相位直接相加:
Φ = φ + 2πk
拿 7 位格雷码加四步相移举例:格雷码把画面分成 128 个条纹周期,每个周期内的相位又被 atan2 细分成连续的亚周期值。最终每个像素得到一个绝对相位,对相位做标定后,就能映射到投影仪的水平坐标,与相机像素形成一一对应,靠双目三角原理重建三维坐标。
5.2 系统标定:把投影仪当成“逆向相机”
很多人第一步跑通相位解算后,卡在“相位怎么换成深度”上。这里的关键认知是:投影仪可以被逆向理解为一台相机——它把灰度图“拍摄”到物体表面。因此可以用张氏标定法把投影仪当相机一起标定:投影棋盘格或编码图案到多个姿态的标定板,建立投影仪像素与三维点之间、相机像素与三维点之间的对应关系,解出两者的内外参和相对位姿。
标定完,前面得到的“绝对相位 → 投影仪x坐标”映射就成了最核心的查找表。此后任意被测物体,只要解出每个相机像素的绝对相位,就能找到它在投影仪图像里对应的列坐标,再结合双目几何直接三角化。
5.3 工程链路里的三个真实战场
第一个战场是投影仪 gamma 非线性。输入灰度 0-255 经过投影仪的光电响应,实际输出光强是输入值的幂函数,余弦条纹会变形,带来固定周期的高次谐波误差。解法不外乎三种:预标定 gamma 曲线做逆变换查表、用多步相移把谐波分量平均掉、或者在做相位误差补偿时用查表修正。我实测下来,最省事的是 gamma 查表预校正,一次标定之后,三步相移也能达到接近四步的效果。
第二个战场是环境光和黑电平。投全黑帧和全白帧做归一化,能把环境光偏置从条纹强度里消掉;对黑色或低反光物体,还要适当增大投影亮度、延长曝光时间。这里不得不提一句:很多团队在算法上花几个月,最后发现噪声来自相机黑电平没校准,调试顺序真的很重要。
第三个战场是动态场景。格雷码加相移整体可能需要 8 到 12 帧,物体稍微晃动,相位场就会出现运动伪影。如果被测物体不是严格静止,要么缩短帧数(格雷码位宽做小、相移用三步),要么上高速投影方案,把格雷码和相移合并成单帧复合图案。后者原理复杂,实际项目里多数人倾向“能静止就静止,不能静止就换散斑方案”。
6. 实战选型:法向图、相位图、深度图分别用在什么场景
6.1 先搞清楚输出物是什么
光度立体最终输出的是法向图和反照率图,它和结构光的深度图有本质区别。法向图描述的是表面的倾斜程度,对微小起伏、划痕、凹坑特别敏感,但不携带绝对尺度信息;结构光输出的是毫米级坐标,适合测量深度落差和形貌尺寸,但对细小平滑缺陷的灵敏度往往不如法向图。选型时别只问“哪个精度高”,要问“我到底要看表面缺陷,还是要测立体尺寸”。
6.2 方法取舍一览
| 方法 | 核心输出 | 帧数 | 适用表面 | 典型场景 |
|---|---|---|---|---|
| 光度立体 | 法向图、反照率 | 3-10 | 漫反射为主 | 纹理表面缺陷检测、材质重建、法向贴图采集 |
| 纯相移 | 包裹相位、相对深度 | 3-9 | 连续光滑表面 | 光学面形测量、静态形变分析 |
| 格雷码+相移 | 绝对相位、绝对深度 | 8-12 | 漫反射、静止物体 | 工业三维测量、逆向建模、尺寸检测 |
| 散斑相关 | 稀疏/稠密深度 | 1-2 | 纹理弱表面 | 动态场景、人体扫描、无法投影条纹的场合 |
光度立体对光源稳定性要求高,但对深度范围不敏感;格雷码+相移对深度范围不敏感,但对静止要求高。两者不是互相替代,而是互补的。我见过不少检测项目,第一遍用结构光测出平整的表面,以为是良品,换光度立体一看,法向图上全是肉眼几乎看不见的砂眼——两种信息缺一不可。
6.3 组合使用的经验
如果条件允许,我建议在系统里同时支持光度立体和格雷码+相移两条链路。光度立体给表面缺陷检测提供高灵敏度法向图,格雷码+相移给宏观尺寸测量提供绝对坐标,两者共用一个相机和光源控制器,只是切换不同的投影/光源驱动。这个思路听起来多了一套校准,但实际项目里收益非常大:同一个物体的缺陷检测和尺寸测量,可以一次装夹完成,不用在两个工位之间反复搬动。
最后再分享一个让我踩过坑后加倍重视的经验:不管用哪种方法,先把“亮度链路”校好再谈算法。光源方向标定、投影仪 gamma 校正、相机黑电平、环境光抑制,这四件事看似基础,实际决定系统上限。公式和代码只是把已经调制好的光解码出来,光本身不干净,后面一切算法都是白搭。我第一次做光度立体时总觉得自己代码有问题,折腾了三天,最终发现是光源标定矩阵少转置了一次。调系统的耐心,很大部分不是花在数学上,而是花在这些不断重复的基础环节里。希望这篇第17讲的内容能帮你在三条编码路径之间建立清晰的地图,少走我走过的弯路。