
简介ProCamCalib 是一款面向计算机视觉与三维重建研究者的投影仪-相机系统校准工具用 Java 编写可对投影机-相机系统完成几何与颜色校准并支持多相机、多投影仪配置。它通过置于原点的首台相机进行立体校准校准板需在所有相机中可见由于纯相机系统是其子集该工具也能以零投影仪方式校准多台相机实现 Fiala 方法。资源包共 24 个文件约 52KB包含 8 个 png 示意图、3 个 xml 与 3 个 java 源码、2 个 cmd 启动脚本、2 个 md 说明文档以及 form、gif 和两套界面外观配置另附 pom.xml 与 LICENSE 等工程文件便于直接构建与二次开发。目前已有 430 人学习下载。对于从事结构光、投影标定或相机阵列研究的读者可借此获得可运行的校准程序、跨平台启动方式与源码结构参考快速复现论文方法并排查标定流程中的常见问题。1. 投影仪标定这件事为什么总在“最后一公里”翻车做过结构光、条纹投影或投影 AR 的人多半有同感相机标定早就轻车熟路张正友那套棋盘格流程闭着眼都能跑可一旦把投影仪拉进来当“反向相机”事情立刻变得玄学。投影仪不能拍照它只能投图案你得靠相机去看它投出来的东西于是误差被放大了一整圈。procamcalib 这类“相机-投影仪系统用户友好标定方法”要解决的正是这个最后一公里让一个没有光学测量背景的工程师也能在普通桌面环境下把投影仪的内参、外参和相机-投影仪之间的位姿关系标出来。它适合做三维重建、投影映射、结构光扫描、投影触控的从业者也适合刚接手一套现成硬件、却卡在标定环节的新手。核心思路不复杂把投影仪当成一台“只会输出、不会输入的相机”用相机当它的眼睛通过多组已知几何关系的图案反解出投影仪的光心与朝向。2. 把投影仪当相机procamcalib 的几何模型与选型理由2.1 为什么投影仪可以建模成针孔相机投影仪内部是一块 DMD 或 LCD 面板光线经过镜头投射出去。从几何上看这个过程和相机成像完全互逆相机是三维世界的光线汇聚到二维传感器投影仪是二维面板的光线发散到三维世界。只要把光路反向投影仪就等价于一台针孔相机同样满足 $s \cdot \tilde{m} K [R|t] \tilde{M}$ 的投影方程其中 $K$ 是投影仪内参$[R|t]$ 是它相对于世界坐标系的位姿。这个等价关系是整套标定方法的理论地基。它意味着你不需要为投影仪发明一套新数学直接复用相机标定的求解框架即可。区别只在于相机的“像素”是传感器上真实存在的感光单元投影仪的“像素”是它投射出去的图案坐标你得通过相机观测来间接获得这些坐标。常见做法是投影一组已知编码的图案比如棋盘格、相移条纹或格雷码让相机拍下投影结果再通过解码把每个相机像素对应的投影仪像素坐标算出来。这一步的质量直接决定标定精度也是新手最容易翻车的地方。2.2 相机-投影仪系统的参数到底有哪些一套完整的相机-投影仪标定输出的是两组内参和一组外参参数类别符号含义典型维度相机内参$K_c$焦距、主点、像素倾斜3×3相机畸变$d_c$径向与切向畸变系数4~5 维投影仪内参$K_p$投影仪等效焦距与主点3×3投影仪畸变$d_p$投影镜头畸变4~5 维相对位姿$R, t$相机到投影仪的旋转与平移3×3 3×1很多人以为标定就是求 $K_p$其实相对位姿 $R, t$ 才是结构光系统里最要命的部分。三维重建时相机和投影仪之间的基线长度和角度误差会直接放大到深度图上基线越短深度误差越敏感。所以标定时不能只盯着重投影误差还要看重建出来的平面是否真的平。2.3 选棋盘格还是选条纹图案方案的取舍投影图案的选择决定了整个标定流程的复杂度。棋盘格方案最直观投影仪投棋盘格相机拍下来用角点检测找到棋盘格角点在相机图像里的位置同时知道这些角点在投影仪图案里的坐标就得到了一组 2D-2D 对应点。优点是工具链成熟OpenCV 的findChessboardCorners直接能用缺点是投影仪投出的棋盘格受镜头畸变和离焦影响角点会变糊检测精度下降。条纹方案精度更高尤其是相移法加多频外差能把投影仪像素坐标解到亚像素级。但代价是解码流程长相位展开容易出错对环境光和物体表面反射率敏感。我一般会建议如果只是做投影映射或粗略的 AR 对齐棋盘格够用如果要做微米级三维扫描老老实实上条纹。提示无论选哪种图案投影面尽量用白色哑光板不要用打印纸纸的平整度和反光会引入系统性误差。3. 从零跑通一套标定图案生成、采集与求解3.1 生成投影图案与采集图像先准备一组棋盘格图案投影仪全屏显示相机从不同角度拍摄。关键点是相机和投影仪的相对位置在整个采集过程中不能动只移动标定板或改变投影图案的位姿。下面是一个生成棋盘格图案的 Python 脚本import cv2 import numpy as np # 棋盘格参数内角点数量决定图案密度 pattern_size (11, 8) # 列数, 行数 square_size 80 # 每个方格像素大小投影仪分辨率越高可适当加大 margin 200 # 留白避免投影边缘被镜头暗角影响 width pattern_size[0] * square_size 2 * margin height pattern_size[1] * square_size 2 * margin # 创建白色背景 img np.ones((height, width), dtypenp.uint8) * 255 # 绘制黑色方格 for i in range(pattern_size[1]): for j in range(pattern_size[0]): if (i j) % 2 0: x0 margin j * square_size y0 margin i * square_size img[y0:y0square_size, x0:x0square_size] 0 cv2.imwrite(projector_chessboard.png, img) print(f图案尺寸: {width}x{height}, 内角点: {pattern_size})这段代码生成一张黑白棋盘格pattern_size是内角点数量不是方格数这是 OpenCV 的约定新手常在这里搞混。square_size控制方格像素大小投影仪分辨率如果是 1920×108080 像素的方格大约能排下 11×8 个内角点。margin留白是为了避开投影镜头的边缘暗角和畸变最严重的区域。采集时相机拍下投影在标定板上的棋盘格。注意标定板本身不要有图案否则会和投影图案混在一起。我一般用一块纯白哑光板投影棋盘格后拍摄这样相机看到的只有投影图案。3.2 提取对应点相机像素到投影仪像素采集到图像后需要找到棋盘格角点在相机图像中的位置同时知道这些角点在投影仪图案中的坐标。下面是对单张图像提取角点的代码import cv2 import numpy as np # 读取相机拍摄的投影棋盘格图像 img cv2.imread(captured_chessboard_01.png) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 棋盘格内角点数量必须与生成时一致 pattern_size (11, 8) # 查找角点 ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if ret: # 亚像素精化窗口大小决定精化范围 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) # 生成投影仪图案中对应的角点坐标 # 投影仪图案的角点坐标是已知的按行列顺序排列 objp np.zeros((pattern_size[0] * pattern_size[1], 2), np.float32) objp[:, 0] np.tile(np.arange(pattern_size[0]), pattern_size[1]) * 80 200 objp[:, 1] np.repeat(np.arange(pattern_size[1]), pattern_size[0]) * 80 200 print(f检测到 {len(corners_refined)} 个角点) print(f相机角点示例: {corners_refined[0].ravel()}) print(f投影仪角点示例: {objp[0]}) else: print(角点检测失败检查图案是否完整、是否模糊)findChessboardCorners返回的角点顺序是固定的从左上到右下按行排列。cornerSubPix用迭代法把角点精度提到亚像素级窗口(11, 11)是搜索范围太大容易受噪声影响太小精化不足。objp是投影仪图案中角点的理论坐标由生成图案时的margin和square_size决定必须和实际投影的图案严格对应。这里有个容易忽略的点投影仪投出的图案经过镜头后会有畸变所以objp里的坐标并不是投影仪面板上的真实坐标而是理想坐标。标定过程会同时求解畸变系数把这个偏差建模进去。3.3 用 OpenCV 求解双目标定拿到多组对应点后就可以求解相机和投影仪的内参、畸变以及相对位姿。OpenCV 的stereoCalibrate可以直接用把投影仪当成第二台相机import cv2 import numpy as np import glob # 棋盘格参数 pattern_size (11, 8) square_size 80 margin 200 # 生成标定板的理论角点坐标世界坐标系Z0 objp np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp * square_size # 存储相机和投影仪的角点 cam_points [] proj_points [] obj_points [] # 遍历所有采集的图像 images sorted(glob.glob(captured_chessboard_*.png)) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if not ret: print(f{fname} 角点检测失败跳过) continue criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) # 相机角点 cam_points.append(corners) # 投影仪角点这里假设投影图案与标定板对齐 # 实际项目中需要通过解码或已知图案映射获得 proj_pts np.zeros((pattern_size[0] * pattern_size[1], 1, 2), np.float32) proj_pts[:, 0, 0] np.tile(np.arange(pattern_size[0]), pattern_size[1]) * square_size margin proj_pts[:, 0, 1] np.repeat(np.arange(pattern_size[1]), pattern_size[0]) * square_size margin proj_points.append(proj_pts) obj_points.append(objp) # 双目标定 ret, K_cam, d_cam, K_proj, d_proj, R, T, E, F cv2.stereoCalibrate( obj_points, cam_points, proj_points, None, None, None, None, (1920, 1080), # 图像尺寸按实际相机分辨率填 flagscv2.CALIB_FIX_INTRINSIC # 如果内参已单独标定可固定 ) print(f重投影误差: {ret:.4f}) print(f相机内参:\n{K_cam}) print(f投影仪内参:\n{K_proj}) print(f相对旋转:\n{R}) print(f相对平移:\n{T})stereoCalibrate的flags参数很关键。如果你已经单独标定过相机内参用CALIB_FIX_INTRINSIC固定住只优化外参结果更稳定。如果从零开始就用默认 flags让所有参数一起优化但需要足够多的图像一般 15 到 20 组才能收敛。ret是重投影误差单位是像素低于 0.5 算不错低于 0.2 算优秀。如果超过 1.0先检查角点检测是否准确再检查投影图案和理论坐标是否对齐。注意stereoCalibrate假设两个相机看到的是同一个标定板。在投影仪场景里投影仪“看到”的是它自己投出的图案所以proj_points必须是投影仪图案坐标而不是相机图像坐标。这个映射关系如果搞错标定结果会完全离谱。4. 标定结果怎么验证重投影误差之外的三把尺子4.1 重投影误差只是及格线stereoCalibrate返回的重投影误差是优化过程的副产品它告诉你求解出的参数在数学上有多自洽但不保证物理上正确。我见过重投影误差 0.15 像素、但重建出来的平面弯曲好几个毫米的案例。原因是优化陷入了局部极小值或者对应点存在系统性偏差。验证标定质量至少要过三关重投影误差、极线校正、实际重建。重投影误差低于 0.5 像素是基本要求极线校正后左右图像的对应点应该在同一水平线上偏差不超过 1 像素实际重建一个已知尺寸的平面或球体测量误差应该在系统精度的合理范围内。4.2 极线校正与三维重建验证极线校正的代码很简单用stereoRectify和initUndistortRectifyMap就能生成校正映射import cv2 import numpy as np # 假设已经得到 K_cam, d_cam, K_proj, d_proj, R, T image_size (1920, 1080) # 极线校正 R1, R2, P1, P2, Q, validPixROI1, validPixROI2 cv2.stereoRectify( K_cam, d_cam, K_proj, d_proj, image_size, R, T, alpha0 # alpha0 表示校正后图像只保留有效区域 ) # 生成校正映射 map1x, map1y cv2.initUndistortRectifyMap(K_cam, d_cam, R1, P1, image_size, cv2.CV_32FC1) map2x, map2y cv2.initUndistortRectifyMap(K_proj, d_proj, R2, P2, image_size, cv2.CV_32FC1) # 对相机图像应用校正 img_cam cv2.imread(captured_chessboard_01.png) img_cam_rect cv2.remap(img_cam, map1x, map1y, cv2.INTER_LINEAR) # 对投影仪图案应用校正需要先把投影图案渲染成图像 img_proj cv2.imread(projector_chessboard.png) img_proj_rect cv2.remap(img_proj, map2x, map2y, cv2.INTER_LINEAR) # 保存结果人工检查对应点是否在同一水平线 cv2.imwrite(rectified_cam.png, img_cam_rect) cv2.imwrite(rectified_proj.png, img_proj_rect) print(极线校正完成检查两张图的对应点是否水平对齐)alpha0会让校正后的图像裁掉无效区域适合做定量分析alpha1保留全部像素适合可视化。校正后相机图像和投影仪图案的对应点应该在同一水平线上如果偏差明显说明外参 $R, T$ 不准。三维重建验证更直接用标定好的参数对一组已知尺寸的物体做重建测量重建点云中平面的平整度或球体的半径。如果误差在 0.1 毫米以内标定质量就足够应付大多数工业场景。4.3 参数敏感性哪些参数不能随便动标定结果里有些参数对误差极其敏感调参时不能拍脑袋。下面这张表是我在实际项目中总结的敏感性排序参数敏感性调整建议相对平移 $T$极高基线越短越敏感尽量拉开相机与投影仪距离投影仪焦距高与投影距离耦合需多组不同位姿解耦相机焦距中单独标定后固定不要和投影仪一起优化主点低对重建影响小可固定为图像中心畸变系数中投影镜头畸变通常比相机大不能忽略基线 $T$ 的敏感性来自三角测量原理深度误差与基线成反比。如果相机和投影仪靠得太近深度方向的分辨率会急剧下降。我一般建议基线至少是工作距离的 1/5比如工作距离 500 毫米基线不要小于 100 毫米。5. 避坑与排查标定失败时先看这五个地方5.1 角点检测失败或角点顺序错乱现象findChessboardCorners返回 False或者检测到的角点数量不对、顺序混乱。原因投影图案模糊、对比度不足、棋盘格被遮挡或者pattern_size与实际图案不一致。投影仪离焦会让棋盘格边缘变糊角点检测算法找不到清晰的角点。解决先检查投影图案是否完整落在标定板内再调整投影仪焦距让图案尽量清晰。如果还是不行改用圆点标定板findCirclesGrid对模糊的容忍度更高。角点顺序错乱通常是pattern_size写反了列数和行数搞混检查生成图案时的参数。5.2 重投影误差大但角点检测正常现象角点检测没问题但stereoCalibrate返回的重投影误差超过 1.0 像素。原因投影仪图案坐标和相机角点坐标的对应关系错了。常见错误是把相机图像的角点坐标直接当成投影仪坐标或者投影图案的理论坐标没有考虑margin和square_size。解决打印出第一组对应点的坐标人工核对。相机角点应该在图像范围内投影仪角点应该在投影图案范围内。如果两者数量不一致检查是否所有图像都检测到了完整的棋盘格。5.3 标定结果在部分区域准确、部分区域偏差大现象重建出来的平面在中心区域平整边缘翘曲。原因镜头畸变没有充分建模或者标定板没有覆盖整个视场。投影镜头的畸变通常比相机镜头大尤其是短焦投影仪边缘畸变可能达到几个像素。解决增加标定板在视场边缘的采集图像让畸变系数有足够的数据支撑。如果畸变仍然拟合不好考虑用高阶畸变模型或者在校正后再做一次残差补偿。5.4 多次标定结果不一致现象同样的硬件两次标定得到的 $R, T$ 差异明显。原因采集图像时相机或投影仪发生了微小移动或者标定板位姿变化不够丰富。标定算法对位姿的多样性很敏感如果所有图像都是同一个角度拍的外参解会不稳定。解决采集时让标定板覆盖不同的角度和位置至少包含 3 个不同的倾斜方向。相机和投影仪用夹具固定避免采集过程中碰触。如果条件允许采集 20 组以上图像用calibrateCamera的flags参数固定内参只优化外参。5.5 投影图案与相机图像不同步现象相机拍到的图案和投影仪显示的图案不一致比如条纹相位错位。原因投影仪刷新和相机曝光没有同步或者投影图案切换时相机正在曝光。这种情况在相移条纹方案里特别常见。解决用硬件触发同步相机和投影仪或者降低相机曝光时间确保曝光窗口落在投影图案稳定显示的时间内。如果无法硬件同步在软件里加延时等投影图案稳定后再触发相机。6. 把标定精度再压一档亚像素对应点与光束法平差标定做到重投影误差 0.3 像素很多人就觉得到头了。但如果你的应用是微米级三维扫描或高精度投影映射这个精度还不够。我一般会做两件事把对应点精度提到亚像素级再用光束法平差Bundle Adjustment做全局优化。亚像素对应点的关键是相位解码。相移法加多频外差能把投影仪像素坐标解到 0.01 像素级比棋盘格角点检测高一个数量级。具体做法是投影一组正弦条纹用相机拍下变形条纹通过相位解算得到每个相机像素对应的投影仪相位再转换成投影仪像素坐标。这一步的代码量不小但核心就是atan2和相位展开import numpy as np def phase_shift(images): 四步相移法解相位images 是四张相移图 I1, I2, I3, I4 images # 相位主值范围 [-pi, pi] phi np.arctan2(I4 - I2, I1 - I3) # 调制强度用于判断相位质量 modulation np.sqrt((I1 - I3)**2 (I4 - I2)**2) / 2 return phi, modulation def unwrap_phase(phi, modulation, threshold10): 基于调制度的相位展开简单版本 # 调制度低的地方相位不可靠标记为无效 mask modulation threshold phi_unwrapped phi.copy() # 实际项目中需要用多频外差或质量引导展开 return phi_unwrapped, maskphase_shift返回的phi是包裹相位范围在 $[-\pi, \pi]$需要用多频外差或空间展开算法解出绝对相位。modulation是调制度反映条纹质量低于阈值的地方相位不可靠直接剔除。这一步的精度直接决定最终标定精度条纹质量差的话后面怎么优化都补不回来。光束法平差是把所有参数和所有对应点放在一个优化问题里最小化重投影误差。OpenCV 没有直接提供投影仪-相机系统的 BA 接口但可以用scipy.optimize.least_squares自己搭。优化变量包括相机内参、投影仪内参、畸变系数、每张图像的位姿以及相对外参。计算量不小但精度提升明显我实测过BA 之后重投影误差能从 0.3 像素降到 0.1 像素以内。最后说个血泪经验标定这件事采集数据的质量占七成算法只占三成。我见过太多人花几天调代码结果发现是标定板不平或者投影仪没对焦。先把硬件和采集流程做扎实再谈算法优化。希望帮到你。本文还有配套的精品资源点击获取