简介一套基于Python的双目立体视觉与三维重建完整项目代码主要面向毕业设计、期末大作业及课程设计场景适合正在学习计算机视觉、数字图像处理或需要快速搭建三维重建系统的本科生与研究生。压缩包为zip格式共25个文件包含14个Python源码脚本涉及相机标定、立体匹配、深度图生成、滤波与空洞填充等核心模块、3个演示视频、5个XML配置文件以及1份项目说明文档整体大小33.78MB部署轻量、易上手。目前已有285人学习下载项目代码写有详细注释新手依照说明即可理解关键逻辑并成功运行。资源内包含各类工具脚本如视频与图片互转、深度图后处理覆盖从图像采集到三维点云输出的完整流程演示录屏则直观展示中间结果方便与算法运行情况对照。对于需要冲刺高分毕业设计或课程项目的同学这套结构清晰、可直接复用的项目能提供从原理到落地的有力支撑且因经过导师认可的98分评价具有较强的参考价值。1. 双目立体视觉到底在做什么一个毕业设计项目的真实分量打开这个标题的读者大概率是两种状态要么导师丢给你一句“做双目重建也行”要么你刚从某个代码包解压出一套“基于Python的双目立体视觉及三维重建项目”却跑不出演示效果。这个项目的本质是用两个普通摄像头模拟人眼靠同一时刻左右两幅图里同一个点的位置差视差反推深度最后生成三维点云。它解决的核心问题只有一个单目图像丢了深度信息双目用几何关系把它捡回来。整条技术链包含双目标定、极线校正、立体匹配、三角测量四步适合做毕业设计、期末大作业或者想从二维图像跨进3D视觉的自学者。但要注意这是一条链路而不是几个独立脚本——任何一步出错点云都是废的。2. 双目标定棋盘格数据决定整个重建的下限2.1 为什么单目标定不够双目有自己的几何关系单目标定得到内参矩阵和畸变系数能把一张图去畸变但完全不知道两个相机在空间里的相对位置。双目重建需要的是左相机坐标系到右相机坐标系的旋转矩阵R和平移向量T。有了R和T才能做极线校正让同一个空间点在左右视图里的投影落在同一水平线上立体匹配才能从二维搜索降成一维搜索。这一环节的产出是左右内参、畸变系数、R、T、本征矩阵E和基础矩阵F。R和T是后续所有计算的起点也是整个项目里最不能含糊的参数。标定质量直接决定重建精度。我见过有人用网上随便下的图片集来标重投影误差跑到1.2像素后面视差图全是麻子还以为是SGBM参数没调好。记住一句话标定是地基地基歪了上面刷多少层漆都没用。一般要求双目标定重投影误差在0.5像素以内超过这个数就该回到采集环节找原因而不是继续往下走。2.2 一个能直接跑的棋盘格标定脚本import cv2 import numpy as np import glob # 棋盘格内角点数不是格子数 CHESS_COLS 9 # 内角点列数 CHESS_ROWS 6 # 内角点行数 # 每个格子的实际边长单位mm按你的棋盘实际尺寸填 SQUARE_SIZE 25.0 # 准备世界坐标系中的角点坐标z0平面 objp np.zeros((CHESS_COLS * CHESS_ROWS, 3), np.float32) objp[:, :2] np.mgrid[0:CHESS_COLS, 0:CHESS_ROWS].T.reshape(-1, 2) objp * SQUARE_SIZE # 存放左/右相机各自的世界点和像素点 objpoints_L, imgpoints_L [], [] objpoints_R, imgpoints_R [], [] # 图片命名约定left_001.jpg / right_001.jpg放在 ./calib_imgs/ left_images sorted(glob.glob(./calib_imgs/left_*.jpg)) right_images sorted(glob.glob(./calib_imgs/right_*.jpg)) # 确保左右图片数量一致且顺序对应 assert len(left_images) len(right_images) and len(left_images) 15, \ 左右标定图必须成对且至少15对 for lpath, rpath in zip(left_images, right_images): limg cv2.imread(lpath) rimg cv2.imread(rpath) grayL cv2.cvtColor(limg, cv2.COLOR_BGR2GRAY) grayR cv2.cvtColor(rimg, cv2.COLOR_BGR2GRAY) foundL, cornersL cv2.findChessboardCorners(grayL, (CHESS_COLS, CHESS_ROWS), None) foundR, cornersR cv2.findChessboardCorners(grayR, (CHESS_COLS, CHESS_ROWS), None) if foundL and foundR: # 亚像素精化提高角点定位精度 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) cornersL cv2.cornerSubPix(grayL, cornersL, (11, 11), (-1, -1), criteria) cornersR cv2.cornerSubPix(grayR, cornersR, (11, 11), (-1, -1), criteria) objpoints_L.append(objp) objpoints_R.append(objp) imgpoints_L.append(cornersL) imgpoints_R.append(cornersR) # 单目标定 retL, K_L, distL, rvecsL, tvecsL cv2.calibrateCamera( objpoints_L, imgpoints_L, grayL.shape[::-1], None, None) retR, K_R, distR, rvecsR, tvecsR cv2.calibrateCamera( objpoints_R, imgpoints_R, grayR.shape[::-1], None, None) # 双目标定核心一步 retS, K_L, distL, K_R, distR, R, T, E, F cv2.stereoCalibrate( objpoints_L, imgpoints_L, imgpoints_R, K_L, distL, K_R, distR, grayL.shape[::-1], flagscv2.CALIB_FIX_INTRINSIC) print(双目标定重投影误差: {:.3f} 像素.format(retS)) np.savez(./stereo_calib.npz, K_LK_L, distLdistL, K_RK_R, distRdistR, RR, TT, EE, FF)逻辑说明脚本的核心是最后的cv2.stereoCalibrate它把左右相机的内参当作已知量通过CALIB_FIX_INTRINSIC标志只优化R、T、E、F。前面的findChessboardCorners负责在每张图里找内角点cornerSubPix做亚像素精化能把角点定位误差从约1像素压到0.1像素级别。左右图是成对处理的任一检测不到角点就整对跳过。参数说明CHESS_COLS和CHESS_ROWS是内角点数量不是棋盘格子的数量。一张9×6的棋盘格子图内角点数量是8×5买棋盘时说明书上写的通常是格子数要自己减1。SQUARE_SIZE必须按实际尺寸填——它决定整个重建点云的尺度单位填25表示最后点云坐标单位是毫米。flagscv2.CALIB_FIX_INTRINSIC的前提是单目标定质量足够好如果单目重投影误差都超过0.5像素了就去掉这个flag让双目标定同时优化内参。2.3 标定图像的采集姿势宁可慢不能糊常见做法是打印一张A4棋盘格贴在硬纸板上两个摄像头同时拍照。注意三点第一画面里棋盘格要占1/3以上太小了角点检测不稳定第二角度要有变化——正对、左右倾斜、上下倾斜、远近各拍几张千万别只在一个平面上平移第三左右图像必须严格同步采集手工按快门也要保证棋盘不动。拍20到30对比较稳少于15对基本没得救。最后检查一遍所有图片里棋盘格的角点都必须完整可见有一边出画面就删掉这对。我自己的习惯是采集后用一段小循环快速预览每对图像的角点检测结果把检测失败的自动归到bad目录里。这一步看着笨实际能省掉后面两个小时的调试时间。另外提醒一下新配置的Python环境安装OpenCV后先跑一句import cv2验证装好了再开始标定别等到出图了才发现import直接报错——这种环境问题最容易在开始阶段消耗耐心。3. 极线校正与SGBM立体匹配把像素对应问题变成一维搜索3.1 极线校正左右图像必须严格行对齐拿到标定得到的R和T还不能直接做匹配。两个摄像头很难做到光轴完全平行左右图像的同一物点往往不在同一水平线上甚至有不小的旋转。极线校正stereoRectify做的事是把左右图像重投影到共同的理想平面上让行严格对齐——同一个物点在左图和右图只相差水平位移这个水平位移就是视差。没有这一步立体匹配得在二维平面上做搜索计算量爆炸结果噪声极大。一个很直观的验证方法是校正完成后在左图上画几条水平线右图对应的特征应该落在同一水平线上。你可以在第4章点云生成前先做这个检查。极线校正需要三步stereoRectify计算校正参数initUndistortRectifyMap生成重映射表最后用cv2.remap按表重采样图像。我一般把这部分封装成一个小类和后面的SGBM串成流水线方便反复调用。3.2 极线校正与视差计算的完整代码import cv2 import numpy as np # 读取标定结果 calib np.load(./stereo_calib.npz) K_L calib[K_L]; distL calib[distL] K_R calib[K_R]; distR calib[distR] R calib[R]; T calib[T] img_size (640, 480) # 和实际图像尺寸一致 # 1. 极线校正得到校正后的内参和投影矩阵 RL, RR, PL, PR, Q, validRoiL, validRoiR cv2.stereoRectify( K_L, distL, K_R, distR, img_size, R, T, alpha0, flagscv2.CALIB_ZERO_DISPARITY) # 2. 生成重映射表用remap比warpPerspective快且支持浮点坐标 mapL1, mapL2 cv2.initUndistortRectifyMap( K_L, distL, RL, PL, img_size, cv2.CV_32FC1) mapR1, mapR2 cv2.initUndistortRectifyMap( K_R, distR, RR, PR, img_size, cv2.CV_32FC1) # 3. 读入左右图像并校正 imgL cv2.imread(./test/left.png) imgR cv2.imread(./test/right.png) grayL cv2.cvtColor(imgL, cv2.COLOR_BGR2GRAY) grayR cv2.cvtColor(imgR, cv2.COLOR_BGR2GRAY) rectL cv2.remap(grayL, mapL1, mapL2, cv2.INTER_LINEAR) rectR cv2.remap(grayR, mapR1, mapR2, cv2.INTER_LINEAR) # 4. SGBM先给一组能出结果的参数 min_disp 0 num_disp 80 # 必须是16的倍数 block_size 11 # 必须是奇数一般5~15 sgbm cv2.StereoSGBM_create( minDisparitymin_disp, numDisparitiesnum_disp, blockSizeblock_size, P18 * 3 * block_size ** 2, P232 * 3 * block_size ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32, modecv2.STEREO_SGBM_MODE_SGBM) disparity sgbm.compute(rectL, rectR).astype(np.float32) / 16.0 # 5. 保存视差图肉眼检查匹配质量 np.save(./disparity.npy, disparity) cv2.imwrite(./disparity.png, cv2.normalize(disparity, None, 0, 255, cv2.NORM_MINMAX))逻辑说明第1步stereoRectify返回的Q矩阵是第4章从视差转点云的关键alpha0表示校正后图像做最大限度的裁剪四角会有黑边但畸变校正最彻底CALIB_ZERO_DISPARITY让主点不偏移视差范围是纯水平方向。第2步的initUndistortRectifyMap输出两张映射表CV_32FC1表示浮点坐标映射比整数映射精度高。第4步SGBM输出的是16位定点数所以要除以16.0才能得到真实单位为像素的视差值。参数说明numDisparities是视差搜索范围范围越大能测的深度越近但计算量线性上涨且对噪声更敏感。blockSize是匹配窗口边长越大对纹理缺失区域越鲁棒但会抹平物体边缘深度突变处容易出现“粘连”。P1和P2是视差平滑惩罚项P1惩罚小梯度变化P2惩罚大跳变经验值是P18×通道数×blockSize²P232×通道数×blockSize²。uniquenessRatio10要求最优匹配代价至少比次优低10%能过滤部分误匹配但设太高会把纹理弱区域全滤成空洞。disp12MaxDiff1是左右一致性检查阈值用于剔除遮挡区域的错误匹配。3.3 调参顺序和常见误用不要一上来就追求精细点云先让视差图“能看”。我一般按这个顺序调先放大numDisparities直到前景不再有明显断层再调blockSize到边缘开始变糊为止最后用uniquenessRatio和speckleWindowSize清理噪点。每次只动一个参数改完存一张视差图对比否则调参就是碰运气。一个常见误用是P2设得比P1小OpenCV内部对P2有下限约束但不报错结果就是视差图像打满麻子很多人调半天发现是这个原因。另一个高频翻车点是numDisparities不是16的倍数会直接抛出异常这是SGBM最常见的报错之一注意看异常提示就能秒定位。另外提醒一句如果两台摄像头采集的画面亮度差异大先用直方图均衡化或简单地转成灰度再进SGBM否则匹配质量会明显下降。4. 从视差图到三维点云重投影与点云后处理4.1 Q矩阵的几何含义一步乘法完成三角测量stereoRectify返回的Q矩阵是4×4的重投影矩阵它把校正后图像上的齐次像素坐标(u, v, disparity, 1)直接映射到相机坐标系下的三维点(X, Y, Z, W)。实际计算里Z等于焦距×基线÷视差Q矩阵把这条三角测量公式编码成了矩阵乘法OpenCV提供了cv2.reprojectImageTo3D一步到位不需要手动写反投影矩阵。输入是视差图输出是三维点图每个像素位置存一个(x, y, z)坐标。这里有个关键边界视差图里无效像素的值通常是0或负值重投影后会被算到无穷远必须在生成点云前过滤掉。常见做法是结合视差有效掩码和深度范围掩码双重过滤宁可多滤掉一些可疑点也别让飞点污染后续的可视化和测量。4.2 用Open3D生成、保存和查看点云import cv2 import numpy as np import open3d as o3d calib np.load(./stereo_calib.npz) Q calib[Q] # 第3章stereoRectify的返回值务必存下来 disparity np.load(./disparity.npy) # 1. 重投影视差图 - 三维点图 points_3d cv2.reprojectImageTo3D(disparity, Q, handleMissingValuesTrue) # handleMissingValuesTrue会把无效深度置为一个很大的负数 # 2. 构造mask过滤无效深度 过滤离摄像头太远/太近的点 mask disparity 0 # 有效视差 depth_map points_3d[:, :, 2] # 只保留0.3m~3m范围内的点具体范围按你的场景调 mask (depth_map 300) (depth_map 3000) # 3. 取出有效点及其颜色 colors cv2.cvtColor(cv2.imread(./test/left.png), cv2.COLOR_BGR2RGB) xyz points_3d[mask].reshape(-1, 3) rgb colors[mask].reshape(-1, 3).astype(np.float32) / 255.0 # 4. 构图并可视化 pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(xyz) pcd.colors o3d.utility.Vector3dVector(rgb) # 5. 保存ply方便放到meshlab里进一步处理 o3d.io.write_point_cloud(./output_pointcloud.ply, pcd) # 6. 可视化需要图形窗口 o3d.visualization.draw_geometries([pcd])逻辑说明reprojectImageTo3D输出的points_3d形状和原图一样是(H, W, 3)每个像素的3个通道分别存X、Y、Z坐标。我们构造的mask是布尔数组同时做了两件事过滤视差非正的无匹配像素过滤深度范围外的点——这一步至关重要如果不做点云里会出现大量飞点和背景墙。第3步从原始左图取颜色注意OpenCV读图是BGR可视化前要转成RGB。参数说明深度范围(300, 3000)的单位是mm和标定时SQUARE_SIZE的单位保持一致——这是整个项目最容易自我矛盾的地方你得先确认SQUARE_SIZE用的什么单位再决定这里的数值。如果标定时填了0.025米那这里深度范围就应该是(0.3, 3.0)。handleMissingValuesTrue时无效点会被填成-10000所以单靠depth_map 300就能把它们一并过滤掉。这里用的是左图颜色如果你想要更真实的纹理可以考虑用左右图融合后的颜色但初版用左图就够了。4.3 点云后处理降采样、离群点去除和坐标裁剪draw_geometries出来效果不错只能说明流程通了实际项目里点云没这么干净立体匹配会在边缘、反光面和遮挡处产生零星误匹配这些点在三维空间里表现为孤岛。最常见的处理顺序是先voxel降采样降低点数再用统计离群点去除孤立噪点最后按需求裁剪。# 1. 降采样每0.005m一个体素控制点云密度 pcd_down pcd.voxel_down_sample(voxel_size0.005) # 2. 统计离群点去除每个点用周围30个邻居计算平均距离 pcd_clean, ind pcd_down.remove_statistical_outlier( nb_neighbors30, std_ratio1.5) # 3. 按坐标轴裁剪比如只保留Z2m和X在(-1,1)之间的点 points np.asarray(pcd_clean.points) bbox (points[:, 2] 2000) (np.abs(points[:, 0]) 1000) (np.abs(points[:, 1]) 1000) pcd_final pcd_clean.select_by_index(np.where(bbox)[0]) o3d.io.write_point_cloud(./output_clean.ply, pcd_final)参数说明voxel_size0.005表示5毫米内只保留一个点对室内场景够用。nb_neighbors30和std_ratio1.5是Open3D里比较经典的组合能去掉大部分孤立噪点又不伤主体。裁剪用点级布尔索引比crop_box更直观适合只关心某个区域的场景。这里裁剪范围也是毫米单位和第4.2节保持同一套坐标系。5. 双目重建最容易翻车的五个坑现象、原因与解决5.1 现象视差图全是噪点边界糊成一团原因标定图像太少或者图片有运动模糊。双目匹配对图像质量极其敏感标定板在快门时间内有轻微晃动角点亚像素定位就会偏出好几个像素算出的R和T带毒后面全链路的视差都会崩。解决回到第2章重新采集至少20组清晰的标定图。采集时把相机架在三脚架上不要手持按快门条件允许就用手动固定曝光避免自动曝光导致左右图亮度差异大。检查双目标定的retS返回值超过0.5像素就直接放弃这套参数重新标。5.2 现象点云整体扭曲地面像拱桥墙面是弯的原因极线校正没做对。最常见的是校正时用的图像尺寸和实际图像不一致或者initUndistortRectifyMap的映射表被错误复用到了另一组图像上。另一个隐蔽原因左右图像在采集时通道顺序不一致但视差计算输入的是灰度图时不显现问题出在第4章用彩色图贴点云那一步。解决先确认img_size和实际图像尺寸严格一致。再用cv2.line在校正后的左右图上画几条水平线逐行对比同一物体的边缘是否在同一y坐标。画线法是最直观的验证手段比盯着点云猜半天高效得多。5.3 现象SGBM慢到一秒一帧原因numDisparities设得太大或者图像分辨率过高。有人把参数调到320视差图看起来很满但算力代价翻了好几倍。另一个原因是把mode设成了默认的串行模式电脑又是虚拟机单核。解决优先缩小输入图像保持长宽比缩到一半计算量降为四分之一。然后按场景调整视差范围基线60mm、被测物在1到3米时视差范围一般不超过80像素numDisparities64通常够。真正需要大视差范围的场景换基线更长的相机才是正道。5.4 现象点云里全是飞点像雪崩挂在物体边缘原因物体边缘在左右视图里有遮挡区本来就匹配不上加上SGBM窗口平滑会把前景和背景的深度混在一起。speckleWindowSize和speckleRange没调或者disp12MaxDiff0禁用了左右一致性检查。解决第一步先开disp12MaxDiff1做左右一致性检查把不一致的像素置为无效第二步把speckleWindowSize100、speckleRange32设置上把面积太小的孤立连通域抹掉最后生成点云时按第4章的深度范围裁剪兜底。三个手段叠加后飞点通常能压到可接受水平。5.5 现象点云颜色错乱物体颜色和现实对不上原因两个摄像头视角里同一个点的颜色本来就有差异——白平衡、曝光、传感器响应都不同。如果直接用左图颜色贴点云拼接处会出现明显色块看起来像两张图硬凑在一起。解决在标定阶段就把两个摄像头的白平衡和曝光设为固定值不要用自动模式。USB摄像头在OpenCV里可以用cv2.CAP_PROP_WB_TEMPERATURE和cv2.CAP_PROP_EXPOSURE强制设置。颜色一致性是双目项目里典型的“看不出来但很影响结果”的环节最好从源头锁死别指望后处理能完全救回来。6. 重建质量验证三个量化手段和一个长期习惯最后这章写给想让项目能答辩、能落地的你。视差图看起来顺眼不算数点云能转起来也不代表深度正确。我常用的验证手法有三招。第一重投影误差复核。标定时输出的retS是所有角点在双目标定下的均方根误差。这个值低于0.3像素说明内参和畸变估计得不错高于0.5像素就需要重新采集图。答辩时把这个数打印出来比“我的系统很准”有说服力得多。第二视差图有效像素占比。disparity 0的像素数除以图的总像素数。室内场景一般要求不低于60%如果只有20%说明相机基线、光照或标定参数里有一环明显出问题。这个指标调试时能快速判断是匹配问题还是标定问题。第三实物尺度验证。拿一个已知长度的物体比如A4纸长边297毫米放进视场在点云里框出两个端点测量欧氏距离。误差在5%以内说明尺度标定可信误差超过10%优先检查SQUARE_SIZE的单位再查Q矩阵是否从同一套标定流程生成。我自己的一个长期习惯是固定好双目模组后用一点热熔胶把两个摄像头的位置锁死把标定当天拍的棋盘格照片存到一个只读的reference目录里之后每次改代码都先跑这组老照片做回归测试。这个习惯救过我一次大翻车——改了一行预处理后视差图全空调了两小时最后发现是把灰度图转成了三通道回归测试三秒就指出了问题位置。双目立体视觉这套东西理论不复杂但每个环节都可能成为黑匣子标定误差、匹配参数、单位混用任何一个都会让最终结果面目全非。希望这篇笔记能让你少走几段弯路祝你把点云跑出理想形状的那天手感顺利。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?