
简介一份基于Python实现的单目与双目视觉三维重建课程设计源码包面向计算机专业学生可作为本科毕设、课程设计或期末大作业的完整参考。项目由大三大作业提炼经导师指导评审获得99分代码完整且可直接运行附有使用说明零基础也能按步骤完成复现。压缩包共41个文件包含3个核心Python脚本单目重建、双目重建及图像拼接、34张实拍测试图片、2个运行说明文本、1份README文档和1张效果示意图整体约80MB目录结构清晰便于按需查阅。目前已有142人学习下载。通过源码可了解特征提取、视差计算、深度恢复等关键流程也可学习相机标定与极线匹配思路并借助自带的多组静物场景图片快速验证效果适合正在做相关课题、毕业设计或希望提升项目实战能力的学生。1. 单目双目视觉三维重建这份 Python 课程设计源码解决什么问题把“课程设计基于python实现的单目双目视觉三维重建源码”挂上选题列表的人多半不是被理论吓退的而是搞不清标定、特征匹配、视差、点云这些词到底怎么连成一条能跑通的链路。单目重建做的事是用像素坐标与已知三维点的对应关系反推相机姿态双目重建则靠两台相机观察同一场景的视角差先算视差图再反投影成三维点云。两者共用一套相机标定结果代码量可控可视化效果好是课程设计里性价比最高的方向之一。这篇笔记适合正在选课设题目或已经拿到题目的学生不需要深度相机一台普通笔记本足够跑通。2. 重建路线怎么选单目、双目、SfM 与深度方案的取舍2.1 几条路线对比精度、硬件成本与代码量先摆清现状。课程设计常见做法有四类基于 SfM 的增量式重建、基于单目 PnP 的姿态恢复、基于双目立体匹配的稠密重建以及基于深度学习的深度估计。SfM 听着最正统但内部要处理特征跟踪、稀疏三角化、BA 优化自己从零写很容易在调试阶段翻车深度学习路线出图快可课设机房不一定有 GPU预训练模型也未必下载得动。我一般建议走“单目 PnP 双目 SGBM”这条双线方案单目负责少点数姿态估计双目负责稠密点云输出两个模块共用一个标定结果工作量连贯答辩时也有两条完整的故事线。路线输入输出典型工具课程设计适配度单目 PnP已知 3D 点 对应 2D 点相机位姿OpenCV solvePnP代码量最小适合先跑通SfM多视角图像序列稀疏点云 位姿OpenMVG / COLMAP流程完整但工程量大双目立体匹配已标定左右图深度图 稠密点云OpenCV SGBM可视化效果好参数敏感深度学习单张或多张 RGB深度图 / 3D 高斯重建MiDaS / 3DGS效果强但依赖 GPU 与权重文件即使以后想往 3D 高斯重建这类更前沿的方向扩脚下仍是标定和反投影这两块地基。因此课设阶段不要贪大先做最稳的路线把每个环节的参数讲清楚分数就不会低。2.2 相机标定是整个工程的基石标定代码与参数无论单目还是双目第一步都是相机标定。标定解决两件事一是拿到内参矩阵 K二是拿到畸变系数 dist。手机和普通摄像头拍出来的照片边缘弯曲明显不校正直接算三维坐标点云会在边缘翘起来。下面是 OpenCV 棋盘格标定的标准流程我习惯在标定时把格子物理尺寸写进世界坐标这样后续重建出来的点云自带真实尺度。import numpy as np import cv2 import glob # 棋盘格内角点数必须是“每行格子数-1” × “每列格子数-1” # 以 7×10 格的棋盘为例内角点是 (6, 9) CHECKERBOARD (6, 9) criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) # 每个小格的物理尺寸这里按 20mm 一格处理 # 这个值只影响整体尺度不影响相对位置写成 1.0 也可以 scale 20.0 objp np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) objp[:, :2] np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2) objp * scale objpoints, imgpoints [], [] for fname in sorted(glob.glob(calib/left/*.jpg)): img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, CHECKERBOARD, None) if ret: objpoints.append(objp) corners2 cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) imgpoints.append(corners2) ret, K, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None) print(重投影误差(px): , ret) print(内参矩阵K: , K) print(畸变系数dist: , dist)这段代码的逻辑是findChessboardCorners在灰度图中定位棋盘内角点cornerSubPix把角点坐标细化到亚像素精度所有角点的世界坐标放在同一平面y0上x、z按格子间距铺开。scale 20.0意味着一个格子按 20 毫米算后续单目 PnP 恢复出来的一米就是真实的一米。calibrateCamera返回的ret是平均重投影误差正常应小于 0.5 像素K里是fx, fy, cx, cydist是径向和切向畸变系数。误差压不下去后面所有环节都会跟着出错。2.3 数据怎么组织目录结构与拍摄要求代码写好了最影响进度的其实是数据组织。我见过好几组同学把左右图放反最后点云完全镜像排查半天还以为是算法问题。建议按下面的目录结构存放脚本直接用glob遍历就能一次读全。dataset/ ├── calib/ │ ├── left/ # 标定用左图15 张左右 │ └── right/ # 标定用右图15 张左右 ├── stereo/ │ ├── left/ # 双目重建左图序列 │ └── right/ # 双目重建右图序列 └── object/ ├── box_3d.txt # 已知物体的世界坐标 └── box.jpg # 摄像头实拍的物体照片拍摄标定图有四个硬性要求数量至少 15 张棋盘在画面中的占比要接近三分之一到二分之一拍摄时让棋盘倾斜 30° 到 45°四个角和画面中心都要覆盖到不要用手捏住棋盘边缘以免板面弯曲。如果手边没有打印棋盘用平板电脑显示一张 A4 棋盘图也可以但屏幕反光会影响角点检测尽量调低亮度再拍。数据准备得越规矩后面调参的时间就越少。3. 单目重建跑通SIFT 特征匹配与 solvePnP 姿态估计3.1 从像素对应到相机位姿单目重建的最小流程单目单帧图像无法直接得到绝对深度这是物理约束绕不过去。课程设计里做的单目重建通常是借助已知物体先验物体上若干特征点的三维坐标提前量好再由当前帧的像素坐标反推相机位姿。比如在纸盒上贴几个图案并量出相互间距这就是一组“已知 3D 点”。整条流程分四步提取特征点与描述子对两帧图像做特征匹配把匹配对送进 solvePnP把旋转向量和平移向量画成坐标轴验证。这里推荐用 SIFT 而不是 ORBSIFT 对尺度变化和视角变化更稳课设场景里物体纹理可控匹配成功率会高不少。3.2 SIFT 特征匹配用代码把对应点挖出来如果cv2.SIFT_create()报错说明环境里装的是不带 contrib 模块的 opencv-python需要先卸载再安装 opencv-contrib-python这个问题每年都能卡住不少人。匹配代码本身不长核心是先算描述子再用暴力匹配取最近邻。import cv2 import numpy as np sift cv2.SIFT_create(nfeatures5000) kp1, des1 sift.detectAndCompute(gray_frame_1, None) kp2, des2 sift.detectAndCompute(gray_frame_2, None) bf cv2.BFMatcher(cv2.NORM_L2, crossCheckTrue) raw_matches bf.match(des1, des2) raw_matches sorted(raw_matches, keylambda m: m.distance) # 取前 150 个最近邻匹配物体在画面中占比小可放宽到 300 good raw_matches[:150] pts_3d np.float32([model_points[m.queryIdx] for m in good]) pts_2d np.float32([kp2[m.trainIdx].pt for m in good])nfeatures5000表示最多保留 5000 个特征点这不是硬上限而是一个控制量crossCheckTrue保证匹配是双向验证的A 图中某点匹配到 B 图后B 图那个点必须也能匹配回 A 图才接受。bf.match返回全部匹配对按距离升序排列后距离越小代表描述子越相似。model_points是从提前量好的三维坐标文件里读进来的索引顺序必须与kp1一致这是单目方案成立的先决条件。如果匹配质量还不够可以在取前 150 对之前再加一道 ratio test对每个查询点找最近邻和次近邻两个匹配只有当最近距离与次近距离的比值小于 0.75 时才接受。3.3 solvePnP 姿态估计的代码与调参有了成对的 3D 与 2D 点姿态估计就交给solvePnPRansac。它的名字已经说明问题RANSAC 会在匹配里随机采样、反复计算位姿最后留下最一致的那组点。_, rvec, tvec, inliers cv2.solvePnPRansac( objectPointspts_3d, # 物体坐标系下的三维点(N, 3) imagePointspts_2d, # 对应像素坐标(N, 2)顺序不能乱 cameraMatrixK, # 第 2 章标定得到的内参矩阵 distCoeffsdist, # 畸变系数 reprojectionError8.0, # 重投影误差阈值单位像素越大内点越多 iterationsCount1000, # RANSAC 迭代次数 confidence0.99, # 置信度 flagscv2.SOLVEPNP_ITERATIVE ) R, _ cv2.Rodrigues(rvec) # 旋转向量转旋转矩阵 print(旋转矩阵R:, R) print(平移向量t:, tvec)reprojectionError是最值得调的参数设成 2 会过于严格少量外点就让位姿发抖设成 8 到 10 在课设场景比较合适能留下足够内点又不会吞掉错误匹配。flags在点到数量少于 10 时用SOLVEPNP_ITERATIVE点数充足时换成SOLVEPNP_EPNP速度更快。inliers返回的是内点在输入数组中的索引调试时打印len(inliers)如果内点数不足匹配总数的 30%说明特征质量差回去检查 3.2 节的匹配过滤条件。做完这些用cv2.projectPoints把三维坐标轴端点重投影到画面里看坐标轴是否贴在物体上这一步是单目姿态最直观的验证见第 6 章。提示solvePnP 的pts_3d与pts_2d顺序必须一一对应顺序错乱不会报错只会得到完全随机的位姿。这是最常见的“不报错但结果错”来源。4. 双目重建跑通SGBM 立体匹配与点云生成4.1 极线校正与视差搜索双目重建的原理一句话能讲清空间中一点投影到左右两图时横向位置会错开这个错开的距离叫视差视差越大说明物体离相机越近。所谓极线校正就是通过双目标定得到的相对位姿把左右图重采样成“同一行对齐”的状态之后匹配只需要在同一行里搜索计算量大幅下降。原图直接丢给 SGBM 会得到一团噪声必须先校正。# 假设已分别得到 K1, dist1, K2, dist2 # 双目标定求左右相机相对位姿 R, T R, T, E, F cv2.stereoCalibrate( objpoints, imgpoints_left, imgpoints_right, K1, dist1, K2, dist2, image_size, flagscv2.CALIB_FIX_INTRINSIC) R1, R2, P1, P2, Q, _, _ cv2.stereoRectify( K1, dist1, K2, dist2, image_size, R, T, alpha0, newImageSize(w, h)) map1x, map1y cv2.initUndistortRectifyMap( K1, dist1, R1, P1, (w, h), cv2.CV_32FC1) map2x, map2y cv2.initUndistortRectifyMap( K2, dist2, R2, P2, (w, h), cv2.CV_32FC1) rect_l cv2.remap(imgL, map1x, map1y, cv2.INTER_LINEAR) rect_r cv2.remap(imgR, map2x, map2y, cv2.INTER_LINEAR)stereoRectify输出的Q矩阵是后面反投影的关键先打印出来看一眼Q[0][3]和Q[1][3]如果为 0说明校正参数有问题。alpha0表示把校正后的图像裁剪到只保留有效区域优点是不会出现黑边缺点是四角轻微损失。initUndistortRectifyMap生成两张浮点查找表remap按表重采样整张图。完成这一步后可以随手写一句代码把左右图并排保存肉眼确认同一行上的物体高度一致再继续往后调参。4.2 封装一个 SGBM参数说明与调参表SGBM 全称 Semi-Global Block Matching是 OpenCV 在 CPU 上性价比最高的立体匹配方案兼顾局部块匹配的速度和全局平滑的质量。下面这个封装函数把常用参数都暴露出来调参时只需要改入口数值。import cv2 def make_stereo_sgbm( min_disparity-3, num_disparities64, block_size15, p1_mult8, p2_mult32 ): block block_size if block_size % 2 1 else block_size 1 stereo cv2.StereoSGBM_create( minDisparitymin_disparity, numDisparitiesnum_disparities, blockSizeblock, P1p1_mult * 3 * block ** 2, P2p2_mult * 3 * block ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32, modecv2.STEREO_SGBM_MODE_SGBM ) return stereo stereo make_stereo_sgbm() disp_raw stereo.compute(rect_l, rect_r).astype(np.float32) / 16.0StereoSGBM_create输出的视差是定点数单位是 1/16 像素所以compute之后必须除以 16 才是真实视差这一步漏掉会让深度图整体暗一个量级。参数表如下参数经验值作用minDisparity-3 到 0容忍标定残差造成的负视差不要设成负数以下太多numDisparities64 到 128必须是 16 的倍数决定可检测的最近距离blockSize15 或 21匹配窗口边长必须是奇数越大越平滑但边缘越糊P1 / P28/32 × 3 × block²平滑惩罚系数P2 太小会出现深度条纹uniquenessRatio10 到 15唯一性约束过大容易产生空洞speckleWindowSize100 到 200连通域滤波尺寸滤掉小块噪点调参有个顺序先把numDisparities调到场景能看清的尺度再动blockSize找边缘清晰度最后用 P1/P2 压噪点。不要上来就同时改四个参数那样出了问题根本不知道是哪个变量引起的。双目课设里“深度图大面积黑色”多半就是numDisparities太小或者根本没做极线校正把 4.1 节的图先贴出来检查一遍再调。4.3 视差转点云原理与 ply 输出代码视差图转点云不需要手写三角化公式stereoRectify已经给出了Q矩阵reprojectImageTo3D一行就能把每个像素反投影成三维坐标。上色用左图最自然因为视差就是以左图为基准算出来的。def disp_to_ply(disp, Q, color_img, out_fileoutput.ply): points_3d cv2.reprojectImageTo3D(disp, Q) valid disp 1.0 # 去掉视差无效的背景区域 pts points_3d[valid] colors color_img[valid] with open(out_file, w) as f: f.write(ply\n) f.write(format ascii 1.0\n) f.write(felement vertex {len(pts)}\n) f.write(property float x\n) f.write(property float y\n) f.write(property float z\n) f.write(property uchar red\n) f.write(property uchar green\n) f.write(property uchar blue\n) f.write(end_header\n) for (x, y, z), (r, g, b) in zip(pts, colors): f.write(f{x:.4f} {y:.4f} {z:.4f} {int(r)} {int(g)} {int(b)}\n) print(点云已写出: , out_file)valid disp 1.0的阈值不要设太高否则近处小物体会被成片滤掉disp是除以 16 后的视差图所以 1.0 像素以下基本是无效区。PLY 写成 ASCII 是为了可以用记事本直接打开检查数据量大时改成 binary 格式能快不少。如果生成的 ply 在可视化软件里上下颠倒或左右镜像优先检查 4.1 节里rect_l和rect_r是不是放反了这是最常见的原因。5. 三维重建避坑五个必踩的坑与排查思路这类项目有三分之二的时间花在排错上而且很多错误不会抛异常只会在点云里形成一片难看的噪声。“不报错但结果错”比“报错”更难定位下面五条是我在带课设时反复见到的典型问题按现象、原因、解决的顺序写清楚。5.1 标定环节的坑现象calibrateCamera返回的重投影误差一直在 1 px 以上重建点云边缘明显弯曲。原因九成情况是CHECKERBOARD内角点数与实际棋盘不一致。你可能拿了 7×10 的棋盘代码里却写了(7, 10)正确写法是(6, 9)。其次原因是图片质量差棋盘在画面里太小、板面弯曲、数量不足。解决把代码里的内角点数与实际棋盘逐行核对采集时保证棋盘倾斜、远近交错至少 15 张跑完标定后打印每张图的单图误差把误差最大的几张删掉重新标。ret低于 0.5 px 再继续往下走。5.2 特征匹配与位姿环节的坑现象SIFT 提取到的特征点很少或者solvePnPRansac的位姿在连续帧之间抖得厉害坐标轴在物体上跳来跳去。原因特征点少通常是输入图像太暗、对比度低contrastThreshold默认值对新场景不友好。位姿抖动则是匹配质量差的外点混进了 PnP 计算reprojectionError设置不合理或者直接用了未过滤的全量匹配。解决对输入图做一次直方图均衡化把contrastThreshold从默认值降到 0.03nfeatures提到 8000。匹配阶段加 ratio test阈值 0.75并打印len(inliers)内点比例低于 30% 时回看匹配热力图把明显错配的对删掉再重算。现象重建出来的物体整体上下颠倒或者 x、y 轴发生镜像。原因单目里是pts_3d与pts_2d的索引对应错位双目里是左右图加载顺序与Q矩阵约定不符T的符号反了。解决先在物体上做一个明显颜色标记验证该点在三维坐标里的符号方向如果是单目检查model_points[m.queryIdx]的索引是否对齐双目则交换左右图重跑或直接查看stereoRectify返回的T符号必要时对点云某一轴取反。5.3 深度图与点云环节的坑现象SGBM 输出的大面积黑色只有物体轮廓有一圈白边深度图上布满条带噪声。原因最常见的是跳过了极线校正把原始左右图直接丢给 SGBM其次是numDisparities设得比 48 还小或者blockSize写成了偶数。解决先确认rect_l和rect_r在同一行的物体高度一致再把numDisparities提到 64 或 128blockSize固定为 15 或 21最后把speckleWindowSize提到 150滤掉点状噪点。若输出仍有条带把P2从 32 倍降到 24 倍试试。6. 验证与提分用 Open3D 把点云做成可视化成果6.1 Open3D 可视化法线估计与泊松重建裸点云在截图里和噪声很难区分课程设计答辩时不加分。建议用 Open3D 补一步法线估计和泊松重建把散点变成曲面网格视觉冲击力完全不同。import open3d as o3d pcd o3d.io.read_point_cloud(output.ply) pcd.estimate_normals( search_paramo3d.geometry.KDTreeSearchParamHybrid( radius0.05, max_nn30)) mesh, _ o3d.geometry.TriangleMesh.create_from_point_cloud_poisson( pcd, depth8) o3d.visualization.draw_geometries([mesh])radius0.05的单位是米必须与重建物体实际尺度匹配。如果重建对象只有 5 厘米大小这个半径就要缩到 0.01否则法线会平滑掉真实结构。depth8是八叉树深度越大细节越多但边缘也越容易鼓包课设场景 8 足够。这里值得多花半小时调法线半径一张干净的曲面网格图比几十行代码截图更能让评委记住你的工作量。6.2 验证方法与报告提分技巧单目姿态的验证方法很直接在画面里画出 x、y、z 三根坐标轴然后绕物体的一个轴慢慢转动它看坐标轴是否跟着物体同步转。如果转的方向对、角度大致吻合姿态结果就是可信的这比看任何误差数字都直观。数值验证方面把重建点云里任意两点的三维距离和真实量测长度对比误差控制在 3% 以内这个数字可以直接写进课程设计报告的实验结果页。报告的提分思路也有套路把 SGBM 调参前的“大片黑色深度图”和调参后的清晰深度图并排放旁边加一段参数变化说明这是最好的“过程证据”再放一个单目坐标轴重投影的动图或连续帧截图说明姿态估计在动态场景下的稳定性最后补一组点云距离误差表。代码注释里保留关键参数的解释答辩时问到就能直接答出来。我带过的课设小组里多数人卡在坐标符号问题上没能跑完整体流程如果你也是第一次做先追求跑通最小闭环再回来补功能。希望帮到你。本文还有配套的精品资源点击获取