简介:这是一套面向计算机视觉方向学习者的完整工程资源,聚焦相机标定、投影仪标定与三维重建全流程,适合高校学生作为毕设、课程设计或项目初期参考,也适合有Python基础的入门者进阶实践。代码经作者测试通过,在答辩中获得高分,具备较好的完整性和可运行性。压缩包共464个文件,约24.38MB,核心为326个Python脚本,覆盖标定计算、相位解算与重建逻辑;另有65个BMP和31个PNG图像样本(如正弦/余弦光栅图像)用于标定和相位分析,还包含可执行文件、配置文件、模型权重及说明文档,方便直接复现和调试。配套资源尤其注重实操:提供虚拟环境激活脚本和启动批处理,降低环境配置门槛;同时保留UI界面文件、XML配置和PTH模型权重,便于从界面操作到底层算法层层拆解学习。目前已有160人学习下载,适合想通过真实项目掌握结构光三维重建技术的读者反复研究。
1. 结构光三维重建的入口:为什么要同时标定相机和投影仪
做单目测量时,标定相机只需要解决内参和畸变,但一旦引入投影仪搭建结构光三维重建系统,问题就变成“多了一个反向成像单元”。投影仪本质上是一个逆相机:它把编码图案投射出去,相机再拍回来,两边的成像几何必须同时建模,否则三角测量的空间直线根本交不到正确位置。这个项目把相机标定、投影仪标定和三维重建串成了完整链路,其中sin_wave、cos_wave相移图和wave_grid0网格图,是给投影仪构造“可解码特征”的关键素材。
对于正在做结构光实验、双目重建,或想把OpenCV标定流程迁移到投影系统的开发者和研究生,这套方案的参考价值在于:它没有停留在只标定相机的阶段,而是用相位编码解决了投影仪无法直接拍摄的问题。下文按“相位编码 → 相位展开 → 投影仪标定 → 三维重建 → 误差验证”推进,把资源里每个文件的作用和参数含义拆开讲。
2. 相位条纹编码与投影基元生成
2.1 为什么投影仪标定不能只靠棋盘格
标定相机时,棋盘格角点是最直观的特征,这些角点的图像坐标可以用OpenCV直接提取。但投影仪没有“拍摄”能力,它只能向外投射图案,那么投影仪图像坐标中角点在哪里?答案是:用相位条纹为投影仪图像平面上的每个像素建立一个连续的编码坐标。
投射出去的sin_wave和cos_wave条纹,在相机采集到的图像中表现为亮度随像素位置呈正弦变化的图案。通过多步相移算法,每个相机像素都能解出一个对应投影仪像素坐标的相位值。相位与投影仪像素坐标之间的映射一旦建立,投影仪就获得了与相机对等的特征点观测能力。
项目中出现的四个sin_wave文件,其命名中的数值1.5707963267948966、3.141592653589793、4.71238898038469分别对应π/2、π、3π/2,加上sin_wave0.bmp构成一个标准的四步相移序列。相比三步相移,四步相移在解算时可以直接通过差分消除背景光强项,对传感器噪声和曝光不均匀的敏感度更低。
2.2 四步相移的相位解算实现
四步相移的采集光强模型为:
- I0 = A + B·cos(φ)
- I1 = A + B·cos(φ + π/2)
- I2 = A + B·cos(φ + π)
- I3 = A + B·cos(φ + 3π/2)
其中A是背景光强,B是条纹调制强度,φ就是待求的相位。联立四个方程后,相位φ的闭式解为:φ = arctan((I1 - I3) / (I0 - I2))。该式通过两对反向相位的差分,把A直接抵消掉,这是四步相移在实际工程中被广泛使用的原因。
import cv2 import numpy as np base = "sin_wave" phases = [0.0, 1.5707963267948966, 3.141592653589793, 4.71238898038469] imgs = [] for p in phases: img = cv2.imread(f"{base}{p}.bmp", cv2.IMREAD_GRAYSCALE) imgs.append(img.astype(np.float32)) I0, I1, I2, I3 = imgs denominator = I0 - I2 numerator = I1 - I3 # 处理分母接近0的像素,避免除零异常 denominator[abs(denominator) < 1e-6] = 1e-6 wrapped_phase = np.arctan2(numerator, denominator)这段代码的核心是np.arctan2而非np.arctan,原因是四步相移解算出来的相位可能落在任意象限,arctan2能根据分子分母的符号自动判断象限,输出范围是(-π, π]。denominator中接近0的像素点通常在相位极值附近,若不修正会出现除零异常或产生异常大值。这批像素在后续步骤中需要格外关注,它们往往是相位展开跳变的高发区。
2.3 wave_grid0.bmp在标定流程里的作用
wave_grid0.bmp从命名上看是波网格图,它在整个标定序列中的角色是提供“粗定位”参照。正弦条纹是连续变化的灰度图案,算法无法从中直接找到标定板的边界角点。先投射一张高对比网格图,用形态学或角点检测锁定标定板的候选区域,再把这个区域作为ROI去解算条纹相位,能显著减少背景噪声对相位图的影响。
实际项目中我一般会在投射条纹序列前先投一帧wave_grid0.bmp,检测网格交点的粗略位置,用它生成一个掩膜。掩膜之外的像素在后续相位解算中直接置零,这样既降低了计算量,也避免了标定板背景区域的反光干扰。需要留意的是,wave_grid0.bmp的网格间距应当与投影仪分辨率形成整数倍关系,否则网格交点与条纹相位之间的对应会产生亚像素错位。
3. 相位展开与投影仪像素映射建立
3.1 包裹相位的周期歧义
上一章解出的wrapped_phase被限制在(-π, π]区间,这叫包裹相位。真实物体表面连续变化时,实际相位可能跨越多个2π周期,于是相邻像素之间就出现了“跳变”走廊。如果不做相位展开,相机图像上两个相邻像素可能被映射到投影仪图像上相差一个完整周期的坐标,同名点查找会直接错乱。
展开相位最直接的方法是沿x方向做积分累加,遇到从π跳到-π的位置就加一个2π。但这种方法对噪声极其敏感,一个错误跳变会向后续所有像素传播。工程上更可靠的做法是借助多个频率的条纹图做双频展开,也就是这个项目里cos_wave_Period_64_initial_phase_-2.0943951023931953.bmp和cos_wave_Period_59_initial_phase_0.bmp存在的原因。
3.2 双频相位展开的最小公倍数原理
周期为T1=64像素的高频条纹,和周期为T2=59像素的低频条纹,它们的拍频周期为LCM(64,59)=3776像素。在这个范围内,两个频率的包裹相位组合是唯一的,因此可以先由低频相位确定高频条纹处于第几个周期,再把高频包裹相位展开成绝对相位。
def unwrap_two_frequency(phi_high, phi_low, T_high=64, T_low=59): """ 双频相位展开 phi_high: 高频包裹相位,范围(-pi, pi] phi_low: 低频包裹相位 返回绝对相位,单位是像素 """ # 归一化到[0,1)区间,便于周期计数 high_norm = (phi_high + np.pi) / (2 * np.pi) low_norm = (phi_low + np.pi) / (2 * np.pi) # 用高频归一化相位估算低频条纹的周期序号 k_low = np.round(high_norm * T_low / T_high - low_norm) # 恢复低频绝对相位(以周期数表示) phase_low_abs = low_norm + k_low # 再用低频绝对相位精确计算高频周期序号 k_high = np.round(phase_low_abs * T_high / T_low - high_norm) # 最终绝对相位换算为像素坐标 abs_phase_px = (high_norm + k_high) * T_high return abs_phase_px这里两次np.round是关键操作。第一次round先做个粗略估计,第二次round用低频绝对相位做精确修正。高频条纹的周期越小,相位测量噪声对round结果的影响就越大,一旦在边界处判断错了一个整周期,展开后的相位会整体平移T_high个像素。实际使用时,建议先对两张包裹相位图各做一次5×5中值滤波,消除孤立像素的椒盐噪声再进入展开函数。
3.3 从绝对相位到投影仪像素坐标的映射
展开得到的绝对相位,本质上是以像素数为单位的物理量。假设条纹沿投影仪水平方向变化,那么投影仪x坐标与绝对相位的关系为:
x_proj = abs_phase_px
如果绝对相位是从垂直条纹解出来的,那么y_proj = abs_phase_px。这里要注意,绝对相位的数值单位已经是像素,不需要再乘任何比例系数。实际操作中,由于投影仪的DMD分辨率和投射图像分辨率完全一致,相位与像素坐标是一一对应的关系。
生成条纹图时用的initial_phase参数会影响低频条纹的绝对相位偏移。cos_wave_Period_64_initial_phase_-2.0943951023931953中的-2.0943951023931953是-2π/3的弧度值,这个初始相位存在的意义是让两种频率条纹的零点错开,避免在相位展开时出现整周期模糊的临界点。
4. 投影仪标定落地:把投影仪当作逆相机做张正友标定
4.1 数据采集的完整操作流程
投影仪标定不能直接对投影仪“拍照”,而是要借助相机间接获得投影仪图像平面上的特征点坐标。整个采集流程需要标定板、投影仪和相机三者固定在一个稳固的支架上。具体步骤如下:
- 将棋盘格或网格标定板放置在相机和投影仪的共同视野内,固定位置。
- 关闭投影仪,相机拍摄一帧标定板图像,用于检测角点的相机图像坐标。
- 投射wave_grid0.bmp,再次拍摄,粗定位标定板区域。
- 依次投射四张sin_wave相移图和两张cos_wave低频率条纹图,同步采集图像。
- 对各组条纹图做相位解算和双频展开,获得每个相机角点对应的投影仪像素坐标。
- 改变标定板的姿态(旋转、平移、倾斜),重复步骤2到6,采集12到20组数据。
每个姿态下,相机图像坐标的角点由OpenCV的角点检测获得,投影仪图像坐标则由相位映射获得。一组姿态产生一组二维点对,多组姿态就能覆盖投影仪视场的不同区域。
4.2 张正友标定的代码与参数选择
采集完成后,将相机角点坐标和投影仪角点坐标分别整理成数组,就可以调用标定函数。投影仪标定的数学过程与相机标定完全一致,都是求解内参、畸变系数和外参的最小化问题。
import cv2 import numpy as np # object_pts: 世界坐标系下标定板角点,形状为(N, 1, 3) # proj_pts: 投影仪图像坐标,形状为(N, 1, 2) # proj_size: 投影仪分辨率,例如(1280, 720) ret, K_proj, dist_proj, rvecs, tvecs = cv2.calibrateCamera( object_pts, proj_pts, proj_size, None, None, flags=cv2.CALIB_RATIONAL_MODEL ) print(f"重投影误差: {ret:.4f}") print(f"内参矩阵:\n{K_proj}") print(f"畸变系数: {dist_proj.ravel()}")object_pts中每个角点的三维坐标是按照标定板网格间距生成的,比如方格边长20mm,就以毫米为单位填坐标。proj_pts必须是float32类型,且维度要与OpenCV要求的格式严格一致。CALIB_RATIONAL_MODEL标志启用带6阶径向畸变系数的有理模型,投影仪镜头的畸变通常比工业相机更明显,建议开启这个选项。
4.3 标定结果质量判读
标定完成后,直接看ret值是最快的检验方式。单个相机标定的重投影误差通常小于0.3像素,投影仪标定由于有相位解算这一中间环节,误差会在0.3到0.8像素之间。
| 参数 | 正常范围 | 异常信号 |
|---|---|---|
| fx, fy | 和投影仪分辨率同量级,如800~2500 | 小于500或大于4000说明角点坐标输入有误 |
| 主点 cx, cy | 接近投影仪分辨率中心 | 偏差超过10%则检查ROI是否被裁切 |
| k1, k2 | -1.0到1.0之间 | 超出此范围说明标定图像数量不足或相位展开有跳变 |
| 重投影误差 | 小于0.8像素 | 大于1像素优先查相位展开和角点检测 |
这里有个工程经验:如果标定出的畸变系数绝对值异常大,不要急着怀疑镜头,先回头检查第3章的双频展开是否正确,相位跳变点被当成有效特征点送入标定后,畸变模型会反过来吸收这些错误,表现就是k1、k2异常。
4.4 相机-投影仪联合外参标定
投影仪标定完成后,还要用双目标定得到相机和投影仪之间的相对位姿。假设已经分别标出相机内参K_cam和投影仪内参K_proj,可以用cv2.stereoCalibrate联合优化外参:
ret_stereo, K_cam, D_cam, K_proj, D_proj, R, T = cv2.stereoCalibrate( cam_object_pts, # 世界坐标 cam_img_pts, # 相机图像坐标 proj_img_pts, # 投影仪图像坐标 K_cam_init, D_cam_init, K_proj_init, D_proj_init, proj_size, flags=cv2.CALIB_FIX_INTRINSIC )联合标定中我一般固定已经标好的内参,只优化外参R和T,这样能减少参数耦合。R是相机坐标系到投影仪坐标系的旋转矩阵,T是平移向量。这个R和T就是结构光三维重建中三角测量所需的相对几何关系。
5. 三维重建中的三角测量与极线约束
5.1 重建策略的选型
标定结束后,整个系统的几何模型已完整建立。三维重建的思路是把相机和投影仪看作一个双目系统:相机是真正的成像设备,投影仪是一个“反向相机”。两者共享同一个世界坐标系,通过R和T关联。
重建方案有两种。方案一是极线校正后做立体匹配,把两幅图像校正到行对齐,然后查视差求深度。这个方案计算量小,但前提是投影仪图像上的相位映射误差足够小,否则校正后的边缘区域畸变较大。方案二是直接用三角测量函数,把相机像素坐标和由绝对相位恢复的投影仪像素坐标作为对应点,求出两条光线的交点。
我倾向于方案二。原因在于结构光系统的相位映射已经将对应关系精确到亚像素级别,不需要再做代价计算和匹配搜索,直接用triangulatePoints可以获得更高的重建精度。
5.2 三角测量代码详解
三角测量的输入是两组成像点的归一化无畸变坐标和内外参构成的投影矩阵。投影矩阵P的形式是K[R|t],相机坐标系作为世界坐标系时,相机的R为单位阵、t为零向量。
def reconstruct_point(cam_pt, proj_pt, K_cam, D_cam, K_proj, D_proj, R, T): # cam_pt: 相机像素坐标 (x, y) # proj_pt: 投影仪像素坐标 (x, y) # 将像素坐标转换为无畸变归一化坐标 p_cam = cv2.undistortPoints( np.array([[[cam_pt[0], cam_pt[1]]]], dtype=np.float64), K_cam, D_cam ) p_proj = cv2.undistortPoints( np.array([[[proj_pt[0], proj_pt[1]]]], dtype=np.float64), K_proj, D_proj ) # 相机作为参考系,投影仪相对相机的外参为R, T P_cam = np.hstack((K_cam, np.zeros((3, 1)))) P_proj = K_proj @ np.hstack((R, T)) # 三角化,得到齐次坐标 pts4d = cv2.triangulatePoints( P_cam, P_proj, p_cam.reshape(1, -1, 2), p_proj.reshape(1, -1, 2) ) # 除以齐次分量,得到非齐次三维坐标 pts3d = pts4d[:3] / pts4d[3] return pts3d.ravel()undistortPoints默认输出归一化平面坐标,正好符合triangulatePoints的输入要求。P_cam中相机外参是单位矩阵,意味着重建输出的坐标位于相机坐标系。pts4d[3]是齐次坐标的缩放项,除以它才能把坐标还原到欧氏空间。
5.3 重建精度的验证方法
重建完成后,如何判断这套系统的精度足够支撑“三维重建”这个目标?可以从三个层面验证。
第一层是平面拟合。将一块已知平面度的平板(如大理石平台)放入视野,重建表面的点云,用最小二乘拟合平面,统计点到平面的残差标准差。结构光系统的平面残差应小于0.1mm,如果超过这个值,说明相位展开或标定数据存在系统性问题。
第二层是长度比对。在样件上标记两个特征点,重建它们的空间坐标并求距离,与千分尺测量值对比。这一步能验证尺度因子的正确性,通常会暴露T向量缩放不一致的问题。
第三层是台阶高度验证。重建一个已知高度的量块,测量台阶两侧点云的Z方向差值。这个测试专门检测Z轴方向的系统性偏差,如果误差随着高度增大而增大,基本可以确定是投影仪畸变未得到充分校正。
6. 相位噪声的定位与标定参数边界
6.1 条纹周期与分辨率之间的约束关系
高频条纹的周期并不是越小越好。当条纹宽度小于相机单个像素在物体表面的投影尺寸时,相机传感器无法准确采样完整的正弦波形,相位解算的误差会急剧增大。
| 投影仪分辨率 | 最小可靠周期(像素) | 推荐周期(像素) | 每个姿态采集次数 |
|---|---|---|---|
| 1280×720 | 5 | 8~16 | 12~16 |
| 1920×1080 | 6 | 16~32 | 12~16 |
| 3840×2160 | 8 | 32~64 | 16~20 |
如果被重建物体表面有大面积高光或深色吸收材料,条纹调制强度B会偏小,此时相位解算的置信度很低。可以在解出相位的同时计算调制强度B,将B过低的像素直接置为无效点。
# 调制强度B的计算,用于像素置信度过滤 B = np.sqrt(numerator**2 + denominator**2) / 2.0 confidence_mask = B > (B.mean() * 0.3) wrapped_phase[~confidence_mask] = 0.0这里用0.3倍均值作为阈值是我经验中的常用值,具体项目需要根据曝光情况调整。过高的阈值会丢失边缘有效像素,过低的阈值无法滤除阴影区域的噪声,建议在标定阶段多次测试确定。
6.2 相位展开失败的快速排查路径
相位展开失败是结构光项目中最隐蔽的坑之一。失败后的典型症状是重建点云上出现沟壑状的“台阶”,沿条纹方向呈条状分布。排查路径可以按下面三步走:
第一,打印展开后的绝对相位灰度图,观察邻域内是否有明显的跳变线。跳变线位置出现在物体边缘,通常是阴影遮挡导致信噪比过低,需要调整光照或增加投影亮度。跳变线随机分布在平坦区域,通常是双频展开中np.round边界判断出错,需要检查两个频率的周期比是否合适。
第二,检查低频条纹的initial_phase选择。cos_wave_Period_64_initial_phase_-2.0943951023931953中的-2π/3初始相位,如果改成0可能会导致高频和低频条纹的零点重合,让展开函数在相位接近0的区域失去区分度。
第三,验证相机采集到的条纹是否过曝。将投影亮度调低,保证条纹波峰值低于相机灰度上限的85%,否则波形顶部被削平,相位解算会出现系统性偏差。
6.3 反投影验证的闭环检查
最后一个技巧是反投影验证。将重建得到的二维点云重新投影回相机图像坐标系,计算与原始特征点的像素距离,这个偏差能直接反映标定和重建整个链路的闭环质量。
def backprojection_check(points3d, K_cam, D_cam, R_cam, t_cam): proj_2d, _ = cv2.projectPoints( points3d, R_cam, t_cam, K_cam, D_cam ) return proj_2d.reshape(-1, 2)反投影误差小于1像素说明相机内参、畸变、外参和三角测量结果一致性好。如果反投影误差很大但标定重投影误差很小,问题主要集中在相位映射环节。把相位解算得到的投影仪坐标和相机坐标的对应关系画成散点图,如果出现明显的周期性错位,就可以确认是双频展开时周期计数错误,需要回到第3章的展开环节重新检查T_high和T_low的参数设置。
本文还有配套的精品资源,点击获取