SLAM实战:如何用Python+OpenCV搞定相机标定(附完整代码)
PythonOpenCV相机标定实战从棋盘格检测到三维重建相机标定是计算机视觉和SLAM系统中的基础环节它决定了三维空间信息能否准确映射到二维图像。本文将带您用Python和OpenCV实现完整的相机标定流程包含棋盘格检测技巧、内外参矩阵计算等实战内容最后还会展示如何将这些参数应用于实际的三维重建任务。1. 准备工作与环境配置在开始标定前我们需要准备合适的硬件和软件环境。推荐使用分辨率为1920×1080以上的USB或网络摄像头避免使用手机摄像头因为其内置的自动校正会影响标定结果。安装必要的Python库pip install opencv-python numpy matplotlib准备标定板推荐使用A4纸打印的棋盘格棋盘格尺寸9×6内部角点数量每个方格边长30mm实际尺寸需精确测量使用哑光材质打印避免反光影响检测提示棋盘格角点数量是指内部交叉点不是方格数量。比如9×6的棋盘格实际有8×5个方格。2. 采集标定图像数据高质量的数据采集是标定成功的关键。我们需要从不同角度拍摄15-20张棋盘格照片覆盖相机视野的各个区域。import cv2 import os cap cv2.VideoCapture(0) save_dir calibration_images os.makedirs(save_dir, exist_okTrue) count 0 while count 20: ret, frame cap.read() if not ret: continue gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, (8,5), None) if ret: cv2.drawChessboardCorners(frame, (8,5), corners, ret) cv2.imshow(Frame, frame) key cv2.waitKey(500) # 显示0.5秒 if key ! ord(q): cv2.imwrite(f{save_dir}/calib_{count}.jpg, frame) count 1 else: cv2.imshow(Frame, frame) if cv2.waitKey(30) ord(q): break cap.release() cv2.destroyAllWindows()采集时需注意棋盘格应占据图像1/3到1/2的面积包含不同倾斜角度俯视、仰视、左倾、右倾确保棋盘格完整出现在画面中光照均匀避免强烈阴影3. 实现张氏标定法张氏标定法是当前最常用的相机标定方法它只需要相机从不同方向观测平面标定板如棋盘格即可完成标定。3.1 角点检测与优化def find_corners(images, pattern_size(8,5)): obj_points [] # 3D点世界坐标系 img_points [] # 2D点图像坐标系 # 准备物体点(0,0,0), (1,0,0), (2,0,0) ..., (7,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) 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: # 亚像素级精确化 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners2 cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria) img_points.append(corners2) obj_points.append(objp) return obj_points, img_points, gray.shape[::-1]3.2 计算相机参数def calibrate_camera(obj_points, img_points, img_size): ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, img_size, None, None) print(相机矩阵:\n, mtx) print(\n畸变系数:, dist.ravel()) return ret, mtx, dist, rvecs, tvecs相机标定的核心输出参数参数类型说明典型值示例相机矩阵 (mtx)包含焦距(fx,fy)和光学中心(cx,cy)[[fx,0,cx],[0,fy,cy],[0,0,1]]畸变系数 (dist)径向和切向畸变参数[k1,k2,p1,p2,k3]旋转向量 (rvecs)每张图像的旋转向量每个向量3元素平移向量 (tvecs)每张图像的平移向量每个向量3元素4. 评估标定结果质量标定完成后我们需要验证结果的准确性。常用的评估指标是重投影误差它反映了标定参数将3D点重新投影到2D图像上的精度。def evaluate_calibration(obj_points, img_points, mtx, dist, rvecs, tvecs): mean_error 0 for i in range(len(obj_points)): img_points2, _ cv2.projectPoints( obj_points[i], rvecs[i], tvecs[i], mtx, dist) error cv2.norm(img_points[i], img_points2, cv2.NORM_L2)/len(img_points2) mean_error error print(\n平均重投影误差: {:.2f}像素.format(mean_error/len(obj_points))) # 可视化一个示例的投影点 img cv2.imread(images[0]) img_points2, _ cv2.projectPoints( obj_points[0], rvecs[0], tvecs[0], mtx, dist) for i in range(len(img_points[0])): cv2.circle(img, tuple(img_points[0][i].ravel().astype(int)), 5, (0,255,0), -1) cv2.circle(img, tuple(img_points2[i].ravel().astype(int)), 5, (0,0,255), 2) cv2.imshow(Reprojection, img) cv2.waitKey(0) cv2.destroyAllWindows()良好的标定结果通常具有平均重投影误差 0.5像素不同图像的重投影误差波动小可视化检查时原始角点和重投影点基本重合5. 应用标定结果进行图像校正获得相机参数后我们可以校正图像的畸变并利用这些参数进行三维测量。5.1 图像去畸变def undistort_image(img, mtx, dist): h, w img.shape[:2] newcameramtx, roi cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h)) # 方法1使用cv2.undistort dst cv2.undistort(img, mtx, dist, None, newcameramtx) # 方法2使用remapping适合视频流处理 # mapx, mapy cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5) # dst cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR) # 裁剪图像 x, y, w, h roi dst dst[y:yh, x:xw] return dst5.2 三维测量示例利用标定参数我们可以从二维图像中恢复三维信息。以下是一个简单的测量物体高度的示例def measure_height(img, mtx, dist): # 假设我们已经通过某种方法获得了物体的底部和顶部在图像中的坐标 bottom_pixel (x1, y1) # 物体底部坐标 top_pixel (x2, y2) # 物体顶部坐标 # 假设物体底部位于Z0平面地面 # 构造物体底部的3D点假设我们知道物体底部在世界坐标系中的XY位置 bottom_3d np.array([[X, Y, 0]], dtypenp.float32) # 将3D点投影到图像平面验证准确性 projected, _ cv2.projectPoints(bottom_3d, np.zeros(3), np.zeros(3), mtx, dist) # 计算从相机中心到物体底部的射线方向 bottom_homogeneous np.linalg.inv(mtx) np.array([bottom_pixel[0], bottom_pixel[1], 1]) # 计算从相机中心到物体顶部的射线方向 top_homogeneous np.linalg.inv(mtx) np.array([top_pixel[0], top_pixel[1], 1]) # 假设我们知道相机高度H可以计算物体高度 H_camera 1.5 # 相机高度1.5米实际需要测量 object_height H_camera * (1 - bottom_homogeneous[1]/top_homogeneous[1]) return object_height6. 高级应用与技巧6.1 多相机系统标定对于立体视觉系统我们需要标定两个相机之间的相对位置关系def stereo_calibrate(obj_points, img_points1, img_points2, mtx1, dist1, mtx2, dist2, img_size): flags 0 flags | cv2.CALIB_FIX_INTRINSIC # 使用已标定的相机参数 ret, _, _, _, _, R, T, E, F cv2.stereoCalibrate( obj_points, img_points1, img_points2, mtx1, dist1, mtx2, dist2, img_size, flagsflags) print(旋转矩阵:\n, R) print(\n平移向量:, T) return R, T6.2 标定参数优化技巧温度补偿相机传感器会随温度变化产生微小形变对于高精度应用建议在不同温度下标定并建立补偿模型自动对焦处理如果相机有自动对焦功能需要对不同焦距分别标定非线性优化使用ceres-solver或g2o等工具对OpenCV的标定结果进行进一步优化# 使用scipy进行非线性优化的示例 from scipy.optimize import least_squares def residual(params, obj_points, img_points, n_images): # 解包参数前4个是内参接着是每张图像的外参 fx, fy, cx, cy params[:4] k1, k2, p1, p2, k3 params[4:9] mtx np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]]) dist np.array([k1, k2, p1, p2, k3]) errors [] for i in range(n_images): rvec params[9i*6:12i*6] tvec params[12i*6:15i*6] projected, _ cv2.projectPoints(obj_points[i], rvec, tvec, mtx, dist) errors.append((projected - img_points[i]).ravel()) return np.concatenate(errors) # 初始猜测使用OpenCV标定结果 x0 np.concatenate([mtx[0,0], mtx[1,1], mtx[0,2], mtx[1,2], dist.ravel(), np.concatenate(rvecs).ravel(), np.concatenate(tvecs).ravel()]) res least_squares(residual, x0, args(obj_points, img_points, len(obj_points)))7. 实际项目中的经验分享在工业应用中我们发现以下实践能显著提高标定精度和稳定性标定板制作使用铝板或玻璃基底制作标定板确保绝对平整棋盘格边缘清晰黑白对比度90%尺寸精度误差0.1mm数据采集采集50-100张图像进行标定包含极端视角如±60度倾斜覆盖整个工作空间标定验证在工作空间内放置已知尺寸的验证物体测量多个位置的长度、角度等几何量要求测量误差1/1000参数维护每次相机调整后重新标定定期如每月验证标定参数建立标定参数的历史记录一个常见的坑是忽略了镜头的景深影响。当工作距离变化较大时镜头的对焦变化会导致内参变化。解决方法是固定对焦或对不同工作距离分别标定。对于需要极高精度的应用可以考虑以下改进方案使用主动发光标定板如LED阵列控制环境温度恒定采用热像仪监测相机温度变化实现标定参数的在线自动更新