OpenCV相机标定与位姿估计实战:从棋盘格到三维空间理解
1. 项目概述:从棋盘格到三维世界的钥匙
在计算机视觉和机器人领域,让机器“看懂”世界的第一步,往往是教会它如何理解自己“眼睛”——也就是相机——看到的东西。一个未经标定的相机,就像一个人戴着度数不匹配的眼镜,看到的图像是扭曲的,测量的距离是失真的。你无法直接从一张照片里判断一个物体离你到底有多远,它的真实尺寸是多少。这就是相机标定要解决的核心问题:建立图像二维像素坐标与世界三维物理坐标之间的精确数学关系。
而“位姿估计”,则是这个关系的直接应用。它回答的是“目标物体相对于相机在哪里,姿态如何”这个问题。想象一下,一个机械臂要精准抓取工作台上的零件,它必须知道零件相对于自己摄像头坐标系的三维位置和旋转角度。这个项目,就是通过最经典、最实用的棋盘格标定法,带你一步步实现从相机内参、畸变系数标定,到利用单张棋盘格图像实时估算其相对于相机位姿的全过程。整个过程完全基于OpenCV-Python,代码清晰,原理透彻,无论是做机器人视觉引导、增强现实(AR)、三维重建,还是简单的视觉测量,这都是你必须掌握的基石技能。
2. 核心原理拆解:针孔模型、畸变与透视变换
在动手写代码之前,我们需要把背后的数学和物理模型搞清楚。这能让你在调试时心中有数,而不是盲目地调参。
2.1 相机成像的几何模型:小孔成像与坐标系转换
现代数码相机的原理源于小孔成像。OpenCV使用的也是这个模型的扩展——针孔相机模型。这个模型描述了三维空间点如何投影到二维图像平面的过程,涉及四次坐标系转换:
- 世界坐标系 (World Coordinate System):这是客观世界的绝对坐标系,单位是米或毫米。棋盘格角点的物理位置就在这个坐标系下定义(例如,我们设定棋盘格左上角为(0,0,0),每个格子宽高为30mm)。
- 相机坐标系 (Camera Coordinate System):原点在相机的光心,Z轴沿光轴方向指向场景。世界坐标系下的点需要通过旋转和平移(即我们最终要求解的外参)转换到此坐标系。
- 图像坐标系 (Image Coordinate System):单位是毫米,原点在光轴与成像平面的交点(主点)。相机坐标系下的点通过透视投影(与焦距f相关)落在这个平面上。
- 像素坐标系 (Pixel Coordinate System):单位是像素,原点通常在图像的左上角。图像坐标系下的点通过缩放(与像素尺寸dx, dy相关)和平移(主点cx, cy的像素坐标)得到最终的像素坐标。
这整个转换过程,用一个矩阵乘法就可以表示:s * [u, v, 1]^T = A * [R|t] * [X, Y, Z, 1]^T。
[u, v]是像素坐标。[X, Y, Z]是世界坐标。A是内参矩阵,包含了焦距fx, fy和主点cx, cy。它由相机自身的物理特性决定,标定的主要目标之一就是求出它。[R|t]是旋转矩阵和平移向量组成的外参矩阵,描述了世界坐标系到相机坐标系的变换。对于单张标定板图像,我们求的就是这一组特定的R和t。s是一个缩放因子。
2.2 镜头畸变:理想与现实的差距
针孔模型是理想的,但真实的镜头由于光学设计、组装工艺等因素,会引入畸变,导致直线变弯。主要考虑两种径向畸变和两种切向畸变:
- 径向畸变:图像像素点以畸变中心为原点,沿径向发生位移。
k1, k2, k3是它的系数。桶形畸变(负径向畸变)和枕形畸变(正径向畸变)都属于此类。 - 切向畸变:由于镜头制造时与成像平面不平行引起。
p1, p2是它的系数。
标定的另一个核心目标,就是求出这5个(或更多)畸变系数[k1, k2, p1, p2, k3]。OpenCV的cv2.calibrateCamera函数在优化过程中,会同时求解内参矩阵、畸变系数和每张图片的外参。
2.3 单应性矩阵与位姿估计:从2D到3D的桥梁
当我们用cv2.findChessboardCorners找到棋盘格的角点像素坐标[u, v],并且已知这些角点在世界坐标系下的平面坐标[X, Y, 0](因为棋盘格是平的,Z坐标全为0)时,它们之间的投影关系可以用一个3x3的单应性矩阵H来描述:s * [u, v, 1]^T = H * [X, Y, 1]^T。
这个H矩阵蕴含了丰富的信息。理论上,H = A * [r1, r2, t],其中r1, r2是旋转矩阵R的前两列。因此,一旦我们通过标定得到了准确的内参矩阵A,就可以从单应性矩阵H中分解出r1, r2, t,再利用r3 = r1 × r2(叉乘)得到旋转矩阵的第三列,最后通过奇异值分解(SVD)对[r1, r2, r3]进行正交化,得到最终的、最接近真实旋转的旋转矩阵R和平移向量t。OpenCV的cv2.solvePnP函数正是基于这个原理,提供了更鲁棒、更通用的求解方法(支持非平面点),它也是我们本项目后期进行实时位姿估计的核心函数。
注意:这里有一个关键点,
cv2.solvePnP需要已知内参。这就是为什么标定必须先于位姿估计进行。没有准确的内参,解出的位姿误差会非常大。
3. 环境准备与工具选型
工欲善其事,必先利其器。一个干净、版本匹配的环境能避免很多莫名其妙的错误。
3.1 Python与OpenCV版本选择
我强烈建议使用Python 3.8+的版本,这个版本区间生态稳定,兼容性好。OpenCV-Python(opencv-python)库有几个变体:
opencv-python:只包含主模块,最常用。opencv-contrib-python:包含主模块和贡献库(opencv_contrib),里面有很多额外的、好用的模块(如SIFT、SURF等专利算法,以及更丰富的标定工具)。对于本项目,如果你后续想探索更多功能,建议直接安装这个版本。opencv-python-headless:无GUI依赖的版本,适用于服务器或无显示环境。
安装命令非常简单:
pip install opencv-contrib-python -i https://pypi.tuna.tsinghua.edu.cn/simple使用国内的镜像源(如清华源)可以大幅加快下载速度。
3.2 开发环境与辅助工具
IDE/编辑器:VSCode是绝佳选择。轻量、插件生态强大。务必安装Python扩展和Pylance,它能提供优秀的代码提示和类型检查。配置好你的Python解释器路径,确保VSCode使用的是你安装了OpenCV的那个环境。
标定板:理论上你可以用任何已知尺寸的棋盘格图案。但为了精度,请注意:
- 打印时务必确保**“实际尺寸”**。如果你设计的是30mm格子,打印出来用尺子量一下,误差应小于0.5mm。使用高质量的激光打印机和哑光铜版纸打印效果较好。
- 棋盘格需要平整地贴在一个坚硬的底板上(如亚克力板、铝板)。拍摄时标定板的平整度对精度影响极大。
- 棋盘格的行列数(
patternSize)指的是内角点的数量。例如一个8x10的棋盘格,其内角点就是7x9。这是cv2.findChessboardCorners函数的关键参数,千万别搞错。
图像采集:使用你项目将要用的实际相机。手机摄像头、USB网络摄像头、工业相机均可。采集时,要像摄影师一样,让标定板“充满”整个相机的视野:
- 覆盖视野的各个位置(中心、四个角、四条边)。
- 覆盖各种姿态(平放、倾斜、旋转、远近)。
- 通常需要15-25张有效图像。图像越多、姿态越丰富,标定结果越鲁棒,但计算量也越大。
4. 相机标定全流程实操
现在,我们进入实战环节。我会把每一步的代码和背后的意图都讲清楚。
4.1 采集标定图像与角点检测
首先,我们需要写一个脚本或者手动拍摄一系列棋盘格照片,保存到一个文件夹(如calib_imgs/)中。然后,编写角点检测的代码。
import cv2 import numpy as np import glob # 1. 定义棋盘格规格(内角点数量) pattern_size = (7, 9) # 例如,棋盘格每行有8个格子,每列有10个格子,则内角点为7x9 square_size = 0.03 # 每个格子的实际物理尺寸,单位:米 (30mm) # 2. 准备世界坐标系中的对象点 (0,0,0), (1,0,0), (2,0,0) ....,(8,5,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 # 乘上物理尺寸 # 3. 用于存储所有图像的对象点和图像点的数组 objpoints = [] # 真实世界中的3D点 imgpoints = [] # 图像中的2D点 # 4. 读取所有标定图像 images = glob.glob('calib_imgs/*.jpg') if not images: print("未找到图像文件,请检查路径!") exit() for i, fname in enumerate(images): img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 5. 查找棋盘格角点 ret, corners = cv2.findChessboardCorners(gray, pattern_size, None) # 6. 如果找到,添加对象点和图像点(细化后) if ret: objpoints.append(objp) # 角点亚像素精细化,提高定位精度 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) imgpoints.append(corners_refined) # 可视化角点(可选,用于检查) cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret) cv2.imshow(f'Chessboard Corners {i}', img) cv2.waitKey(500) # 显示500毫秒 cv2.destroyWindow(f'Chessboard Corners {i}') else: print(f"警告:在图像 {fname} 中未找到棋盘格角点!") cv2.destroyAllWindows() print(f"成功处理了 {len(objpoints)} 张图像。")实操心得:
cv2.findChessboardCorners的成败很大程度上取决于图像质量、对比度和棋盘格的完整性。如果某些图片总是检测失败,可以尝试:
- 对图像进行直方图均衡化 (
cv2.equalizeHist) 来增强对比度。- 调整
findChessboardCorners的flags参数,例如尝试cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_NORMALIZE_IMAGE。- 手动检查图像,确保棋盘格没有被遮挡,且光照均匀。
4.2 执行标定与参数解读
收集到足够的objpoints和imgpoints后,就可以调用核心的标定函数了。
# 7. 执行相机标定 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) if ret: print("相机标定成功!") print("\n=== 内参矩阵 (mtx) ===") print(mtx) print("\n=== 畸变系数 (dist) ===") print(dist.ravel()) # 通常为5个参数 [k1, k2, p1, p2, k3] # 计算重投影误差,评估标定质量 mean_error = 0 for i in range(len(objpoints)): imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2) mean_error += error print(f"\n=== 平均重投影误差 ===\n{mean_error / len(objpoints):.6f} 像素") else: print("相机标定失败!")参数解读与质量评估:
ret: 标定是否成功的布尔值。mtx: 内参矩阵。一个3x3的矩阵,[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]。fx, fy是以像素为单位的焦距,cx, cy是主点坐标(通常接近图像中心)。dist: 畸变系数向量。通常是5个或更多。[k1, k2, p1, p2, k3]。rvecs,tvecs: 每张标定图像对应的旋转向量和平移向量(外参)。- 重投影误差:这是衡量标定精度的黄金标准。它计算的是检测到的角点位置与用标定参数重新投影回去的理论位置之间的平均像素距离。这个值通常应小于0.5像素,越接近0越好。如果误差大于1像素,说明标定质量不佳,需要检查标定板平整度、角点检测精度或图像数量/姿态。
4.3 标定结果验证与图像去畸变
标定完了,怎么知道对不对?最直观的方法就是看去畸变后的图像,直线是否变直了。
# 读取一张测试图像(可以是标定集之外的) test_img = cv2.imread('test_image.jpg') h, w = test_img.shape[:2] # 优化内参矩阵和 ROI (Region of Interest) newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) # 方法1:使用 cv2.undistort 去畸变 dst = cv2.undistort(test_img, mtx, dist, None, newcameramtx) # 方法2:使用 remapping(对于视频流更高效) mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w, h), 5) dst_remap = cv2.remap(test_img, mapx, mapy, cv2.INTER_LINEAR) # 裁剪掉 ROI 之外的黑边 x, y, w_roi, h_roi = roi dst_cropped = dst[y:y+h_roi, x:x+w_roi] # 并排显示原图和去畸变图 cv2.imshow('Original', test_img) cv2.imshow('Undistorted', dst) cv2.imshow('Undistorted and Cropped', dst_cropped) cv2.waitKey(0) cv2.destroyAllWindows() # 保存标定参数,供后续使用 np.savez('camera_calib_params.npz', mtx=mtx, dist=dist, newcameramtx=newcameramtx, roi=roi) print("标定参数已保存至 'camera_calib_params.npz'")注意事项:
cv2.getOptimalNewCameraMatrix中的最后一个参数alpha控制裁剪量。alpha=0会裁剪掉所有无效像素(无黑边),但会损失部分视野;alpha=1保留所有原始像素(可能有黑边),视野无损。通常取0.5-1之间的值进行权衡。观察去畸变后图像边缘的直线(如门窗、桌面边缘)是否恢复笔直,是验证畸变系数有效性的好方法。
5. 基于单张图像的棋盘格位姿估计
有了精确的内参和畸变系数,我们现在可以“读懂”单张图片中棋盘格的空间位置了。这是AR叠加、机械臂抓取等应用的关键一步。
5.1 实时视频流中的位姿解算
我们将编写一个程序,打开摄像头,实时检测棋盘格并估算其位姿,同时将坐标系绘制在图像上。
import cv2 import numpy as np # 1. 加载之前保存的标定参数 calib_data = np.load('camera_calib_params.npz') mtx = calib_data['mtx'] dist = calib_data['dist'] # 可以使用 newcameramtx 以获得更好的去畸变效果,这里为简化直接用 mtx # newcameramtx = calib_data['newcameramtx'] # 2. 定义棋盘格规格(必须与标定时一致!) pattern_size = (7, 9) square_size = 0.03 # 单位:米 # 3. 准备世界坐标系下的棋盘格角点3D坐标 (Z=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 # 4. 定义用于绘制的3D坐标系轴点 (长度约为3个格子) axis = np.float32([[3*square_size, 0, 0], # X轴:红色 [0, 3*square_size, 0], # Y轴:绿色 [0, 0, -3*square_size]]).reshape(-1, 3) # Z轴:蓝色 (指向相机外) # 5. 打开摄像头 cap = cv2.VideoCapture(0) if not cap.isOpened(): print("无法打开摄像头") exit() while True: ret, frame = cap.read() if not ret: print("无法获取帧") break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 查找棋盘格角点 ret_corners, corners = cv2.findChessboardCorners(gray, pattern_size, None) if ret_corners: # 角点亚像素精细化 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) # 核心:使用 solvePnP 求解旋转向量和平移向量 # 参数:对象点,图像点,内参,畸变系数,初始解(可选),使用迭代法 ret_pnp, rvec, tvec = cv2.solvePnP(objp, corners_refined, mtx, dist, flags=cv2.SOLVEPNP_ITERATIVE) if ret_pnp: # 将旋转向量转换为旋转矩阵(可选,用于更直观的理解) R, _ = cv2.Rodrigues(rvec) # print(f"Rotation Matrix:\n{R}") # print(f"Translation Vector (meters):\n{tvec}") # 将3D坐标系轴点投影到2D图像平面 imgpts, _ = cv2.projectPoints(axis, rvec, tvec, mtx, dist) # 在图像上绘制坐标系 origin = tuple(corners_refined[0].ravel().astype(int)) # 以第一个角点为原点 img = cv2.line(frame, origin, tuple(imgpts[0].ravel().astype(int)), (0, 0, 255), 5) # X - 红 img = cv2.line(frame, origin, tuple(imgpts[1].ravel().astype(int)), (0, 255, 0), 5) # Y - 绿 img = cv2.line(frame, origin, tuple(imgpts[2].ravel().astype(int)), (255, 0, 0), 5) # Z - 蓝 # 可选:在图像上显示位姿数值 cv2.putText(frame, f"tvec (m): [{tvec[0][0]:.2f}, {tvec[1][0]:.2f}, {tvec[2][0]:.2f}]", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) # 也可以计算欧拉角(需要从旋转矩阵R转换) # ... # 绘制棋盘格角点(可选) cv2.drawChessboardCorners(frame, pattern_size, corners_refined, ret_corners) cv2.imshow('Real-time Pose Estimation', frame) # 按 'q' 键退出 if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()5.2 位姿结果的物理意义与应用
运行上面的代码,你会看到一个三维坐标系被“钉”在了棋盘格上。
- 红色轴 (X):通常指向棋盘格的右侧。
- 绿色轴 (Y):通常指向棋盘格的下方。
- 蓝色轴 (Z):遵循右手定则,垂直于棋盘格平面指向相机(注意我们在定义
axis时Z值为负,所以画出来是指向相机外的,这更符合“物体指向观察者”的直觉)。
tvec向量的三个分量[tx, ty, tz]就是棋盘格坐标系原点(我们设定的第一个角点)在相机坐标系下的坐标,单位是米。tz就是棋盘格到相机的距离。这是非常有用的信息!
应用场景举例:
- 增强现实 (AR):知道了棋盘格(作为标记物)的精确位姿,就可以将虚拟的3D模型准确地“放置”在棋盘格所在的位置,并随其移动。
- 机器人抓取:如果棋盘格贴在目标物体上,机械臂的视觉系统通过解算出的
tvec和R,可以计算出工具末端需要移动到的位置和姿态。 - 视觉测量:通过测量图像中已知尺寸物体的像素距离,结合内参和位姿,可以反推其他物体的实际尺寸。
核心技巧:
cv2.solvePnP有多个求解算法(通过flags指定)。SOLVEPNP_ITERATIVE是基于Levenberg-Marquardt优化的迭代法,最常用也最稳定。对于平面目标(如棋盘格),SOLVEPNP_IPPE(无限平面姿态估计)速度更快且精度高,是较新的推荐选项。如果解算的位姿跳动很大,可以尝试使用SOLVEPNP_EPNP或SOLVEPNP_DLS,并结合RANSAC (cv2.solvePnPRansac) 来剔除错误的角点匹配,提高鲁棒性。
6. 精度提升与高级技巧
基础功能实现后,我们总希望它更准、更稳。下面分享几个提升精度的关键点。
6.1 标定阶段的精度陷阱与规避
- 标定板质量与平整度:这是最大的误差来源。打印的纸张会弯曲。务必使用刚性底板(如光学平板、厚亚克力板)并确保粘贴平整。有条件可以使用出厂标定的陶瓷或玻璃标定板。
- 角点检测精度:
cv2.cornerSubPix是必须的。它的窗口大小(11,11)需要根据图像中角点区域的对比度调整。对比度低则增大窗口,但计算量也增大。 - 图像数量与姿态分布:至少需要10-15张不同姿态的图像。姿态要尽可能覆盖相机视野的各个角落和倾斜角度,避免所有图像都集中在一个很小的姿态范围内。远近也要有变化。
- 光照均匀性:避免强光直射产生反光,也避免阴影覆盖部分角点。均匀的漫射光是最理想的。
- 重投影误差分析:标定完成后,逐一检查每张图像的重投影误差。如果某一张图像的误差远高于平均值,很可能是该图像角点检测有误或标定板不平整,应考虑剔除这张图像后重新标定。
6.2 位姿估计的稳定性优化
使用
solvePnPRansac:在实时场景中,cv2.findChessboardCorners偶尔会检测到错误的角点或受到噪声干扰。使用RANSAC版本的PnP求解器可以自动剔除局外点,得到更稳定、更抗干扰的位姿结果。ret_pnp, rvec, tvec, inliers = cv2.solvePnPRansac( objp, corners_refined, mtx, dist, iterationsCount=100, reprojectionError=8.0, confidence=0.99 )参数
reprojectionError是判定内点的阈值(单位像素),根据你的精度要求调整。inliers输出的是被认为是内点的角点索引列表。滤波与平滑:对于视频流,直接使用单帧解算的位姿可能会抖动。可以采用简单的滑动平均滤波(Moving Average)或卡尔曼滤波(Kalman Filter)对
rvec和tvec进行时序上的平滑。OpenCV也提供了cv2.KalmanFilter类可供使用。多帧融合:对于静态场景,可以连续采集多帧,分别计算位姿后取平均值或中位数,能有效抑制随机噪声。
坐标系定义一致性:确保你定义的世界坐标系(
objp)和绘制的3D轴(axis)与你的应用逻辑一致。例如,在机器人学中,通常定义Z轴向前,X轴向左,Y轴向上。混乱的坐标系定义会导致后续应用层出现难以调试的错误。
7. 常见问题排查与解决实录
在实际操作中,你几乎一定会遇到下面这些问题。这里是我的踩坑记录和解决方案。
7.1 角点检测失败或不准
- 现象:
cv2.findChessboardCorners经常返回False,或者检测到的角点位置明显偏移。 - 排查:
- 检查
pattern_size:这是最常出错的地方!务必数清楚内角点(棋盘格内部黑白方块的交点)的行数和列数。 - 检查图像清晰度和对比度:图像是否模糊?棋盘格黑白是否分明?尝试对图像进行高斯模糊去噪 (
cv2.GaussianBlur) 或对比度拉伸。 - 调整检测参数:使用
flags参数。cv2.CALIB_CB_ADAPTIVE_THRESH使用自适应阈值,对光照不均更有效。cv2.CALIB_CB_NORMALIZE_IMAGE在阈值化前先归一化图像亮度。 - 手动验证:用画图工具打开图片,放大后数一数角点,确认棋盘格是否完整出现在视野中。
- 检查
7.2 标定误差过大(重投影误差 > 1像素)
- 现象:标定程序能跑通,但输出的平均重投影误差很高。
- 排查:
- 检查标定板物理尺寸:用游标卡尺精确测量打印的棋盘格格子尺寸,确保与代码中
square_size完全一致。1%的尺寸误差可能导致数像素的重投影误差。 - 检查标定板平整度:这是隐形杀手。将标定板放在绝对平整的桌面上拍摄一组,对比手持拍摄的误差,立刻见分晓。
- 检查图像集合:剔除那些角点检测看起来就不准的、或者标定板严重倾斜(透视变形极大)的图片。质量优于数量。
- 尝试不同标定算法:
cv2.calibrateCamera默认使用CALIB_FIX_PRINCIPAL_POINT等标志。对于主流相机,可以尝试不固定主点,让算法优化所有参数:flags=cv2.CALIB_ZERO_TANGENT_DIST + cv2.CALIB_RATIONAL_MODEL。但要注意,增加自由度可能在小数据集上导致过拟合。
- 检查标定板物理尺寸:用游标卡尺精确测量打印的棋盘格格子尺寸,确保与代码中
7.3 位姿估计结果抖动或明显错误
- 现象:实时显示时,坐标系乱跳,或者
tz(距离)明显不合理(如几米或负值)。 - 排查:
- 确认内参正确加载:最基础的错误。打印出加载的
mtx和dist,确认它们不是全零或默认值。 - 检查
solvePnP的输入:确保传入的objp和检测到的corners顺序一一对应,且数量一致。cv2.findChessboardCorners返回的角点顺序是确定的,通常从左上角开始,按行排列。 - 验证角点顺序:在图像上绘制角点序号,观察其顺序是否与世界点定义顺序匹配。
for i, corner in enumerate(corners_refined): pos = tuple(corner.ravel().astype(int)) cv2.putText(frame, str(i), pos, cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 0), 1) - 单位一致性:确保
square_size的单位(米)与你的应用场景期望的单位一致。tvec的输出单位与此相同。 - 使用
solvePnPRansac:如前所述,它能有效抵抗个别错误角点的干扰。 - 检查相机镜头是否对焦清晰:失焦的模糊图像会极大降低角点定位精度。
- 确认内参正确加载:最基础的错误。打印出加载的
7.4 去畸变后图像出现黑边或严重变形
- 现象:调用
cv2.undistort后,图像四周有黑色区域,或者中心物体被拉伸变形。 - 排查:
- 理解
getOptimalNewCameraMatrix:黑边是因为畸变校正后,有效图像区域变成了一个不规则的四边形,映射回矩形时,四角没有像素信息。使用roi参数可以裁剪掉这些黑边。变形则可能是畸变系数k3等高阶项标定不准确,可以尝试在标定时只使用k1, k2(flags=cv2.CALIB_FIX_K3)。 - 畸变模型是否合适:对于鱼眼镜头或超广角镜头,标准的布朗畸变模型可能不适用,需要使用
cv2.fisheye模块进行鱼眼相机标定。
- 理解
这个从相机标定到位姿估计的完整流程,是开启任何严肃的计算机视觉项目的大门。它没有太多炫酷的深度学习模型,但却是所有上层应用赖以稳定的基石。我自己的经验是,花在精心制作标定板和采集高质量数据上的时间,会在后期节省数倍的调试时间。当你看到那个小小的坐标系稳稳地“坐”在棋盘格上,随着你的移动而精准变化时,你就掌握了让机器理解空间关系的第一把钥匙。