多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

用MATLAB实现双目结构光三维重建:从标定到点云全流程

用MATLAB实现双目结构光三维重建:从标定到点云全流程 用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/3260个周期需要6位格雷码因为2^664正好覆盖60个区间。格雷码相比普通二进制码相邻两个码字之间只有一位不同解码时哪怕某个bit因为噪声判错了带来的误差也往往只是一个周期而不是满屏错位鲁棒性好很多。格雷码图案的生成可以用这个思路将每个像素的周期索引转换为格雷码每一位格雷码决定该像素在对应图案中是亮还是暗。MATLAB里生成图案时我习惯把周期数、像素宽度这些参数提前算好方便后面统一管理。3.3 从包裹相位到绝对相位的完整流程拿到所有格雷码图之后解码流程如下对每张格雷码图先用全亮图IMax和全暗图IMin做归一化消除物体表面反射率差异和环境光的影响Gnorm (G - IMin) ./ (IMax - IMin)对归一化后的格雷码图取阈值0.5得到一串二进制码再把格雷码转换成十进制周期索引k。将周期索引和包裹相位组合成绝对相位Φ 2π * k φ对垂直条纹来说Φ就和投影仪的列坐标x_proj成正比比例系数是每个周期的像素宽度。这一步是整个系统里承上启下的关键后续不管是做双相机匹配还是用投影仪参与三角化靠的都是这个绝对相位。3.4 投影仪gamma失真与相位误差补偿我必须单独强调一下gamma问题。大多数投影仪的输出亮度与输入灰度之间是幂函数关系假设gamma2.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、反光每一个拿出来都是能让结果归零的“杀手”。如果你也在做类似的项目希望这篇记录能帮你少走几段弯路哪怕只是省下几个排查问题的通宵也算值了。
返回列表