用MATLAB做双目结构光三维重建,是我在实验室里折腾了大半年才跑通的一条完整链路。这个方向的核心逻辑其实不神秘:传统双目相机在光滑白墙、纯色塑料这类无纹理区域几乎失明,找不到匹配点;而结构光投影仪会主动往物体表面“画”上可解码的编码图案,把这些原本“空白”的表面变成带特征点的区域,左右相机再根据这些特征完成匹配,最后由三角测量求出每个像素的三维坐标。整套系统跑通之后,一个工业零件的点云几分钟内就能在MATLAB里完整呈现,这种成就感是单纯跑仿真给不了的。
这篇内容不是论文复述,而是我从硬件搭建、标定、编码解码、立体匹配到点云输出的全流程实操记录,目标是让读者看完之后能在自己机器上复现一套可用的原型系统。适合理工科研究生、机器视觉工程师,以及想快速验证结构光方案的自学者。文章里所有关键步骤我都会给出可落地的做法,也会把那些在论文里不会写、只有真正上手才会撞见的坑,原原本本讲出来。
1. 为什么是“双目+结构光”:系统成像模型与硬件组成
1.1 从传统双目的“无纹理困境”说起
先聊一个最基础的问题:为什么一定要用结构光?纯双目立体视觉的原理很简单,左右相机同时拍下场景,通过匹配同名点,再用视差计算深度。可一旦物体表面是白墙、金属件、透明塑料这类灰度变化很小的区域,局部窗口里的像素长得几乎一模一样,匹配算法直接“迷路”。哪怕强上全局匹配约束,要么深度图空洞一片,要么边缘出现大量飞点。
结构光解决这个问题的思路很直接:我不等你表面“自带特征”,而是主动投射编码图案,相当于往物体表面贴了一层可识别、可解码的“二维码”。只要解码成功,每个像素都带上了唯一或近似唯一的编码信息,匹配就从“猜哪个点像哪个点”变成了“查表对应”。这也是为什么结构光在工业测量里这么受欢迎——精度可控、对纹理不敏感、对环境光有一定的容忍度。
1.2 三种系统配置,哪种才是“双目结构光”
实际工程里,结构光系统大致有三种配置,很多新手一开始会把它们混在一起:
| 配置方式 | 组成 | 匹配逻辑 | 适用场景 |
|---|---|---|---|
| 单相机+投影仪 | 1个相机+1台投影仪 | 利用投影仪像素坐标充当“虚拟相机”的像点,与相机像素构成双目几何 | 低成本原型、离线测量 |
| 双相机+投影仪 | 2个相机+1台投影仪 | 两个相机都解码结构光图案,按相位一致性匹配,再双目三角化 | 精度优先、遮挡较严重的场景 |
| 多相机+投影仪 | 2个以上相机+1台或多台投影仪 | 多视角融合重建 | 大型工件、360度重建 |
很多论文里说的“双目结构光”,最严格的解释是第二种:两个相机同时采集,投影仪只负责“制造特征”。但还有一种常见的理解是第一种——因为单相机+投影仪本质上也是一个“双目系统”,投影仪相当于另一只眼。我在项目里先跑了单相机+投影仪的方案,后来为了降低遮挡导致的空洞,升级成了真正的双相机系统。本文的匹配和三角化部分会同时覆盖这两种逻辑,因为它们的标定和解码几乎完全一样,只有最后一步怎么用相位不一样。
1.3 我用的硬件与MATLAB环境清单
硬件方面,我使用的是一台DLP投影仪(分辨率1920×1080)和两个USB3.0工业相机(分辨率1920×1200,全局快门),镜头焦距8mm,基线距离约200mm,工作距离约500mm。如果你手上只有普通USB摄像头和家用投影仪,也能跑通,但要注意两点:一是必须用全局快门相机,卷帘快门在投影图案快速切换时会出现严重的条纹变形;二是最好有硬件同步触发,否则相机采图和投影仪切图不同步,解码出来的相位就是花的。
软件方面,我用的是MATLAB R2022a,依赖Image Acquisition Toolbox采集图像,Computer Vision Toolbox做标定和点云处理,Image Processing Toolbox做图像归一化和滤波。如果暂时没有相机和投影仪,也可以用离线图像序列先跑算法,把采集这步替换成读图就行,后面所有处理都不受影响。
2. 标定方案:把投影仪也当作一台“逆相机”
标定是整个系统里最枯燥但最决定下限的一步。我的经验是:标定做不好,后面解码和匹配做得再漂亮,重建出来的点云也是弯的。相机标定大家比较熟,难的是投影仪怎么标,下面分清楚讲。
2.1 相机标定:直接用MATLAB标定工具箱
相机标定我用的是MATLAB自带的Camera Calibrator工具箱,打印一张7×5的棋盘格标定板,采集15到20个不同位姿的图片,剔除重投影误差大的图像,得到每个相机的内参K、畸变系数以及两个相机之间的相对位姿[R, t],最后导出为stereoParams对象。
这一步有几个经验:标定板要尽量平整,最好贴在玻璃板或铝板上,不要用软纸;拍摄时标定板要覆盖视场的中心和边缘,尤其是四角,否则畸变参数外推会不准;每次转动标定板的角度不要太大,倾斜30度左右就够了,太斜会提取不到角点。标定结果看两个指标——单相机重投影误差要小于0.1像素,立体标定的重投影误差也要控制在0.3像素以内,否则后面重建误差会明显放大。
2.2 投影仪标定的完整套路
投影仪没法自己“看”标定板,所以需要一个中间人:相机。基本思路是,相机拍下投影仪投射到标定板上的编码图案,通过解码反推出每个相机像素对应的投影仪像素坐标,再把这些对应关系带入张正友标定框架,算出投影仪的内参和畸变。
我用的具体流程是这样的:
- 固定标定板位姿,先用正常白光拍摄一张图像,提取棋盘格角点坐标(这是“物理点”)。
- 投影仪投射竖直方向的正弦相移条纹和格雷码条纹,相机同步采集。对每个相机像素解码出对应的投影仪列坐标x_proj。
- 在相机图像中,对每一步位姿下的棋盘格角点,通过角点邻域的相位插值,得到每个角点对应的投影仪列坐标。因为角点不一定正好落在整数像素上,我用周围像素的绝对相位做双线性插值,获取亚像素精度的投影仪坐标。
- 此时每个标定板位姿下,都有“标定板上的三维物理点”和“投影仪像素坐标”的对应关系,正好满足张正友标定对输入数据的要求,用MATLAB的estimateCameraParameters就能标出投影仪的内参。
- 旋转标定板,换8到10个位姿,重复以上步骤。
关键细节:只投影竖直条纹只能求出x_proj,投影仪行坐标v_proj是缺失的。如果你后面是用“相机像素 + 投影仪列坐标”来三角化,那么行坐标可以用极线约束补出来,我会在匹配章节详细说。如果图省事,也可以再投影一次水平条纹,额外求出v_proj,但采集时间会翻倍。实际项目中我通常只做竖直条纹,配合极线几何解行坐标,速度快很多。
2.3 投影仪标定常见的三个坑
第一个坑是棋盘格角点提取受投影图案干扰。投影仪把条纹投到棋盘格上,黑白格子边缘会出现明暗变化的干扰,角点坐标很容易偏。我的解决办法是:相机标定和投影仪标定分开采样,相机标定时不投任何图案,投影仪标定时先用全亮图和全暗图做一帧差分,尽量消除环境光和棋盘格本身的反射不均,再进行角点提取。
第二个坑是标定板位姿太少或者变化太单一。投影仪的“视觉”范围其实只有投影区域,如果标定板只在同一个平面附近移动,投影仪标定的外参很容易退化。我至少让标定板在深度方向变化20%以上、并且绕光轴旋转多个角度,这样投影仪的焦距和畸变才能被约束住。
第三个坑是投影仪gamma畸变影响相位提取。投影仪的亮度响应通常不是线性的,投射出去的理想正弦条纹变成“馒头形”波形,导致解出的相位带周期性误差,进而影响投影仪坐标提取精度。这个问题在解码章节会专门讲,但标定阶段就已经开始影响你了,建议标定前先做一次gamma校正。
3. 结构光图案设计与相位解码:格雷码+相移的组合拳
编码方案我选的是格雷码+多步相移,这是工业结构光里最经典也最皮实的组合。格雷码负责把整个投影画面切分成一个一个的周期区间,相移负责在区间内部给出精细位置,二者结合就能得到每个像素唯一的绝对相位。
3.1 相移法:亚像素级的相位测量
相移法的原理很简单:投影几幅相位依次平移的正弦条纹,相机采集后通过反正切运算求出每一点的相位。我用的是四步相移,投射的四幅条纹可以写成:
I1 = A + B cos(φ) I2 = A + B cos(φ + π/2) I3 = A + B cos(φ + π) I4 = A + B cos(φ + 3π/2)其中A是背景光强,B是调制强度,φ是待求相位。联立这四个方程,相位由下式解出:
φ = atan2(I4 - I2, I1 - I3)atan2的好处是能把相位范围扩展到(-π, π],比单用arccos或arcsin靠谱得多。你也可以用三步相移,少投一幅图,但抗噪能力稍差,我测试下来四步在普通环境下更稳。
3.2 格雷码:消除2π歧义
上面求出的φ是一个“包裹相位”,范围只有2π,同一个相位值可以对应投影画面里的第1个周期、第2个周期……并不知道是哪一个。这时候格雷码就派上用场了。
我的做法是按周期数选择格雷码位数。比如投影仪分辨率1920像素,如果设计每个正弦周期占32像素,那么整个画面有1920/32=60个周期,需要6位格雷码,因为2^6=64,正好覆盖60个区间。格雷码相比普通二进制码,相邻两个码字之间只有一位不同,解码时哪怕某个bit因为噪声判错了,带来的误差也往往只是一个周期而不是满屏错位,鲁棒性好很多。
格雷码图案的生成可以用这个思路:将每个像素的周期索引转换为格雷码,每一位格雷码决定该像素在对应图案中是亮还是暗。MATLAB里生成图案时,我习惯把周期数、像素宽度这些参数提前算好,方便后面统一管理。
3.3 从包裹相位到绝对相位的完整流程
拿到所有格雷码图之后,解码流程如下:
- 对每张格雷码图,先用全亮图IMax和全暗图IMin做归一化,消除物体表面反射率差异和环境光的影响:
Gnorm = (G - IMin) ./ (IMax - IMin)- 对归一化后的格雷码图取阈值0.5,得到一串二进制码,再把格雷码转换成十进制周期索引k。
- 将周期索引和包裹相位组合成绝对相位:
Φ = 2π * k + φ对垂直条纹来说,Φ就和投影仪的列坐标x_proj成正比,比例系数是每个周期的像素宽度。这一步是整个系统里承上启下的关键:后续不管是做双相机匹配,还是用投影仪参与三角化,靠的都是这个绝对相位。
3.4 投影仪gamma失真与相位误差补偿
我必须单独强调一下gamma问题。大多数投影仪的输出亮度与输入灰度之间是幂函数关系,假设gamma=2.2,你投出去的正弦条纹会变成类似“正弦的2.2次方”的尖顶波形,解出来的相位会有明显的二倍频误差,表现为重建点云表面出现规律性波纹。
处理办法有两个方向。一个是硬件层面做gamma校正:用相机分别拍摄一系列已知灰度值的图案,拟合出投影仪实际的光度响应曲线,然后查表反走。另一个是算法层面做误差补偿:如果相位误差是固定的周期性误差,可以预先测量误差模板,在解码后用误差查表修正。我在项目里是先做了gamma查找表,把相位误差从大约0.3rad压到了0.05rad以下,这个幅度对重建精度影响很大,千万别偷懒跳过。
4. 立体匹配与三角化:从绝对相位到三维点云
解码完成后,整个问题的核心从“图像处理”切换到了“几何计算”。怎么把每个像素的绝对相位变成三维坐标,下面分两种模式讲清楚。
4.1 基于相位一致性的双相机匹配
当使用真正的双相机时,两个相机各自独立解码,得到两幅绝对相位图ΦL和ΦR。因为两个相机看到的是同一片投射图案,同一个物理点在左右图中的绝对相位理论上应该完全相等,所以匹配就变成了“找相位相等的点”。
实际实现时,我先用stereoParams做极线校正,把左右图校正成行对齐的状态,这样匹配搜索从二维范围直接降到同一行的一维搜索,计算量小了一个量级。对左图每个像素,在右图同一行搜索绝对相位最接近的位置。由于相位是连续变化的,我可以在最小误差位置附近做抛物线插值,把匹配精度提到亚像素级别。
搜到匹配点对之后,如果用的是校正后的图,可以直接用视差公式算深度:
Z = f * B / d其中f是校正后的焦距(像素单位),B是基线长度,d是左右匹配点的列坐标差。如果用的是原始图像坐标,则直接用MATLAB的triangulate函数:
points3D = triangulate(matchedPoints1, matchedPoints2, stereoParams);这个方法的好处是遮挡区域里只要有一个相机能看到,就可能重建出来,比单相机+投影仪模式的空洞少很多。
4.2 单相机+投影仪模式下的匹配思路
如果你的系统是单相机+投影仪,匹配的路径会略有不同:对相机图像中的每个像素,通过绝对相位Φ求出对应的投影仪列坐标x_proj,这样每个相机像素就获得了一个“投影仪像素坐标”伙伴。此时可以把投影仪看成虚拟相机,用“相机像素 (u_cam, v_cam)”和“投影仪像素 (x_proj, v_proj)”组成点对做三角化。
问题是v_proj怎么来。投影仪只投射了竖直条纹,我们并不知道行坐标。我的做法是用极线约束求解:相机像素和投影仪像素之间满足对极几何关系,给定相机点坐标和基础矩阵F_cp,投影仪侧的匹配点必须落在一条极线上。已知x_proj,再与极线方程联立,就能解出唯一的v_proj。
这个方案比双相机省了一个相机的成本,但精度和遮挡鲁棒性都会差一些。我是先在笔记本上用这个方案验证了整体流程,再升级到双相机,所以也把它写在这里,方便预算有限的朋友参考。
4.3 三角化原理与MATLAB实现
三角化本身的数学不复杂。每个相机/投影仪都有一个3×4的投影矩阵P,它把三维齐次坐标X映射到二维像素坐标x:
λ * x = P * X展开后有两条线性方程。两个视角提供四条方程,正好可以解出X的三个自由度。我习惯手动写一个最小二乘求解函数,用SVD分解,代码短而且不依赖工具箱版本:
function X = triangulatePoint(P1, P2, pt1, pt2) A = [pt1(1) * P1(3,:) - P1(1,:); pt1(2) * P1(3,:) - P1(2,:); pt2(1) * P2(3,:) - P2(1,:); pt2(2) * P2(3,:) - P2(2,:)]; [~, ~, V] = svd(A); X = V(:, end); X = X / X(4); end其中pt1和pt2分别是两个视图的像素坐标,P1和P2是标定得到的投影矩阵。如果点对本身有噪声,SVD解会自然给出最小二乘意义上的最优三维点。
4.4 点云后处理:从密度到可用的完整曲面
三角化输出的原始点云通常很“脏”,我一般会做这几步处理:
先做离群点剔除。点云里会出现一些孤立飞点,典型来源是解码错误和遮挡边缘,我使用pcdenoise(统计滤波)把距离邻域均值过远的点去掉。
再做表面平滑。三角化点云自带亚像素噪声,直接用点云会看到表面像砂纸。我使用基于移动最小二乘或简单中值滤波的方法做平滑,但要控制滤波半径,否则会把几何棱角磨平。对于规则工业件,我还会用pcfitplane拟合平面做验证。
最后做孔洞填充和网格化。这一步按需处理,如果只是看密度可视化,直接pcshow就行;如果要导出CAD比较模型,我会用Delaunay三角化生成网格,配合alphaShape控制表面重建精度。
5. MATLAB工程实现细节与实测踩坑记录
最后这部分是真正的“手把手”环节,我把工程里最核心的代码骨架和实测中的教训总结出来。
5.1 投影图案生成脚本
图案生成是纯离线计算,我把参数集中管理:投影仪分辨率、周期像素数、格雷码位数、相移步数。核心代码如下:
% 投影图案生成 projW = 1920; projH = 1080; periodPix = 32; numPeriods = projW / periodPix; numGrayBits = ceil(log2(numPeriods)); [xx, yy] = meshgrid(1:projW, 1:projH); phase = 2 * pi * (xx - 1) / periodPix; % 四步相移 phaseShifts = [0, pi/2, pi, 3*pi/2]; for k = 1:4 img = 127.5 + 127.5 * cos(phase + phaseShifts(k)); imwrite(uint8(img), sprintf('phase_%d.bmp', k - 1)); end % 格雷码图案 grayCodeImgs = cell(numGrayBits, 1); for b = 1:numGrayBits period = 2^(numGrayBits - b); codeIdx = floor((xx - 1) / period); gray = bitxor(codeIdx, bitshift(codeIdx, -1)); bitVal = bitand(gray, 2^(b - 1)); img = 255 * (bitVal > 0); imwrite(uint8(img), sprintf('gray_%d.bmp', b - 1)); end注意格雷码的位顺序:我先生成最高位,再依次到最低位,这样解码重建周期索引时,顺序刚好对应二进制位权重从高到低,不容易搞乱。
5.2 解码主流程代码骨架
采集完图像后,我按下面的流程解出绝对相位:
% I0~I3: 四步相移图; G: 格雷码图序列; IMax, IMin: 全亮和全暗图 phi = atan2(I3 - I1, I0 - I2); % 包裹相位,注意相移顺序 ref = (IMax - IMin) / 255; % 归一化因子 ref(ref < eps) = eps; K = zeros(size(I0)); for b = 1:numGrayBits Gnorm = (double(G{b}) - IMin) ./ ref; K = K * 2 + (Gnorm > 0.5); % 二进制索引,格雷码顺序 end % 如果是格雷码,还需要把Gray码转回二进制 K = gray2bin(K, numGrayBits); % 绝对相位 Phi = K * 2 * pi + phi;需要提醒的是,如果归一化后Gnorm整体偏移严重,比如物体表面有强反光导致IMax饱和,相移图和格雷码图的曝光基准就不一致了,这时候要先做曝光调整或者多曝光合成,硬解出来的相位会有大面积错误。
5.3 实测中最容易翻车的五个问题
我把自己踩过的坑按危害排序如下:
| 问题 | 现象 | 我的处理方式 |
|---|---|---|
| 相机和投影仪没有同步 | 相位图出现横向撕裂条纹 | 加硬件触发,或至少用持续投影+快速连拍减少偏差 |
| 物体表面强反光 | 高光区域相位全错,点云凹陷 | 降低投影亮度、加偏振片、或用多曝光HDR |
| 黑色/吸光物体 | 格雷码信噪比低,解码跳码 | 增加投影亮度,但注意不要过曝;必要时涂显像剂 |
| gamma未校正 | 点云表面周期性波纹 | 投影仪gamma查表校正+相位误差补偿 |
| 标定板不平整 | 重投影误差高,点云翘曲 | 换成玻璃或铝基板,标定前用平面度检测 |
其中同步问题是最容易忽略的。前几次实验我用普通USB相机连续采集,投影仪图案一切换,相机刚好拍到切换瞬间,解码出来的相位乱七八糟。后来用了相机的外部触发信号,把投影仪图案切换和相机曝光锁在同一时钟上,才彻底解决。
5.4 精度验证:不能只看点云“像不像”
最后说下验证。很多新手看到点云成像漂亮就觉得系统没问题,但工业测量不是看颜值,得量化精度。我的验证方法是:放一个已知尺寸的陶瓷量块或金属平面,重建后用pcfitplane拟合平面,计算点云到拟合平面的RMS误差;再用游标卡尺量出物体台阶高度,和点云里对应两点距离对比。
对于我目前的系统,工作距离500mm、视场约200mm×150mm,平面拟合RMS误差大约在0.05mm到0.1mm之间,台阶高度误差在0.1mm左右。这个精度对结构光原型来说算正常水平。如果你的误差明显偏大,优先检查标定重投影误差和解码相位误差,这两个环节是精度的天花板。
从我个人的体验来说,整套项目里最花时间的不是算法推导,而是把光源、编码、标定、几何这些环节像齿轮一样咬合起来。MATLAB最大的优势是每个环节都有现成工具箱兜底,哪怕你不是计算机视觉科班出身,也能一步步把系统搭起来。但也不要低估工程细节的分量——同步、gamma、反光,每一个拿出来都是能让结果归零的“杀手”。如果你也在做类似的项目,希望这篇记录能帮你少走几段弯路,哪怕只是省下几个排查问题的通宵,也算值了。