ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python三维重建实战:双目立体匹配与单目深度估计点云生成

Python三维重建实战:双目立体匹配与单目深度估计点云生成 简介这份资源面向希望入门或进阶计算机视觉的学习者提供基于 Python 实现的单目与双目视觉三维重建完整项目可作为毕业设计、课程设计、大作业或工程实训的参考方案帮助读者理解从图像采集到深度恢复的基本流程。压缩包共 41 个文件约 80.24MB以 jpg 实拍图像为主要数据来源配合 3 个 py 脚本分别承担单目重建、双目重建与图像拼接功能另有 txt 说明与 md 文档辅助理解项目结构与运行方式。目前已有 244 人学习下载。项目目录中单目与双目代码相互独立便于对照阅读配套的多组实拍图像覆盖不同物体与场景可直接用于复现实验与调试。读者可借此掌握相机标定、立体匹配、视差计算与点云生成等关键环节并参考现有脚本快速搭建自己的三维重建流程适合作为视觉方向入门与项目立项的实践素材。1. 从两张照片到一堆点云这套 Python 三维重建资源到底能干什么你手上有两张从不同角度拍的同一物体照片想还原出它的三维结构或者只有一颗摄像头想从单帧图像里估出深度——这类需求在工业检测、逆向建模、机器人抓取里非常常见。这套基于 Python 实现的单目/双目视觉三维重建资源核心就是解决「从二维图像恢复三维信息」这件事。它把双目立体匹配、单目深度估计、点云生成与可视化这几条链路串了起来代码可直接跑不依赖复杂编译环境。适合有 Python 基础、想快速上手三维重建的开发者也适合做课程设计或原型验证的工程师。单目方案成本低但尺度模糊双目精度高但标定麻烦资源里两条路线都有对应实现你可以按场景选。2. 双目重建链路拆解标定、校正、匹配、三角化2.1 为什么双目能算出深度双目视觉的底层逻辑不复杂两个相机在同一时刻拍同一场景同一个物点在左右图像上的像素位置有差异这个差异叫视差。视差越大物体越近视差越小物体越远。深度 Z 和视差 d 的关系是 Z f × B / d其中 f 是焦距像素单位B 是两个相机光心的基线距离。这个公式是整个双目重建的基石所有后续步骤都是为了拿到准确的 d。但直接拿原始图像做匹配会翻车。因为两个相机的镜头畸变、安装角度、焦距不可能完全一致同一物点在左右图上的极线不水平匹配搜索会变成二维搜索计算量大且容易错配。所以必须先做立体校正把左右图像拉到同一极线水平上让匹配变成沿水平方向的一维搜索。这一步是双目链路里最容易被跳过、也最容易导致后续点云「重影」的环节。资源里的双目部分整体流程是相机标定 → 立体校正 → 立体匹配 → 视差转深度 → 点云生成。下面按这个顺序拆。2.2 相机标定拿到内外参和畸变系数标定是双目的第一步目的是拿到每个相机的内参矩阵、畸变系数以及两个相机之间的旋转和平移关系。常见做法是用棋盘格标定板拍 15 到 20 组不同角度的照片。资源里用的是 OpenCV 的标定接口代码结构如下import cv2 import numpy as np import glob # 棋盘格内角点数量比如 9x6 chessboard_size (9, 6) # 棋盘格方格实际物理尺寸单位 mm square_size 25.0 # 生成棋盘格三维坐标 objp np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2) objp * square_size objpoints [] # 三维点 imgpoints_left [] # 左图角点 imgpoints_right [] # 右图角点 left_images sorted(glob.glob(calib/left/*.jpg)) right_images sorted(glob.glob(calib/right/*.jpg)) for l_img, r_img in zip(left_images, right_images): img_l cv2.imread(l_img) img_r cv2.imread(r_img) gray_l cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY) ret_l, corners_l cv2.findChessboardCorners(gray_l, chessboard_size, None) ret_r, corners_r cv2.findChessboardCorners(gray_r, chessboard_size, None) if ret_l and ret_r: # 亚像素级角点优化 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_l cv2.cornerSubPix(gray_l, corners_l, (11, 11), (-1, -1), criteria) corners_r cv2.cornerSubPix(gray_r, corners_r, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints_left.append(corners_l) imgpoints_right.append(corners_r) # 分别标定左右相机 ret_l, mtx_l, dist_l, _, _ cv2.calibrateCamera(objpoints, imgpoints_left, gray_l.shape[::-1], None, None) ret_r, mtx_r, dist_r, _, _ cv2.calibrateCamera(objpoints, imgpoints_right, gray_r.shape[::-1], None, None) # 双目标定拿到 R 和 T ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F cv2.stereoCalibrate( objpoints, imgpoints_left, imgpoints_right, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], flagscv2.CALIB_FIX_INTRINSIC )这段代码的关键参数有三个chessboard_size必须和实际棋盘格内角点数一致写错会导致角点检测失败square_size影响平移向量的物理尺度如果只关心相对深度可以随便填但要输出真实毫米单位就必须准确CALIB_FIX_INTRINSIC表示双目标定时固定单目标定得到的内参避免过拟合。标定完成后重投影误差ret最好控制在 0.5 像素以内超过 1 像素说明标定板角度不够丰富或者图像有运动模糊需要重拍。2.3 立体校正与匹配把搜索降到一维标定拿到 R 和 T 之后用stereoRectify计算校正映射再用initUndistortRectifyMap生成映射表最后用remap把左右图拉到极线水平。这一步做完同一物点在左右图上的 y 坐标应该基本一致匹配只需要沿 x 方向找。# 立体校正 R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T, alpha0 # alpha0 表示校正后图像只保留有效像素 ) # 生成映射表 map1_l, map2_l cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_16SC2) map1_r, map2_r cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_16SC2) # 对后续每一帧做校正 img_l cv2.imread(test/left.jpg) img_r cv2.imread(test/right.jpg) rect_l cv2.remap(img_l, map1_l, map2_l, cv2.INTER_LINEAR) rect_r cv2.remap(img_r, map1_r, map2_r, cv2.INTER_LINEAR) # SGBM 立体匹配 window_size 5 min_disp 0 num_disp 16 * 5 # 必须是 16 的倍数 stereo cv2.StereoSGBM_create( minDisparitymin_disp, numDisparitiesnum_disp, blockSizewindow_size, P18 * 3 * window_size ** 2, P232 * 3 * window_size ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32 ) disparity stereo.compute(rect_l, rect_r).astype(np.float32) / 16.0numDisparities决定搜索范围设小了近处物体视差超出范围会丢深度设大了远处噪声会增多常见做法是先估一下场景最近距离对应的视差上限。blockSize影响匹配窗口大小纹理丰富的场景可以小一点保留细节纹理弱的场景要加大否则匹配失败。uniquenessRatio是唯一性检查值越大越严格能过滤掉一些误匹配但太大会导致有效点变少。SGBM 输出的视差图边缘会有空洞这是半全局匹配的固有特性后面可以用左右一致性检查或者滤波补一下。2.4 视差转点云Q 矩阵与 reprojectImageTo3D校正完成后stereoRectify返回的 Q 矩阵就是视差到三维坐标的映射矩阵。OpenCV 提供了reprojectImageTo3D直接做转换# 视差转三维点云 points_3d cv2.reprojectImageTo3D(disparity, Q) # 过滤无效点 mask disparity disparity.min() mask mask (disparity num_disp) points points_3d[mask] colors rect_l[mask] # 保存为 PLY def write_ply(filename, points, colors): with open(filename, w) as f: f.write(ply\nformat ascii 1.0\n) f.write(felement vertex {len(points)}\n) f.write(property float x\nproperty float y\nproperty float z\n) f.write(property uchar red\nproperty uchar green\nproperty uchar blue\n) f.write(end_header\n) for p, c in zip(points, colors): f.write(f{p[0]} {p[1]} {p[2]} {c[2]} {c[1]} {c[0]}\n) write_ply(output.ply, points, colors)Q 矩阵里包含了基线、焦距和主点信息reprojectImageTo3D输出的坐标单位跟标定时square_size一致。如果标定时填的是毫米点云单位就是毫米。过滤条件里disparity disparity.min()是为了去掉匹配失败的区域这些区域视差为负或零转出来的三维点会飞到无穷远。PLY 文件可以直接用 MeshLab 或 CloudCompare 打开检查如果点云整体倾斜大概率是校正没做好或者 Q 矩阵用错了。提示双目重建对光照和纹理很敏感。纯色墙面、反光金属、透明玻璃这些场景SGBM 基本拿不到有效视差这不是代码问题是算法边界。3. 单目路线怎么走深度估计与尺度恢复3.1 单目重建的两种思路单目只有一张图没有视差理论上无法直接恢复绝对深度。常见做法分两类一类是基于几何线索比如从运动恢复结构SfM用多帧图像估计相机位姿和稀疏点云另一类是基于学习的方法用预训练模型直接预测每像素深度图比如 MiDaS、Depth Anything 这类。资源里的单目部分主要走的是深度估计加尺度对齐的路线适合只有单相机或者只有单帧图像的场景。单目深度估计模型输出的是相对深度数值范围归一化过没有物理单位。要拿到真实尺度通常需要引入一个已知尺寸的参照物或者用双目做一次标定来给单目定标。资源里给了一个简单的尺度恢复方案在场景里放一个已知边长的标定块用深度图里标定块区域的深度值和实际距离做线性拟合得到缩放系数。3.2 用预训练模型跑单目深度资源里单目部分用的是 ONNX 格式的深度估计模型不依赖 PyTorch 训练环境推理只需要 OpenCV 的 dnn 模块import cv2 import numpy as np # 加载 ONNX 深度估计模型 net cv2.dnn.readNetFromONNX(models/depth_model.onnx) img cv2.imread(test/single.jpg) h, w img.shape[:2] # 模型输入通常是 384x384 或 512x512按模型要求调整 input_size (384, 384) blob cv2.dnn.blobFromImage(img, 1/255.0, input_size, swapRBTrue, cropFalse) net.setInput(blob) depth net.forward() # 输出可能是 [1,1,H,W] 或 [1,H,W]统一 reshape depth depth.reshape(input_size[1], input_size[0]) # 归一化到 0-255 方便可视化 depth_norm cv2.normalize(depth, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) depth_color cv2.applyColorMap(depth_norm, cv2.COLORMAP_INFERNO) # 恢复到原图尺寸 depth_resized cv2.resize(depth, (w, h), interpolationcv2.INTER_LINEAR)blobFromImage里的swapRBTrue是因为 OpenCV 读图是 BGR 顺序而模型训练时通常用 RGB。input_size必须和模型导出时一致写错会直接报维度错误。输出深度图是相对值近处数值大还是小取决于模型定义用之前最好拿一张已知场景验证一下方向。可视化用COLORMAP_INFERNO是因为它在深度图上对比度比 JET 更自然暗处细节不容易丢。3.3 尺度对齐与点云生成拿到相对深度后要转成三维点云还需要相机内参。如果单目相机做过标定直接用内参矩阵反投影# 假设已有内参 mtx fx mtx[0, 0] fy mtx[1, 1] cx mtx[0, 2] cy mtx[1, 2] # 尺度因子由参照物确定 scale 1000.0 # 示例值需根据实际标定块计算 points [] colors [] for v in range(h): for u in range(w): z depth_resized[v, u] * scale if z 0: continue x (u - cx) * z / fx y (v - cy) * z / fy points.append([x, y, z]) colors.append(img[v, u]) points np.array(points) colors np.array(colors) write_ply(mono_output.ply, points, colors)这段代码是逐像素反投影速度慢但逻辑清晰。实际用的时候可以向量化用 numpy 的 meshgrid 一次性算完。scale是单目重建里最玄学的参数它直接决定点云的整体大小。如果场景里没有已知尺寸物体只能得到相对结构绝对尺度没有意义。常见做法是拿一个已知长度的物体放在场景里量出它在深度图里对应的深度值反推 scale。注意单目深度估计模型对训练集分布外的场景泛化能力有限。室内模型拍室外、或者拍训练时没见过的物体类别深度图可能完全不可用。用之前先拿几张典型场景图跑一下看深度边缘是否贴合物体轮廓。4. 避坑与排查标定、匹配、点云里最容易翻车的几件事4.1 标定重投影误差大点云整体扭曲现象双目标定后stereoCalibrate返回的误差超过 1.5 像素生成的点云看起来像被拧过一样。原因标定板角度太单一大部分照片都是正对相机导致内参中的焦距和畸变系数耦合解不稳定。或者标定板在左右图中检测到的角点顺序不一致。解决重拍标定图保证标定板在画面中覆盖各个区域倾斜角度至少覆盖 ±30 度。拍的时候左右相机要同步如果物体动了而相机没同步角点对应关系就错了。拍完先单独检查左右相机的单目标定误差单目误差都大双目肯定好不了。4.2 SGBM 视差图大片黑色点云稀疏现象视差图里大部分区域是黑色只有纹理丰富的角落有值。原因numDisparities设得太小近处物体的视差超出了搜索范围或者blockSize太小弱纹理区域匹配窗口内没有足够信息。解决先估算场景最近距离对应的视差。如果基线是 60mm焦距 600 像素最近物体 500mm视差约 72 像素那numDisparities至少要设到 80 以上取 16 的倍数。弱纹理场景把blockSize从 5 加到 11 或 15同时把uniquenessRatio从 10 降到 5放宽匹配条件。4.3 点云颜色和位置对不上现象PLY 文件在 MeshLab 里打开点云形状对但颜色错位或者颜色整体偏蓝偏红。原因OpenCV 读图是 BGR 顺序写 PLY 时如果直接按 BGR 写入而查看器按 RGB 解析红蓝通道就反了。另外reprojectImageTo3D输出的点云和原图是像素对齐的但如果用了校正后的图做颜色采样而点云是用校正前的参数生成的位置就会偏。解决写 PLY 时确认通道顺序OpenCV 的c[0]是蓝、c[2]是红写入时按 RGB 顺序写。颜色采样统一用校正后的左图因为视差图是基于校正图算的点云坐标也是在校正坐标系下。4.4 单目深度图边缘糊成一团现象深度图整体趋势对但物体边缘模糊前后景交界处深度渐变而不是突变。原因深度估计模型输出分辨率通常低于原图上采样时用了双线性插值边缘被平滑了。另外模型本身对细结构的分辨能力有限。解决如果模型支持多尺度输出取高分辨率那一层。上采样时改用最近邻插值保留边缘或者用引导滤波guided filter以原图为引导做边缘保持上采样。对精度要求高的场景单目深度只做粗定位精细结构还是得靠双目或者结构光。4.5 点云文件太大打开就卡死现象PLY 文件几百 MBMeshLab 加载几分钟没响应。原因逐像素生成点云一张 1920×1080 的图就是两百万个点其中大量是背景和无效区域。解决生成点云前先做掩膜只保留视差有效且深度在合理范围内的区域。另外可以做体素下采样把空间上距离小于阈值的点合并点数能降一个数量级。如果只是看结构隔行隔列采样也够用。5. 从能跑到好用点云滤波、网格化与精度验证点云生成出来只是第一步原始点云里混着离群点、噪声和冗余数据直接拿去做测量或者建模基本不能用。我一般会走一遍统计滤波加半径滤波把明显飞出去的点去掉。统计滤波的思路是算每个点到最近 k 个邻居的平均距离假设整体服从高斯分布把距离超过均值加两倍标准差以上的点判为离群点。OpenCV 没有直接的点云滤波接口资源里是用 numpy 手写的几十行代码比装 Open3D 轻量。from scipy.spatial import cKDTree def statistical_outlier_removal(points, k20, std_ratio2.0): tree cKDTree(points) dists, _ tree.query(points, kk1) # 去掉第一个是自己到自己的距离 mean_dists dists[:, 1:].mean(axis1) global_mean mean_dists.mean() global_std mean_dists.std() threshold global_mean std_ratio * global_std mask mean_dists threshold return points[mask], maskk取 20 左右比较稳太小了对局部密度变化敏感太大了计算慢且会误删细节。std_ratio设 2.0 是保守值想滤得狠一点可以降到 1.0但有可能把真实细节也删掉。滤波之后如果要做网格化常见做法是用泊松重建或者贪婪三角化。泊松重建对噪声容忍度高但会平滑掉尖锐边缘贪婪三角化保留细节好但对点云密度均匀性要求高。资源里两种都给了接口按场景选。精度验证这块最直接的办法是拿一个已知尺寸的标准件重建之后量它的尺寸跟真实值对比。我习惯用一个小立方体边长 50mm重建后量三个方向的边长误差在 1% 以内算合格。如果误差大先查标定再查视差最后查 Q 矩阵。单目的话尺度本身就是拟合出来的验证意义不大主要看结构比例对不对。从那以后我每次跑完重建都强制走一遍「标准件验证 → 统计滤波 → 体素下采样 → 再验证」的流程不跳过任何一步。希望帮到你。本文还有配套的精品资源点击获取
返回列表