2026/10/26 9:14:06

相机标定与2D到3D坐标转换:RGB/红外工程实践

相机标定与2D到3D坐标转换:RGB/红外工程实践 简介面向计算机视觉入门者及机器人、自动驾驶、增强现实方向的开发者这份相机标定与二维到三维坐标转换系统覆盖RGB与红外相机标定、图像去畸变、平面直线、PnP和8点算法并以Docker封装降低环境配置难度可用于课程实验、算法复现与项目原型验证。压缩包共210个文件、约71.12MB包含大量jpg/png标定图像、py与cpp源码、npz标定数据、xml/json配置和说明文档兼顾样例数据、可运行代码与目录说明便于按模块检索和二次开发。目前已有141人学习下载具备一定参考热度。读者可据此串联标定、去畸变、特征对应与位姿求解流程对照样例理解内参外参估计、重投影误差和二维点反投影到三维空间的关键环节并借助文档快速搭建实验环境、定位常见参数与数据格式问题。1. 像素坐标到毫米坐标一套标定系统要先解决哪三件事产线上机械臂要抓零件RGB 相机给出像素坐标红外相机给出温度异常区域可执行机构只认毫米坐标。中间这条链就是相机标定与 2D 到 3D 坐标转换系统要干的事RGB 相机标定和红外相机标定把内参、畸变、外参固定下来图像去畸变把像素拉回理想针孔模型PnP 解出靶标或工件的位姿平面直线算法把像素射线打到已知工作平面上8 点算法则用来做多视图几何的极线校验和外点剔除。标题末尾的压缩包只是交付形式真正常用的就是这几段代码。做视觉引导抓取、尺寸测量、双相机配准的工程师先判断一件事你的 3D 点是从平面交会来的还是从多视图三角化来的这决定了后面参数怎么设。第一次做最容易翻车的地方是把标定误差当成全部误差。2. 标定链路先立住内参、外参与重投影误差的验收线2.1 针孔模型不是一个矩阵内参、外参、畸变各自管什么相机标定最常见的误解是以为calibrateCamera输出一个矩阵就完事。实际输出是一组分层参数内参矩阵K描述焦距和主点畸变系数dist描述镜头把直线弯成曲线的程度外参rvec/tvec描述每一张标定板相对相机的位置。像素坐标和世界坐标的关系是pixel K * (R * X t)再去套畸变模型反过来做 2D 到 3D就是先去掉畸变再用K的逆把像素变成相机坐标系下的射线。内参只跟相机和镜头有关外参只跟当次拍摄的靶标位姿有关。这个区分很重要换镜头必须重标内参动相机安装位置必须重标外参只动工作平面则只需要重新确认平面方程。畸变模型常用k1,k2,p1,p2,k3五个参数径向畸变管桶形和枕形切向畸变管镜头与传感器不平行。焦距不够准远处点的深度误差会被放大主点偏几个像素平面交会点会整体平移。下表是标定结果里最该看的几个量参数作用异常信号fx,fy焦距的像素表示与镜头标称焦距换算后差太多先查图像是否被缩放cx,cy主点通常接近图像中心偏离中心超过图像尺寸的 5%查裁剪和分辨率设置k1,k2,k3径向畸变绝对值异常大通常是棋盘格覆盖太集中p1,p2切向畸变量级很小才正常偏大查镜头安装rvec,tvec每张图的外参平移量的单位取决于棋盘格格长单位标定板格长用毫米外参平移就是毫米用“格”做单位外参平移就是格数。内参不受这个单位影响所以只做像素到归一化坐标时无所谓一旦要做 2D 到 3D 的毫米输出格长必须写进配置。2.2 用棋盘格跑通 RGB 标定的最小代码下面这段是 RGB 相机标定的最小闭环输入是一组不同姿态的棋盘格图像输出内参、畸变和重投影误差。棋盘格内角点数量按你实际打印的板子改代码里用(11,8)只是示例。import cv2 import numpy as np import glob pattern_size (11, 8) # 内角点数量不是方格数量 square_size 20.0 # 毫米仅用于外参尺度 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 obj_points, img_points [], [] for f in sorted(glob.glob(rgb/*.png)): img cv2.imread(f, cv2.IMREAD_GRAYSCALE) if img is None: continue # SB 版本自带亚像素和鲁棒性不需要再调 cornerSubPix ok, corners cv2.findChessboardCornersSB( img, pattern_size, flagscv2.CALIB_CB_EXHAUSTIVE | cv2.CALIB_CB_ACCURACY ) if not ok: print(skip, f) continue img_points.append(corners.reshape(-1, 1, 2).astype(np.float32)) obj_points.append(objp.copy()) rms, K, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, img.shape[::-1], None, None, flagscv2.CALIB_FIX_K3 ) np.savez(rgb_calib.npz, KK, distdist, rmsrms, rvecsnp.array(rvecs), tvecsnp.array(tvecs)) print(RMS:, rms)逻辑上分三步先构造世界坐标点objp的 Z 固定为 0因为棋盘格是平面再逐张检测内角点检测不到就跳过并记录文件名不要强行补点最后把所有点对丢给calibrateCamera。CALIB_FIX_K3在拍摄视角不够多时能防止k3乱跑如果覆盖充分可以去掉。findChessboardCornersSB对低对比度和部分遮挡更稳但它对棋盘格行列数很敏感行列数写反会直接返回失败。一个容易忽略的细节img.shape[::-1]给的是(width,height)不是(height,width)。如果这里传错主点会跑到图像外后续undistortPoints和solvePnP全部对不上。标定图像不要只拍正面四角、中心、上下左右倾斜、远近两档距离都要有否则畸变系数和焦距会互相补偿重投影误差看着小实际边缘点偏得厉害。2.3 标定结果验收重投影误差、覆盖率和三个必看数calibrateCamera返回的 RMS 是全体点的均方根误差单位是像素。工业场景一般希望控制在 0.5 像素以内能做到 0.2 像素左右说明采集质量不错。但 RMS 会被大量好图拉低必须看单张误差。下面这段把每张图的重投影误差算出来按从大到小排优先处理最差的那几张。per_view [] for i, (op, ip, rvec, tvec) in enumerate(zip(obj_points, img_points, rvecs, tvecs)): proj, _ cv2.projectPoints(op, rvec, tvec, K, dist) err cv2.norm(ip, proj, cv2.NORM_L2) / len(proj) per_view.append((i, float(err))) for i, err in sorted(per_view, keylambda x: -x[1])[:5]: print(fview {i}: {err:.3f} px)单张误差突然到 1 像素以上常见原因不是算法而是采集棋盘格没压平、图像运动模糊、反光导致角点偏移、棋盘格打印尺寸和配置不一致。覆盖率的检查更直接把所有角点画到一张空白图上看有没有大片空白区域。如果角点只集中在画面中央畸变系数基本靠猜边缘的 2D 到 3D 转换会明显偏差。还有一个必看数是焦距和传感器像素尺寸的换算。已知镜头标称焦距f_mm和像元尺寸pixel_size_mmfx大致应该在f_mm / pixel_size_mm附近。差一个缩放倍数通常是图像被 resize 过但内参没同步缩放。主点cx,cy偏离图像中心太多先查相机是否开了 ROI 裁剪或 binning。标定不是跑完函数就结束验收不过的标定结果后面所有 PnP 和平面交会都会继承这个误差。3. 红外相机标定低对比度图像里把角点稳定提出来3.1 红外图像为什么让角点检测集体翻车RGB 相机标定那套流程搬到红外相机第一件事就是角点检测大面积失败。原因不玄学红外图像看的是热辐射普通黑白棋盘格在红外波段没有足够的辐射差异黑白方格可能几乎同温红外镜头常用锗材料可见光透不过去不能拿 RGB 的靶标直接复用红外传感器分辨率通常比 RGB 低边缘模糊梯度弱自动增益和non-uniformity correction 会让同一块靶标在帧间灰度漂移。Harris、 Shi-Tomasi 这类基于梯度的检测器在低对比度、低信噪比的图上自然不稳。我一般把红外标定拆成两个问题靶标能不能被红外看清角点能不能被稳定定位。前者靠主动靶标解决后者靠预处理和亚像素优化解决。主动靶标常见做法是金属板做棋盘格黑格和白格发射率不同再用水浴或加热片让整板高于环境温度也有用镂空模板加均匀热源的做法。温度不要太高能形成稳定温差即可操作时注意防烫。靶标表面要哑光避免红外反光。还有一个坑是红外相机的对焦。红外镜头对焦环没有刻度手动拧过头整幅图都软角点亚像素会系统性偏移。标定前先把靶标放在工作距离上调焦到边缘最锐然后锁死对焦环标定和测量过程都不要碰。自动增益也建议关掉或者固定积分时间否则同一组标定图里灰度分布不一致阈值分割会时好时坏。3.2 主动靶标与双相机配准一套可复现的采集流程红外标定的采集流程和 RGB 类似但多了靶标热平衡和参数固定。下面这张表是我通常执行的顺序按这个顺序做返工率最低。步骤操作判断标准1制作主动靶标黑格白格发射率差异明显红外图中能看出棋盘格轮廓2开启热源等待整板温度稳定连续三帧灰度均值变化小于 2%3固定红外相机增益、积分时间、对焦后续采集不再调整4在工作距离附近拍摄 15 到 25 个姿态四角、中心、倾斜、远近都有5逐张提取角点失败的图单独记录失败率低于两成再进入标定角点提取可以用“先增强、再二值化、最后找棋盘格”的路线。下面这段不是万能药但比直接对原始红外图调findChessboardCorners稳得多。def ir_corners(gray, pattern_size): # CLAHE 提升局部对比度clipLimit 过大会放大噪声 clahe cv2.createCLAHE(clipLimit3.0, tileGridSize(8, 8)) g clahe.apply(gray) g cv2.GaussianBlur(g, (5, 5), 0) _, bw cv2.threshold(g, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) bw cv2.morphologyEx(bw, cv2.MORPH_CLOSE, np.ones((3, 3), np.uint8)) ok, corners cv2.findChessboardCornersSB( bw, pattern_size, flagscv2.CALIB_CB_EXHAUSTIVE ) if ok: # 亚像素回到原图做避免二值化损失灰度信息 corners cv2.cornerSubPix( gray, corners, (5, 5), (-1, -1), (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.01) ) return ok, cornersclipLimit控制对比度增强强度红外图噪声大时调到 2.0 到 3.0 就够tileGridSize决定局部统计范围图像分辨率低时用(8,8)再大容易把棋盘格边缘吃掉。threshold用 Otsu 是因为靶标温差稳定后直方图双峰明显如果现场环境温度变化大可以换成固定阈值加手动校准。cornerSubPix的窗口不要开太大红外边缘本身就模糊窗口越大越容易把角点拉向背景。红外内参标定同样用calibrateCamera但建议固定k30因为红外图像边缘信噪比低高阶径向畸变很容易过拟合。标定完成后用一个独立姿态的靶标做验证用solvePnP求位姿再把靶标角点反投影回图像看误差分布。如果中心误差小、边缘误差大说明畸变模型没压住优先增加边缘姿态的图而不是加高阶项。3.3 红外与 RGB 外参分别 PnP 再做坐标系桥接RGB 和红外要输出同一个世界坐标必须知道两个相机之间的外参。同模态双目可以直接用stereoCalibrate但 RGB 和红外是跨模态同一块棋盘格在两张图里的对比度完全不同直接双目标定经常失败。更稳的做法是让两个相机同时看同一个主动靶标分别用各自的内参做solvePnP得到靶标到 RGB 的变换T_rgb_target和靶标到红外的变换T_ir_target再桥接def compose_extrinsic(T_rgb_target, T_ir_target): # 都是 4x4定义为 target - camera T_rgb_ir T_rgb_target np.linalg.inv(T_ir_target) return T_rgb_ir这段要求两个相机在同一时刻看到同一个靶标且靶标不能动。T_rgb_target和T_ir_target都由solvePnP得到输入点必须是同一组靶标角点的世界坐标像素点各自检测。桥接结果T_rgb_ir表示红外相机坐标系到 RGB 相机坐标系的变换后续把红外的 3D 点转到 RGB 坐标或者把 RGB 的像素射线转到红外坐标都用它。跨模态配准的误差主要来自时间同步和角点定位。红外帧率通常低于 RGB运动中的靶标会产生时间差所以标定阶段必须静止拍摄。靶标要同时被两个相机完整看到视场重叠不够就先调安装。外参标定完后用一组没参与计算的靶标姿态做验证把红外检测到的角点用T_rgb_ir转到 RGB 坐标系再投影到 RGB 图像看是否落在对应角点附近。这个检查能抓出坐标系方向写反、平移单位不一致这类低级错误。4. 从去畸变到 2D 转 3D射线、平面与 PnP 的串联方式4.1 去畸变的两种路径整图重映射与只去点去畸变有两个入口用途不同。要做图像展示、模板匹配、语义分割通常把整幅图重映射成无畸变图要做坐标转换只需要把参与计算的像素点去畸变整图重映射反而多一次插值还会引入黑边。整图去畸变的代码如下h, w img.shape[:2] newK, roi cv2.getOptimalNewCameraMatrix(K, dist, (w, h), alpha0) map1, map2 cv2.initUndistortRectifyMap( K, dist, None, newK, (w, h), cv2.CV_32FC1 ) und cv2.remap(img, map1, map2, cv2.INTER_LINEAR)alpha0会裁掉有效区域外的黑边适合做检测alpha1保留全部像素适合需要完整视场的标定检查。newK是新内参后续在这张无畸变图上做 PnP必须用newK不能再用原始K。这一点是血泪经验图去畸变了内参没换解出来的位姿会整体偏。只去点的写法更适合 2D 到 3Dpts np.array([[[u, v]]], dtypenp.float32) und_pts cv2.undistortPoints(pts, K, dist, PK) # PK 时输出仍是像素坐标但已经去掉畸变undistortPoints不给P时输出归一化坐标给了PK输出去畸变后的像素坐标。如果后面用solvePnP传的是去畸变像素点畸变参数就要传None或全零如果传原始像素点畸变参数传dist。两种写法不能混。混用的典型现象是中心点还行边缘点越偏越远最后怀疑标定不准其实是重复去畸变。4.2 PnP 解位姿平面目标优先 IPPE非平面才用迭代solvePnP输入世界坐标点、像素点、内参和畸变输出旋转向量和平移向量。标定板、工件上表面这类平面目标优先用SOLVEPNP_IPPE它专门处理平面点数值稳定性比通用迭代好。非平面点云用SOLVEPNP_ITERATIVE或SOLVEPNP_EPNP需要初值时再开useExtrinsicGuess。ok, rvec, tvec cv2.solvePnP( obj_pts, # Nx3世界坐标单位与 tvec 一致 img_pts, # Nx2像素坐标 K, dist, flagscv2.SOLVEPNP_IPPE ) R, _ cv2.Rodrigues(rvec)obj_pts至少 4 个不能共线分布要覆盖整个靶标。平面点用 IPPE 时点序必须和img_pts一一对应点序错了会解出一个镜像位姿重投影看着还行实际方向反了。判断点序有没有错可以把靶标坐标系画到图像上看 X/Y 轴是否指向预期方向。位姿抖动大先看点数和分布再看是不是用了ITERATIVE解平面。平面点用迭代法在视角接近正对时容易陷入局部解换IPPE往往直接稳下来。如果外点明显用solvePnPRansac把重投影阈值设成 2 到 3 像素内点比例低于一半时先查角点检测不要硬调 RANSAC 参数。4.3 平面直线算法像素射线与已知平面求交标题里的平面直线算法落地到 2D 到 3D 最常见的形态是“像素射线与已知平面求交”。相机光心C和像素点确定一条射线工作平面用n·X d 0表示联立求交点。OpenCV 的rvec/tvec定义的是世界到相机的变换所以相机光心在世界坐标下是C -R.T t。def pixel_ray_to_plane(u, v, K, R, t, plane_n, plane_d): Kinv np.linalg.inv(K) p np.array([u, v, 1.0]) d_cam Kinv p # 相机坐标系下的射线方向 d_world R d_cam # 转到世界坐标系 C -R.T t # 相机光心在世界坐标系 denom float(plane_n d_world) if abs(denom) 1e-9: return None # 射线与平面平行 lam -(float(plane_n C) plane_d) / denom return C lam * d_worldplane_n是平面单位法向量plane_d是平面方程常数项。工作平面是Z0时plane_n[0,0,1]、plane_d0。如果射线方向与平面几乎平行denom接近零交点会飞到很远函数返回None比返回一个巨大坐标更安全。这个函数默认u,v已经去畸变如果传原始像素要先把dist加进去先undistortPoints再算射线。平面方程必须和标定外参在同一个世界坐标系。常见做法是标定板上表面就是工作平面靶标坐标系 Z0那么直接把plane_n[0,0,1]、plane_d0代入但R,t必须是当前帧靶标到相机的位姿不是标定时的某一张外参。如果工作平面和靶标平面不重合需要额外测一个平面方程或者用不在同一直线上的三个 3D 点拟合平面。平面拟合误差会直接进入 2D 到 3D 结果测平面时尽量覆盖实际工作区域。4.4 8 点算法基础矩阵适合做校验不适合直接反投影8 点算法解的是基础矩阵F满足x2.T F x1 0。它描述两个视图之间的极线约束不需要知道内参所以常用来做多相机几何校验、外点剔除、极线搜索。OpenCV 里直接调用F, mask cv2.findFundamentalMat( pts1, pts2, cv2.FM_8POINT, 1.0, 0.99 )FM_8POINT是纯 8 点线性解对噪声很敏感实际使用前必须把像素坐标归一化到均值为零、平均距离为根号二否则数值条件很差。OpenCV 内部会做归一化但自己实现时这一步不能省。mask标记内点外点比例高时先查点对匹配不要直接拿F做三角化。8 点算法的边界很清楚平面场景下F退化应该用单应矩阵H纯旋转下F无解F只能给极线约束不能直接给 3D 点。要做 2D 到 3D还是得回到P矩阵和三角化或者用已知平面做射线交会。F和E的关系是E K2.T F K1有了标定内参才能从F转E再从E分解R,t。分解有四个解需要用正深度筛选这一步在代码里容易漏。方法输入输出适用场景PnP3D 点 2D 点 内参相机位姿已知靶标或工件模型8 点法两视图 2D 点对基础矩阵F极线校验、外点剔除单应H平面点对平面映射平面场景、去畸变检查三角化两视图 2D 点 投影矩阵3D 点非平面、多视图重建5. 避坑与排查标定系统上线前最容易翻车的五处5.1 采集与标定阶段的三个坑坑一重投影误差很小实际测量偏几毫米。现象是标定 RMS 只有 0.2 像素但平面交会出来的点跟实测差 2 到 3 毫米。原因通常不是标定算法而是标定时的对焦距离和工作距离不一致或者标定板平面跟实际工作平面不平行。镜头对焦变化会改变有效焦距虽然内参看起来只差一点但工作距离上会放大成毫米级误差。解决方法是把标定板放到实际工作距离附近拍摄锁死对焦环标定后用独立靶标位姿做留点验证如果工作平面和标定板平面不重合单独测平面方程不要默认Z0。坑二红外角点检测整张失败。现象是 RGB 图能正常检出红外图返回False或者角点跳到棋盘格边缘外。原因是红外靶标温差不够、自动增益让帧间灰度变化、镜头离焦。解决方法是换主动靶标固定增益和积分时间先用ir_corners里的 CLAHE 加 Otsu 看二值图是否出现完整棋盘格如果二值图都看不出格子不要继续调findChessboardCorners参数先解决靶标和热平衡。坑三标定图很多但全是正面。现象是 RMS 很低边缘去畸变后直线还是弯的。原因是棋盘格覆盖集中在画面中央畸变系数没有足够约束。解决方法是补拍四角和倾斜姿态每个象限至少有一张倾斜角度拉开远近各拍一组。不要用“多拍几十张正面”代替姿态多样性正面图对畸变系数几乎没有新信息。5.2 坐标转换与算法阶段的三个坑坑四solvePnP位姿抖动平面目标尤其明显。现象是同一静止目标连续解算tvec跳动几个毫米。原因是平面点用了SOLVEPNP_ITERATIVE或者点数太少、点集中在一条线上。解决方法是平面目标换SOLVEPNP_IPPE增加不共线的点检查点序是否一一对应外点多时用solvePnPRansac但先保证角点检测稳定不要用 RANSAC 掩盖检测问题。坑五去畸变做了两次边缘点越算越偏。现象是中心区域 2D 到 3D 正常边缘点偏移随距离增大。原因是先把点undistortPoints又把原始dist传给solvePnP等于去了两次畸变。解决方法是统一约定传原始像素就带dist传去畸变像素就传None或全零整图remap后要用newK不能再用原始K。这个错误不会报异常只会让结果慢慢偏属于典型的黑匣子问题。坑六拿 8 点法算出的F直接反投影。现象是极线看着对三角化出来的 3D 点深度乱跳。原因是F只有极线约束尺度不确定平面场景还会退化。解决方法是把F用在极线校验和外点剔除2D 到 3D 用已知平面交会或带标定投影矩阵的三角化平面场景改用H不要硬解F。如果确实要从F恢复位姿先转E再分解并做正深度筛选。6. 精度验收与参数固化留点交叉验证怎么做才不虚标定跑通只是开始真正决定这套系统能不能上线的是留点验证。我的习惯是标定图只用八成留两成不参与计算标定完成后用留出的图做solvePnP把靶标角点反投影统计 RMS 和最大误差。只看训练集的重投影误差相当于自己考自己参考价值有限。更接近实际工况的做法是换一个独立靶标位姿放在工作距离上用平面交会算几个已知点跟实测坐标比。下表是我通常记录的四项指标指标建议观察值说明留点重投影 RMS小于 0.5 像素超过先查角点和畸变留点最大误差小于 1.5 像素单点跳变往往是对应点错了平面交会误差小于工作精度的三分之一用实测点比对双相机配准误差小于最小特征尺寸的一半跨模态重点看边缘参数固化不要只存K和dist把图像尺寸、棋盘格格长、单位、工作距离、对焦状态、标定日期一起写进配置文件。下面是一个最小示例字段名可以按项目改但单位必须写清楚。{ camera: rgb, image_size: [1920, 1080], K: [[1200.0, 0.0, 960.0], [0.0, 1200.0, 540.0], [0.0, 0.0, 1.0]], dist: [0.12, -0.08, 0.001, 0.002, 0.0], rms: 0.18, unit: mm, square_size: 20.0, working_distance: 600.0 }我一般把标定结果、验证脚本、留点误差报告放在同一个目录换镜头或动相机安装先跑验证脚本不通过就不进入测量流程。早期我吃过一次亏标定 RMS 很漂亮但验证脚本只跑了训练图上线后边缘点偏了 3 毫米返工查了两天才发现是标定板没压平。后来我把“留点不过不交付”写进流程省下的后悔药比多拍几张图值钱。希望这套流程帮到你。本文还有配套的精品资源点击获取