保姆级教程:用Python+OpenCV一步步实现双目相机标定与三维重建(附完整代码)
从标定到点云PythonOpenCV双目视觉三维重建实战指南当你第一次看到双目相机生成的密集点云时那种将二维图像转化为三维世界的震撼感是单目视觉无法比拟的。本教程将手把手带你用Python和OpenCV实现这个魔法——从棋盘格标定到实时三维重建每个步骤都配有可运行的代码片段和可视化调试技巧。1. 环境搭建与硬件准备在开始编码前我们需要确保开发环境配置正确。推荐使用Python 3.8和OpenCV 4.5版本这些版本对双目视觉的支持最为完善pip install opencv-contrib-python4.5.5.64 numpy matplotlib scipy硬件方面ZED 2i或Intel RealSense D435这类消费级双目相机都是不错的选择。它们的主要参数对比如下参数ZED 2iRealSense D435基线长度120mm50mm最大分辨率3840×108030fps1280×72030fps深度范围0.3-20m0.11-10m内置IMU有无提示基线长度越大远距离测量精度越高但会减小最小工作距离。室内场景建议选择基线较短的设备。2. 双目相机标定全流程标定是双目视觉中最关键的步骤直接决定后续三维重建的精度。我们将使用经典的棋盘格法但会加入几个实战技巧提升标定质量。2.1 标定板拍摄要点准备一个8×6的棋盘格标定板每个方格建议2-3cm边长拍摄时注意左右相机同步采集使用cv2.VideoCapture的grab()和retrieve()方法标定板应出现在画面不同位置角落、中心、倾斜等姿态至少采集20组有效图像覆盖整个视场避免强光反射和运动模糊import cv2 import glob def capture_calibration_images(left_cam, right_cam, save_dir): left_dir os.path.join(save_dir, left) right_dir os.path.join(save_dir, right) os.makedirs(left_dir, exist_okTrue) os.makedirs(right_dir, exist_okTrue) count 0 while True: # 同步获取帧 left_ret, left_frame left_cam.read() right_ret, right_frame right_cam.read() # 显示并保存 cv2.imshow(Left, left_frame) cv2.imshow(Right, right_frame) key cv2.waitKey(1) if key ord(s): # 按s保存 left_path os.path.join(left_dir, fleft_{count:03d}.png) right_path os.path.join(right_dir, fright_{count:03d}.png) cv2.imwrite(left_path, left_frame) cv2.imwrite(right_path, right_frame) count 1 elif key 27: # ESC退出 break2.2 标定参数计算OpenCV提供了stereoCalibrate()函数但需要先准备好对象点和图像点def stereo_calibrate(left_dir, right_dir, pattern_size(7,6), square_size0.025): # 准备对象点 (0,0,0), (1,0,0), ..., (6,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) * square_size # 检测角点 left_points [] right_points [] obj_points [] left_images sorted(glob.glob(f{left_dir}/*.png)) right_images sorted(glob.glob(f{right_dir}/*.png)) for left_path, right_path in zip(left_images, right_images): left_img cv2.imread(left_path) right_img cv2.imread(right_path) gray_left cv2.cvtColor(left_img, cv2.COLOR_BGR2GRAY) gray_right cv2.cvtColor(right_img, cv2.COLOR_BGR2GRAY) # 查找角点 ret_left, corners_left cv2.findChessboardCorners(gray_left, pattern_size, None) ret_right, corners_right cv2.findChessboardCorners(gray_right, pattern_size, None) if ret_left and ret_right: # 亚像素精确化 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_left cv2.cornerSubPix(gray_left, corners_left, (11,11), (-1,-1), criteria) corners_right cv2.cornerSubPix(gray_right, corners_right, (11,11), (-1,-1), criteria) left_points.append(corners_left) right_points.append(corners_right) obj_points.append(objp) # 双目标定 ret, K1, D1, K2, D2, R, T, E, F cv2.stereoCalibrate( obj_points, left_points, right_points, None, None, None, None, gray_left.shape[::-1], flagscv2.CALIB_FIX_INTRINSIC) return ret, K1, D1, K2, D2, R, T注意标定误差应小于0.5像素否则需要检查标定板拍摄质量或重新采集数据。3. 图像校正与立体匹配获得标定参数后我们需要校正图像使其行对齐这是高效立体匹配的前提。3.1 Bouguet极线校正def stereo_rectify(K1, D1, K2, D2, R, T, image_size): # 计算校正变换 R1, R2, P1, P2, Q, _, _ cv2.stereoRectify( K1, D1, K2, D2, image_size, R, T, flagscv2.CALIB_ZERO_DISPARITY, alpha0.9) # 生成映射表 left_mapx, left_mapy cv2.initUndistortRectifyMap( K1, D1, R1, P1, image_size, cv2.CV_32FC1) right_mapx, right_mapy cv2.initUndistortRectifyMap( K2, D2, R2, P2, image_size, cv2.CV_32FC1) return left_mapx, left_mapy, right_mapx, right_mapy, Q校正后的图像应该满足左右图像行对齐保留最大有效区域alpha参数控制无明显畸变残留3.2 立体匹配与视差计算OpenCV提供了几种立体匹配算法我们比较它们的性能和效果算法类型速度精度适用场景BM快低实时应用SGBM中高一般用途ELAS慢最高高精度需求def create_disparity_map(left_img, right_img, methodSGBM): gray_left cv2.cvtColor(left_img, cv2.COLOR_BGR2GRAY) gray_right cv2.cvtColor(right_img, cv2.COLOR_BGR2GRAY) if method BM: stereo cv2.StereoBM_create(numDisparities64, blockSize15) elif method SGBM: stereo cv2.StereoSGBM_create( minDisparity0, numDisparities64, blockSize5, P18*3*5**2, P232*3*5**2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32 ) disparity stereo.compute(gray_left, gray_right).astype(np.float32)/16.0 return disparity4. 三维点云生成与可视化最后一步是将视差图转换为三维点云这是最令人兴奋的部分。4.1 视差转深度def disparity_to_depth(disparity, Q): points_3d cv2.reprojectImageTo3D(disparity, Q) mask (disparity disparity.min()) return points_3d[mask], mask4.2 点云可视化使用Matplotlib进行简单可视化def visualize_point_cloud(points, colorsNone): fig plt.figure(figsize(10, 8)) ax fig.add_subplot(111, projection3d) if colors is not None: ax.scatter(points[:,0], points[:,1], points[:,2], ccolors.reshape(-1,3)/255.0, s1) else: ax.scatter(points[:,0], points[:,1], points[:,2], s1, cmapviridis) ax.set_xlabel(X) ax.set_ylabel(Y) ax.set_zlabel(Z) plt.show()完整流程整合示例# 标定参数加载 K1 np.load(calibration/K1.npy) D1 np.load(calibration/D1.npy) K2 np.load(calibration/K2.npy) D2 np.load(calibration/D2.npy) R np.load(calibration/R.npy) T np.load(calibration/T.npy) # 初始化相机 left_cam cv2.VideoCapture(0) right_cam cv2.VideoCapture(1) # 校正映射 left_mapx, left_mapy, right_mapx, right_mapy, Q stereo_rectify( K1, D1, K2, D2, R, T, (640, 480)) while True: # 捕获帧 _, left_frame left_cam.read() _, right_frame right_cam.read() # 校正图像 left_rect cv2.remap(left_frame, left_mapx, left_mapy, cv2.INTER_LINEAR) right_rect cv2.remap(right_frame, right_mapx, right_mapy, cv2.INTER_LINEAR) # 计算视差 disparity create_disparity_map(left_rect, right_rect) # 生成点云 points_3d, mask disparity_to_depth(disparity, Q) colors left_rect[mask] # 可视化 if cv2.waitKey(1) ord(v): visualize_point_cloud(points_3d, colors) # 显示视差图 cv2.imshow(Disparity, (disparity-disparity.min())/(disparity.max()-disparity.min())) if cv2.waitKey(1) 27: break在实际项目中我发现SGBM算法在大多数场景下都能取得不错的平衡。对于动态场景可以考虑使用CUDA加速的版本如cv2.cuda.StereoSGBM_create来提升处理速度。