在三维视觉相关的项目里,我最近遇到一个很典型的需求:一块表面没有明显纹理的白色塑料件,需要把它表面的凹陷、凸台和轮廓完整重建出来,供后续做几何比对。单纯靠普通双目相机拍摄,相当于拿到两张“光滑无特征”的图,立体匹配算法根本无从下手。最后我把方案改成了基于MATLAB的双目结构光三维重建实现,用投影仪往物体表面“打印”可识别的规律条纹,再用双目相机从两个角度观察条纹变形,配合标定参数做三角化,就能把毫米级甚至亚毫米级的表面细节恢复出来。这篇文章我就把整套流程、关键代码、参数计算和踩坑记录一次讲清楚,适合正在使用MATLAB做双目视觉、结构光测量或三维重建验证项目的开发者参考。
1. 项目到底要解决什么问题
1.1 被动双目的痛点:没有纹理就失配
普通双目视觉的思路是,靠左右相机拍摄的同一场景中自然存在的角点、边缘、颜色变化等特征来做匹配。听起来简单,但一旦遇到白墙、未喷漆的塑料壳、均匀金属表面,整幅图在算法眼里就像一张白纸,找不到可区分的特征点。我曾经拿一对普通工业相机扫一块乳白色PVC板,左右图的灰度分布几乎一样,特征匹配结果就是一团乱麻,估计出来的深度图全是噪声点。
问题的本质是:被动双目依赖的是“物体本身带有的信息”,物体表面没有亮度梯度时,左右视图就没有可对应的锚点。与其想办法增强算法对弱纹理的适应能力,不如直接改变测量条件,主动往物体表面投上可控的图案,把“没有特征”变成“到处都是特征”,这就是结构光的基本思路。
1.2 为什么最终选“双目+结构光”而不是单目结构光
结构光三维重建常见有两条路:一是单相机加投影仪,二是双相机加投影仪。单目结构光的做法是,把投影仪当成一台“逆向相机”来标定,投影仪投出的每一条条纹坐标和相机像素形成对应,再由三角关系求深度。这种方案结构简单、成本也低,但有一个很麻烦的环节:投影仪的光学系统、畸变、非线性响应都非常难标准,一个环节的误差会直接传递到最终点云。
我这次选择双目结构光,核心原因是结构光在这里只负责“提供纹理”,不负责“参与测距”。三维坐标完全由左右两台相机通过经典的双目三角化解算,只需要把相机标定做好。相比单目结构光,双目方案少了一部分投影仪标定的不确定性,深度精度更可控,而且左右相机同时还能拍下物体表面的纹理图,便于后续做纹理映射。
1.3 整体流程:四个阶段心里要有数
整套系统在MATLAB里的实现路径可以拆成四段:
- 双目标定:用棋盘格标定板标定左右相机的内参、畸变、外参,得到立体视觉参数。
- 条纹投影与图像采集:投影仪依次投出格雷码和相移条纹图案,两台相机同步拍下对应的左右图序列。
- 结构光解码:对左右图像分别解码,得到每个像素的绝对相位,相位相同的点就是左右图中的同名点。
- 三角化与点云生成:根据同名点的像素坐标和双目参数,反算三维坐标,得到点云。
后面的内容基本就按这条线展开。理解了这个流程,你在调参时脑子里始终有一张“链路图”:标定影响的是基础坐标系,投影解码影响的是匹配质量,三角化影响的是最终精度,不要一上来就纠缠某个滤波参数。
2. 硬件搭建与MATLAB工具链准备
2.1 设备选型与安装布局
这套方案对相机没有特别苛刻的指标要求,普通支持外部触发的工业相机都能跑。我实际用的是两台分辨率1280x1024的灰度相机,曝光时间在20到50毫秒范围,主要看物体表面反光程度。镜头焦距最好保持一致,否则视场覆盖范围不统一,会给极线校正增加额外麻烦。投影仪建议选择DLP类设备,因为它的灰度切换速度相对较快,而且可以接受外部触发信号逐帧切换图案。
安装布局上有两个细节需要提前考虑。
第一是基线长度。两台相机之间的距离决定了深度分辨率,基线越长,同样视差条件下深度变化越敏感,但物体左右遮挡也会越严重。我的经验是根据工作距离来定,工作距离在600毫米左右时,基线取100毫米比较合适,既能保证几十微米级别的视差敏感度,又不会让被测物体的大转角部分完全被一只相机遮住。
第二是投影仪的摆放。投影仪可以放在两只相机中间,也可以放在一侧,但不管怎么放,都要保证投影图案尽量覆盖双目公共视场的中心区域。如果投影仪放得太偏,会出现一边亮一边暗的情况,解码时相位噪声会明显上升。支架尽量固定,标定完了就别再挪动任何设备,否则外参失效,又得重新标。
2.2 投影与采集的同步问题
结构光解码最大的前提是“图案和图像一一对应”。如果相机拍到的明明是第5张条纹图,软件却认为它是第6张,后面所有相位计算都会错乱。
最理想的做法是给相机和投影仪都接入同一路硬件触发信号:投影仪每切换一帧图案,同时给左右相机发一个触发脉冲,保证两幅图像拍到的确实是同一个投影图案。没有硬件触发条件时,也可以用“投影一帧、采集一对图像”的软同步方式,也就是程序先向投影仪发送一张图,等几十毫秒画面稳定后再让左右相机各拍一张。这种办法对静止物体完全够用,但物体只要稍有移动,解码结果就会出现条纹级次的跳变,所以被测物一定要放稳。
同步问题还包含左右相机之间的同步。两台相机如果分别自由运行,哪怕只有几十毫秒的时间差,在物体静止时问题不大,但遇到轻微振动或物体表面温度变化引起的形变,就会出现左右视差错误。建议从一开始就把左右相机的曝光、增益等参数设置成完全一致,并且统一用同一个外部触发源。
2.3 MATLAB用到的工具箱和标定App
MATLAB实现这套流程主要涉及以下几个模块:
- Image Processing Toolbox:做图像读取、灰度变换、滤波、二值化。
- Computer Vision Toolbox:双目标定、极线校正、三角化、点云显示。
- Camera Calibrator App:单目相机标定。
- Stereo Camera Calibrator App:双目相机立体标定。
如果相机通过GigE或USB3 Vision接口采集,还需要Image Acquisition Toolbox或对应的硬件支持包。不过很多验证项目可以先把图片序列存成文件夹,再统一导入MATLAB处理,这样能把硬件问题暂时隔离出去,先跑通算法流程。
安装工具包的版本尽量保持统一,不同版本之间标定接口有差异。比如现在已经推荐使用estimateCameraParameters替代老的标定接口,但很多教程还停留在老写法。我的建议是直接以当前版本Help文档为准,代码调用方式按新版来写,省得后面换机器又要改。
3. 标定过程:决定精度的最基础一步
3.1 相机内参标定:别在标定板上省钱
相机内参标定解决的是“像素坐标到底对应相机坐标系里的哪个方向”的问题。MATLAB的Camera Calibrator App用棋盘格标定板就能完成,但标定板的质量直接决定了后续所有环节的精度下限。
我踩过一次比较大的坑是,用普通喷墨打印机把棋盘格图案打到一张A4纸上,再贴在纸箱板上。拍出来的照片在App里能检测出角点,重投影误差却一直在0.7像素左右,怎么调都不理想。后来换成在平整铝板上印刷的陶瓷棋盘格,同样拍一轮,重投影误差立刻降到0.12像素左右。原因是普通纸张本身有微小起伏,贴在纸板上更是会拱起变形,标定板不是平面,标定算法自然拟合出一个扭曲的相机模型。
拍摄标定图片时要注意几点:左右相机各拍15到20张,标定板要出现在视场的不同位置和角度,不要全放在画面正中间同一个姿势;要有前倾、后仰、左右旋转,让算法能充分解算畸变参数。另外,标定板的角点数量要足够多,比如12x9的内角点,这样每一张图能提供足够的约束。
标定完成后,MATLAB会给出每张图的平均重投影误差,以像素为单位。误差在0.2像素以内属于良好,0.2到0.5像素尚可接受,超过0.5像素就需要重新拍摄标定图片。
3.2 双目标定与极线校正
左右相机各自的单目参数标好之后,用Stereo Camera Calibrator App把左右标定图同时导入,App会算出两台相机之间的旋转矩阵R和平移向量T,还可以一键得到校正后的立体参数对象stereoParams。
极线校正的意义在于,校正完成后左右图像同一行的同名点一定在同一行上,立体匹配从二维搜索退化为一维搜索。MATLAB里调用rectifyStereoImages或直接用stereoParams自带的重投影矩阵就能完成。
双目标定看起来只是把两个单目标定结果拼一起,实际有个很容易忽略的坑:左右相机拍摄标定板的时间必须尽量一致。如果左相机拍完第1张,隔了几秒右相机才拍第1张,期间标定板位置哪怕出现零点几毫米的移动,外参计算就会偏离真实值。所以拍摄时最好用两台相机同时抓拍,或者素材是从同步录制的视频里截取出来的。
3.3 投影仪要不要标?这里有个省事选择
很多教程会把投影仪标定讲得很复杂,但在我这套“双目结构光”方案里,投影仪其实不需要完整标定。原因前面说过:我们的三维坐标由双目三角化求得,投影仪的角色只是给物体表面“画上图案”,图案本身不必对应某个精确的世界坐标。真正需要严格标定的,只有两台相机。
如果你未来要把系统简化为“单目+投影仪”,那投影仪就必须按照逆向相机模型标定,需要投出格雷码条纹来确定投影仪像素和标定板角点的对应关系,流程会多出来一大截,而且投影仪畸变很难标准。就我这次项目而言,选择双目方案从工程角度看不仅省事,精度上限也更高。
3.4 标定完成后如何检查
标定不是看App显示“成功”就结束了,还要做两个实际检查。
第一个检查是看校正后的图像。对一张标定板图片做极线校正后,棋盘格的横线应该在左右图像里严格水平对齐,如果出现明显旋转或梯形变形,说明外参有问题。
第二个检查是看深度图是否平滑。标定完成后拍一块平面平板,重建出来的点云如果出现整体弯曲或波浪状,优先怀疑标定板本身不平,或者标定图片数量不足。
4. 结构光图案设计、解码与三维重建实现
4.1 格雷码+相移的图案设计
结构光编码方式有很多种,我这次用的是格雷码+四步相移的组合。格雷码负责确定条纹的级次序号,解决“这个相位在第几个条纹周期内”的绝对定位问题,相移则负责在一个周期内求出亚像素级的连续相位值。两者结合,既能覆盖大范围,又能做到像素级以下的细分。
投影图案按这样设计:若投影仪横向分辨率为1024像素,我预先设定64个条纹周期,也就是2的6次方,所以需要6张格雷码图案。每个周期约占16个像素,四步相移图案在周期内产生相位变化,最终相位图的横向分辨率就远高于原始图像分辨率。
格雷码图案生成时,要先把条纹序号转换为二进制码,再转换成格雷码,按位生成黑白图。MATLAB里可以用异或操作bitxor(n, bitshift(n, -1))快速得到标准格雷码值。用bitget取对应位,就能生成每一幅投影图。这一步完全可以在代码里循环完成,不需要手工准备图片。
四步相移采用标准公式采集四幅图:
- I1 = A + B·cos(φ)
- I2 = A + B·cos(φ + π/2)
- I3 = A + B·cos(φ + π)
- I4 = A + B·cos(φ + 3π/2)
相位求解用反正切函数:
φ = atan2(I4 - I2, I1 - I3)
为什么用四步而不是三步?四步法能同时通过(I1 - I3)和(I4 - I2)抵消部分背景光噪声,对投影仪非线性也有一定的抑制作用。实测下来四步相位的重建表面比三步版本平滑不少,代价只是多拍一张图,对静止物体来说完全值得。
4.2 采集与相位解包裹
采集流程是这样的:先投影6张格雷码图案,再投影4张相移图案,另外加1帧全亮和1帧全暗用于计算二值化阈值,总共12张图。左右相机对这12张图同步拍摄,得到12对图像。
拿到图像后第一步是二值化格雷码图。简单用固定阈值容易出错,因为物体表面的反光不均匀,阴影区域可能整个低于阈值。更稳的办法是全亮图和全暗图分别求平均亮度,取两者中值作为二值化阈值,这样能自适应不同位置的亮度变化。
格雷码解码成二进制码再转十进制,得到每个像素的条纹级次k。核心代码逻辑:
% grayBits是解码后的格雷码逐位0/1数组,从最高位开始 binBits = zeros(1, Nbits); for i = 1:Nbits if i == 1 binBits(i) = grayBits(i); else binBits(i) = xor(binBits(i-1), grayBits(i)); end end % 转十进制条纹序号 k = sum(binBits .* 2.^(Nbits-1:-1:0));然后结合四步相移得到的包裹相位phase_wrap,得到绝对相位:
absPhase = phase_wrap + 2 * pi * k;这一步最怕出现条纹级次跳变。跳变通常出现在格雷码图像在黑白边界被噪声污染的位置,解决方法是先对二值化结果做中值滤波,或者对二值化后的边缘做形态学处理。
4.3 双目匹配、视差与三角化
左右相机分别计算出绝对相位图以后,匹配就变得非常直观:极线校正后同一行上绝对相位相等的点,就是同一个物理点在左右图像中的投影。
具体实现时我对校正后的左图逐像素处理,在右图同一行上搜索与左图绝对相位差值最小的位置,考虑到视差范围往往有限,搜索窗口可以限制在一个合理区间,例如20到200像素之间。匹配质量指标用左右相位差判断,差超过0.1弧度就标记为无效点。
得到视差图d之后,可以继续用三角化公式反算深度。理想双目模型下深度Z和视差的关系是:
Z = f·b / d
其中f是等效焦距,b是基线长度。做一个简单估算:假设像素焦距约2000像素、基线100毫米、工作距离600毫米,亚像素视差精度0.1像素,则深度分辨率约为0.18毫米。这个数量级完全能满足常规表面几何比对的测量需求。
更严谨的做法是直接用MATLAB的triangulate函数,输入左右关键点坐标和stereoParams参数,函数内部会考虑畸变校正、极线几何和三角化误差,直接输出三维点坐标。实际操作时,把左图像素坐标和匹配到的右图像素坐标分别构造成Mx2矩阵,一行调用就能得到点云:
points3D = triangulate(leftPts, rightPts, stereoParams);注意leftPts和rightPts必须是一一对应的同名点,顺序不能乱。我之前因为匹配循环里漏了一个列坐标交换,导致点云整体斜向拉伸,排查了很久。
4.4 点云后处理与可量化评估
三角化出来的三维点还带着不少噪声,尤其是条纹边界、物体边缘、遮挡区域附近,这些点在点云里表现为飞散的孤立点。
我常用的后处理步骤是:
- 根据相位匹配置信度过滤无效点;
- 用统计离群点剔除算法去掉偏离邻域均值过远的点;
- 对剩余点做小幅高斯平滑,减少高频噪声;
- 导出成PLY或PCD格式,在MeshLab或MATLAB的
pcshow里查看。
后处理之后可以放一个已知高度的台阶或者标准量块来验证重建精度。用MATLAB拟合台阶平面,测量两个平面之间的间距,与量块标准高度比对。我实测的这套配置,量块高度误差能控制在0.2毫米以内,对于100毫米量级的面形测量已经够用。
5. 常见问题与避坑速查
5.1 标定环节的高频坑
标定是整套系统中最容易“积攒坑”的地方,而且很多问题要到重建阶段才爆发。重点排查这么几条:
标定板必须平整、高精度。普通打印纸加纸板很难保证平面度,建议直接买陶瓷或玻璃基材的标定板。标定照片数量宁多勿少,单目最少15张,双目最好20对以上。拍摄时不要只把标定板放在正前方,左右边缘、上下俯仰都要覆盖,否则畸变参数解算不充分。另一个高频问题是没有关闭相机的自动曝光和自动白平衡。灰度相机一般还好,彩色相机的自动白平衡会在不同画面中改变RGB通道响应,导致棋盘格角点检测产生偏差,标定前务必把相机固定在手动模式。
5.2 解码与匹配环节的高频坑
解码环节出现的“条纹级次跳变”有很多种表现:点云表面出现整条沟壑、面片错位、周期性条纹状噪点等。常见原因包括:
- 环境光太强,导致格雷码黑白图案对比度不足;
- 物体表面高光区域,投影白条纹时过曝,投影黑条纹时依然泛白;
- 物体表面颜色过深,比如黑色橡胶件,反射率太低;
- 投影仪的非线性Gamma响应,会让正弦条纹失真,产生周期性相位误差;
- 图像拍摄时运动模糊,比如物体微振动或投影切换太快。
高光反射问题尤其棘手。浅色高光物体在条纹图里容易出现局部全白,解码相位直接丢失。我用过的有效办法是降低投影光强的同时提高相机曝光,尽量保持在响应线性区内,但高光边缘处仍然会有饱和区域,处理时把这些饱和像素直接标记为无效点,不做插值硬算。
5.3 综合排查速查表
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 点云整体弯曲 | 标定板不平或标定图片太少 | 更换高精度标定板,重新补拍20组以上标定图 |
| 点云表面出现周期性条纹噪声 | 投影仪Gamma非线性 | 做Gamma校正,或采用互补条纹平均消除谐波 |
| 物体边缘点云毛刺多 | 相位解码边界跳变 | 对二值化结果做中值滤波,对相位图设置置信度阈值 |
| 大面积空洞 | 表面高光或吸光 | 降低投影亮度加长曝光,或将高光区域标记为无效 |
| 左右匹配错位,点云分层 | 左右相机拍摄时间不同步 | 强制硬件同步触发,或暂用逐帧软同步并对静止物拍摄 |
| 重建尺寸整体偏差 | 标定单位或基线换算有误 | 确认标定板方格尺寸单位一致,检查基线单位是否为毫米 |
排查时要记住,越是在靠后的处理环节找原因,越容易绕远路。出现大面积异常时,先回到标定数据,再看解码中间结果图,最后才检查点云滤波参数。中间的相位图如果有明显问题,后面再怎么后处理都是徒劳。
6. 实测效果与我的建议
6.1 一组实测数据的直观印象
用标称间距50毫米的标准量块做验证,重建出来的台阶间距是49.87毫米,重复测量标准差在0.2毫米以内。对一块白色塑料平面重建,经过后处理的点云平面度RMS大约在0.1到0.15毫米之间。这个精度对绝大多数外观几何比对、逆向工程建模、无损尺寸测量场景都够用。
整体耗时方面,12张投影图案、每张曝光40毫秒,加上切换稳定时间,一轮采集大约0.8秒,MATLAB里解码加匹配约3秒。如果只做离线验证,这个速度不是瓶颈;要做在线测量,后续可以用并行计算甚至C++重写核心解码部分。
6.2 哪些地方还能继续优化
如果让我重新做一次这套系统,我会优先改进两个环节。
一是把四步相移升级成多频外差方案。格雷码+相移虽然实现简单,但对物体表面亮度跳变敏感,多频外差通过几个不同频率的相位叠加得到绝对相位,对高光和阴影区域的容忍度更好,适合处理表面反射率变化剧烈的工件。
二是尝试蓝光或单色性更好的光源加窄带滤光片。环境光对结构光的干扰非常大,在展会上见过用窄带滤光片把大部分环境光挡在相机外面之后,重建质量提升几乎是立竿见影的。普通办公照明下,环境光分量可能占到图像亮度的一半以上,格雷码二值化阈值稍偏就会引入噪声。
最后分享一个个人经验:标定环节最值得多花时间。我一开始图省事,只拍了8对标定图,重投影误差一直在0.7像素左右,重建的平板怎么看都像波浪。后来耐着性子补拍了不同角度和倾斜度的十几组图片,重投影误差降到0.12像素,同样的解码代码,点云立刻平了。真遇到重建问题,先从标定和同步这两个源头排查,比上来调相位滤波算法要有效得多。