首页 / 资讯中心 / 文章详情

计算机视觉相机标定与2D转3D坐标转换实战指南

计算机视觉相机标定与2D转3D坐标转换实战指南 ★ FEATURED ARTICLE
简介这份资源面向计算机视觉初学者与机器人、自动驾驶、AR方向的开发者提供一套从相机标定到2D转3D坐标的完整算法实现。内容覆盖RGB相机与红外相机标定、图像去畸变、平面直线算法、PnP算法及8点算法等核心环节并借助Docker封装降低环境配置门槛便于跨平台部署与二次开发。压缩包共210个文件约71.12MB以jpg与png标定图像、28个Python脚本、npz标定数据、xml与json配置为主另含少量cpp源码、说明文档与项目文件目录结构清晰方便按模块查阅。目前已有140人学习下载。读者可据此掌握标定板采集、内参外参求解、畸变校正到姿态估计的完整链路并参考附赠文档与示例数据快速复现实验、排查参数问题适合作为课程设计、科研原型或工程落地的实践参考。1. 从一张棋盘格照片到三维空间点相机标定与2D转3D到底在解决什么你拍了一张棋盘格照片想用它算出相机的内参和外参再把图像里某个像素点反推回世界坐标系下的三维位置——这件事听起来简单但真正动手时你会发现RGB相机和红外相机的成像模型不一样去畸变和PnP的先后顺序会影响精度平面直线算法和8点算法适用的场景完全不同。这套「基于计算机视觉的相机标定与3D坐标转换系统」要解决的就是把这几个环节串成一条可复现的流水线从采集标定板图像开始分别完成RGB和红外相机的标定拿到内参、外参和畸变系数然后对图像做去畸变最后用PnP或8点算法把2D像素坐标转换成3D空间坐标。适合做机器人抓取、结构光扫描、双目测距、AR叠加的工程师也适合刚接触SLAM或多传感器融合、想把标定这一环彻底搞明白的人。下面按实际落地顺序拆开讲每一步都给可抄的参数和代码。2. RGB相机标定从采集到内参矩阵的完整链路2.1 为什么张正友标定法仍然是首选常见做法是打印一张棋盘格从不同角度拍15到30张用OpenCV的findChessboardCorners提取角点再调calibrateCamera算内参和畸变。选它的理由很直接不需要高精度三维标定物一张A4纸打印的棋盘格就能达到亚像素级角点精度而且OpenCV、MATLAB、ROS的标定工具都原生支持。相比直接线性变换DLT需要已知三维点张正友法只要求平面标定板实操门槛低得多。但这里有个容易被忽略的点棋盘格角点的提取精度直接决定标定结果。我一般会把图像先转灰度用cornerSubPix做亚像素优化窗口设成(11,11)迭代终止条件设成30次或0.001精度。如果角点提取出现明显偏移后面算出来的内参矩阵再准也没用。2.2 采集图像时的四个硬约束标定板在图像中的位置和姿态不是随便拍就行。血泪经验是至少要有5张以上标定板倾斜角度大于30度的图像否则畸变系数中的切向畸变p1、p2会解不稳定。另外标定板要覆盖图像的四角和中心区域不能只集中在中间。如果只用正对相机的图像主点cx、cy的估计会严重偏离真实值。具体采集时我会遵守四条第一标定板平面与相机光轴夹角在20到60度之间变化第二标定板在图像中的面积占比不低于1/4第三避免强反光和运动模糊第四同一姿态不要重复拍超过3张。这四条能保证calibrateCamera的优化函数不会陷入局部极小。2.3 标定代码与参数说明import cv2 import numpy as np import glob # 棋盘格内角点数量比如9x6的棋盘格内角点是8x5 pattern_size (8, 5) # 棋盘格方格实际物理尺寸单位毫米 square_size 25.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 [] # 3D点 img_points [] # 2D点 images glob.glob(calib_rgb/*.jpg) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, pattern_size, None) if ret: corners_refined cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_refined) # 标定输入3D-2D点对输出内参矩阵、畸变系数、旋转和平移向量 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print(内参矩阵:\n, mtx) print(畸变系数:, dist.ravel())这段代码里pattern_size必须和实际棋盘格内角点数一致数错一个角点整组数据都会报错。square_size只影响外参的尺度不影响内参但如果你后续要做三维重建这个值必须和真实物理尺寸一致。calibrateCamera返回的mtx是3x3内参矩阵dist是1x5或1x8的畸变向量顺序是k1, k2, p1, p2, k3。如果重投影误差超过0.5像素优先检查角点提取和采集姿态而不是去调标定函数参数。3. 红外相机标定和RGB相机的三个关键差异3.1 红外成像为什么不能直接套用RGB标定流程红外相机的成像传感器对温度敏感标定板如果只是普通打印纸在红外图像里对比度极低角点根本提不出来。常见做法是用金属板加热后形成热棋盘格或者用主动红外LED阵列做标定靶。另一个差异是红外图像往往分辨率更低、噪声更大findChessboardCorners的默认参数容易失败需要把adaptiveThreshWinSizeMin和adaptiveThreshWinSizeMax调大比如从默认的3和23调到10和50。我一般会先用直方图均衡化增强红外图像的对比度再走角点提取。如果还是提不到角点就改用圆点标定板用findCirclesGrid代替findChessboardCorners对低对比度图像更鲁棒。3.2 红外标定的参数调整与验证# 红外图像预处理直方图均衡化 自适应阈值 gray_ir cv2.cvtColor(img_ir, cv2.COLOR_BGR2GRAY) gray_ir cv2.equalizeHist(gray_ir) # 调整棋盘格检测参数以适应低对比度 flags cv2.CALIB_CB_ADAPTIVE_THRESH cv2.CALIB_CB_NORMALIZE_IMAGE ret, corners cv2.findChessboardCorners(gray_ir, pattern_size, flags) # 如果棋盘格失败改用圆点网格 if not ret: ret, corners cv2.findCirclesGrid(gray_ir, pattern_size, flagscv2.CALIB_CB_SYMMETRIC_GRID)红外标定的验证方法和RGB一样看重投影误差但要注意红外相机的畸变模型可能和RGB不同有些红外镜头畸变很小强行解k3会导致过拟合。如果dist里的k3绝对值小于1e-4可以直接把它固定为0只解k1, k2, p1, p2。另外红外相机的内参和RGB相机的内参通常不共享必须分别标定不能混用。3.3 RGB与红外联合标定的外参求法如果你要做RGB-红外融合还需要求两个相机之间的外参。常见做法是让两个相机同时看同一个标定板分别提取角点然后用stereoCalibrate求旋转矩阵R和平移向量T。这里有个坑两个相机的曝光时间不同红外图像和RGB图像的采集时刻可能有延迟如果标定板在移动角点对应关系会错位。我一般会固定标定板用硬件触发同步采集或者至少保证标定板静止。# 假设已经分别拿到RGB和红外的角点 ret, mtx_rgb, dist_rgb, mtx_ir, dist_ir, R, T, E, F cv2.stereoCalibrate( obj_points, img_points_rgb, img_points_ir, mtx_rgb, dist_rgb, mtx_ir, dist_ir, gray_rgb.shape[::-1], flagscv2.CALIB_FIX_INTRINSIC # 如果内参已经单独标定好就固定住 )stereoCalibrate返回的R和T是从RGB相机坐标系到红外相机坐标系的变换。如果两个相机内参已经单独标定过用CALIB_FIX_INTRINSIC避免联合优化时把内参带偏。重投影误差如果超过1像素优先检查时间同步和角点对应顺序。4. 图像去畸变与平面直线算法什么时候该用哪种4.1 去畸变的两种实现与选择依据去畸变有两条路一是用initUndistortRectifyMap生成映射表再用remap做重映射二是直接用undistort一步到位。前者适合视频流因为映射表可以只算一次后续每帧只做查表后者适合单张图像代码更短。我一般会在视频里用映射表单张图用undistort。# 方法一单张图像直接去畸变 undistorted cv2.undistort(img, mtx, dist, None, mtx) # 方法二生成映射表适合视频流 h, w img.shape[:2] new_mtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) mapx, mapy cv2.initUndistortRectifyMap(mtx, dist, None, new_mtx, (w, h), 5) undistorted cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)getOptimalNewCameraMatrix的最后一个参数alpha控制保留多少边缘像素alpha0会裁掉所有无效像素alpha1保留全部但边缘会有黑边。我一般取alpha0.5做折中。注意new_mtx和原始mtx不同后续做PnP时要用new_mtx还是mtx取决于你是否在去畸变后的图像上提点。4.2 平面直线算法的适用场景与实现平面直线算法通常指在已知平面约束下用直线特征做标定或位姿估计。比如相机看一个带直线边缘的标定板可以用Hough变换提取直线再用直线交点作为虚拟角点。这种方法在棋盘格角点被遮挡时特别有用。实现上先用Canny做边缘检测再调HoughLinesP提直线最后求交点。edges cv2.Canny(gray, 50, 150) lines cv2.HoughLinesP(edges, 1, np.pi/180, threshold80, minLineLength50, maxLineGap10) # 对直线做聚类求两组主方向直线的交点作为虚拟角点threshold越小检测到的直线越多但误检也越多。minLineLength和maxLineGap要根据图像分辨率调一般设成图像宽度的1/10和1/50。平面直线算法的精度不如棋盘格角点但在无法使用标定板的场景下是有效的补充。4.3 去畸变和直线算法的先后顺序这里有个容易翻车的点如果你先提直线再去畸变直线在畸变图像里是弯的Hough变换会把它拆成多段。正确顺序是先对图像去畸变再在去畸变后的图像上提直线。如果必须在畸变图像上操作就要用畸变模型把直线参数也做变换复杂度高很多。我一般统一先去畸变再做后续所有2D处理。5. PnP与8点算法2D到3D坐标转换的两条路径5.1 PnP算法解决的是什么问题PnPPerspective-n-Point解决的是已知n个3D点和它们在图像上的2D投影求相机的位姿旋转向量rvec和平移向量tvec。在标定流程里PnP通常用在标定之后——你已经有了内参和畸变系数现在拿一个新的标定板姿态用solvePnP算相机相对于标定板的位置。OpenCV提供了SOLVEPNP_ITERATIVE、SOLVEPNP_EPNP、SOLVEPNP_P3P等多种解法我一般先用SOLVEPNP_ITERATIVE如果点数少于6个就换SOLVEPNP_EPNP。# 假设obj_points和img_points是当前帧的3D-2D对应点 ret, rvec, tvec cv2.solvePnP( obj_points, img_points, mtx, dist, flagscv2.SOLVEPNP_ITERATIVE ) # 把旋转向量转成旋转矩阵 R, _ cv2.Rodrigues(rvec) # 把3D点投影回图像验证 img_points_proj, _ cv2.projectPoints(obj_points, rvec, tvec, mtx, dist)solvePnP的输入3D点不能全部共面否则SOLVEPNP_ITERATIVE会退化。如果标定板是平面的至少要有4个不共线的点而且要用SOLVEPNP_IPPE或SOLVEPNP_IPPE_SQUARE这类专门处理平面场景的解法。重投影误差超过2像素时先检查3D点和2D点的对应顺序是否一致再检查内参是否用了去畸变后的新矩阵。5.2 8点算法的定位与实现细节8点算法是计算基础矩阵F的经典方法用于未标定相机的两视图几何。它和PnP不是替代关系PnP需要已知内参和3D点8点算法只需要两幅图像的2D对应点输出的是对极几何约束。如果你要做双目立体匹配或SFM8点算法是第一步。OpenCV里用findFundamentalMat方法选cv2.FM_8POINT。# pts1和pts2是两幅图像中的对应点形状为(N,2) F, mask cv2.findFundamentalMat(pts1, pts2, cv2.FM_8POINT, 1.0, 0.99) # 用基础矩阵计算对极线 lines1 cv2.computeCorrespondEpilines(pts2.reshape(-1,1,2), 2, F)8点算法对噪声非常敏感点数刚好8个时结果几乎不可用。我一般会提50个以上的匹配点用RANSAC做鲁棒估计把method换成cv2.FM_RANSACransacReprojThreshold设成1.0到3.0像素。如果匹配点里有大量外点先做特征匹配筛选再用8点算法。5.3 从2D像素到3D坐标的完整转换链把前面所有环节串起来先用RGB或红外相机标定拿到mtx和dist对图像去畸变然后在去畸变图像上提取2D点最后用PnP算位姿。如果你要的是世界坐标系下的3D点还需要知道相机在世界坐标系中的位置或者用多个视角做三角化。# 完整链路去畸变 - 提点 - PnP - 投影验证 undistorted cv2.undistort(img, mtx, dist, None, new_mtx) ret, corners cv2.findChessboardCorners(undistorted, pattern_size, None) if ret: corners cv2.cornerSubPix(undistorted, corners, (11,11), (-1,-1), criteria) ret, rvec, tvec cv2.solvePnP(objp, corners, new_mtx, None) # 此时rvec和tvec就是相机相对于标定板的位姿注意这里solvePnP的畸变系数传None因为图像已经去畸变过了。如果传了dist相当于二次去畸变结果会偏。这个细节很多人在第一次写的时候会搞错。6. 避坑与排查标定和坐标转换中最容易翻车的五个点6.1 重投影误差很小但实际投影偏得离谱现象calibrateCamera返回的重投影误差只有0.1像素但拿标定结果去投影新图像时角点位置偏了十几个像素。原因通常是标定图像采集时标定板只覆盖了图像中心区域主点cx、cy的估计不准而重投影误差是在训练集上算的过拟合了。解决方法是重新采集强制标定板出现在图像四角或者用CALIB_FIX_ASPECT_RATIO固定fx/fy比例减少自由度。6.2 红外标定角点提取失败现象findChessboardCorners在红外图像上一直返回False。原因是红外图像对比度低棋盘格黑白区域温差不够。解决方法是换用加热金属板或主动红外靶标或者在软件上先做直方图均衡化再把adaptiveThreshWinSizeMin从3调到10adaptiveThreshWinSizeMax从23调到50。如果还是不行改用圆点标定板加findCirclesGrid。6.3 PnP解算结果跳变现象连续帧的solvePnP输出的rvec、tvec跳动很大甚至符号翻转。原因是平面标定板的姿态接近退化配置比如标定板几乎正对相机此时SOLVEPNP_ITERATIVE的解不唯一。解决方法是换用SOLVEPNP_IPPE或者在采集时让标定板倾斜角度大于15度。另外如果2D点里有误匹配先用RANSAC剔除再解PnP。6.4 去畸变后图像边缘出现黑边现象用undistort或remap后图像边缘出现黑色区域后续提点时把黑边当成特征。原因是去畸变后有效像素范围缩小了。解决方法是调getOptimalNewCameraMatrix的alpha参数取0.5到0.8之间或者用roi裁剪掉黑边。如果做视频流映射表生成时就要把new_mtx和roi一起保存每帧裁剪。6.5 8点算法基础矩阵秩不为2现象findFundamentalMat返回的F矩阵秩是3不满足对极几何的秩2约束。原因是8点算法对噪声敏感点数太少或外点太多。解决方法是增加匹配点数到100以上用cv2.FM_RANSAC代替cv2.FM_8POINTransacReprojThreshold设成1.0到3.0。拿到F后可以用SVD强制把最小奇异值置0再重构F但这一步只在对精度要求极高时做。7. 进阶技巧用重投影误差热力图定位标定薄弱环节标定做完不是看一个总的重投影误差就完事了。我一般会把每张标定图像的每个角点的重投影误差单独算出来画成热力图这样能直接看出哪张图、哪个区域的标定质量差。具体做法是对每张图用projectPoints把3D点投影到图像上和实际提取的2D点求欧氏距离然后按图像索引和角点索引画二维热力图。import matplotlib.pyplot as plt errors np.zeros((len(obj_points), len(obj_points[0]))) for i in range(len(obj_points)): proj, _ cv2.projectPoints(obj_points[i], rvecs[i], tvecs[i], mtx, dist) errors[i] np.linalg.norm(img_points[i].reshape(-1,2) - proj.reshape(-1,2), axis1) plt.imshow(errors, aspectauto, cmaphot) plt.colorbar(label重投影误差 (像素)) plt.xlabel(角点索引) plt.ylabel(图像索引) plt.show()如果某一行整体偏红说明那张图的采集姿态有问题比如倾斜角度不够或者有运动模糊。如果某一列偏红说明那个角点在所有图像里都提不准可能是棋盘格打印质量差或者镜头边缘畸变太大。我一般会把误差大于1像素的角点直接剔除重新标定一次通常能把整体重投影误差从0.3降到0.15以下。另一个技巧是标定完成后用solvePnP加projectPoints做一次交叉验证——拿一张没有参与标定的图像提角点解PnP再投影回去。如果交叉验证的误差和训练误差接近说明标定没有过拟合如果交叉验证误差明显更大说明标定图像采集不够多样需要补拍。我自己的习惯是每次标定至少跑三遍第一遍全量数据第二遍剔除误差大于1像素的角点第三遍用交叉验证确认。三遍结果一致才把内参写进配置文件。这个流程多花十分钟但能避免后面调车或抓取时出现玄学偏移。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站