ARTICLE DETAIL

资讯详情

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

Python单目与双目视觉三维重建:从标定到点云实战

Python单目与双目视觉三维重建:从标定到点云实战 简介这份资源是面向计算机、通信、人工智能、自动化等专业学生与从业者的单目双目视觉三维重建Python源码源自个人毕业设计项目答辩评审分达98分代码经过调试测试可稳定运行。包内共41个文件以34张jpg图像样本、3个py核心脚本、2个txt说明文件及1个md文档、1个png示意图为主压缩包约80.24MB涵盖单目与双目两套重建流程的完整实现。项目围绕双目立体视觉的标定、校正、匹配与深度计算展开同时提供单目重建的对照方案图像样本可用于验证算法效果脚本结构清晰便于逐模块阅读。已有853人学习下载适合作为期末课程设计、课程大作业或毕业设计的参考模板基础较好的读者可在此基础上调整算法与参数扩展出不同功能具备较高的学习借鉴与二次开发价值。1. 从一张照片到三维点云单目与双目重建到底怎么选手里只有一台普通 USB 摄像头或者一对便宜的双目模组能不能把眼前的场景还原成带尺度的三维点云这是很多做机器人、测量、AR 的工程师真正会问的问题也是「基于 python 实现的单目双目视觉三维重建源码」这个方向最核心的诉求。单目方案硬件成本最低一张图就能跑但天然缺尺度深度靠模型猜或靠运动推双目方案多一个相机靠视差直接算出真实距离代价是标定和立体匹配的工程量翻倍。两条路线不是替代关系而是互补单目适合快速验证和稠密重建的骨架双目适合要绝对尺度的测距和避障。这篇笔记按「先立住原理、再动手复现、最后讲坑」的顺序把两条路线的 Python 实现路径拆开讲清楚新手能照着跑通最小闭环熟手能看到参数边界和翻车点。2. 单目三维重建从标定到稀疏点云的完整链路单目重建的本质是「用二维信息反推三维结构」常见做法分两类一类是单张图的深度估计加相机内参反投影另一类是运动恢复结构SfM靠多视角匹配点三角化。前者上手快但尺度是相对的后者精度高但需要足够的视角变化。我一般先用标定把内参锁死再决定走哪条路因为内参错了后面全错。2.1 相机内参标定棋盘格与参数含义标定的目的是拿到内参矩阵和畸变系数。内参矩阵里的 fx、fy 是焦距的像素表示cx、cy 是主点畸变系数 k1、k2、p1、p2、k3 描述径向和切向畸变。这些参数直接决定反投影的准确性fx 差 5% 深度就偏 5%。import cv2 import numpy as np import glob # 棋盘格内角点数量注意是内角点不是格子数 pattern_size (9, 6) # 棋盘格每个方格的物理尺寸单位毫米这个值决定后续尺度的可信度 square_size 25.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) objp * square_size objpoints [] # 三维世界坐标 imgpoints [] # 二维图像坐标 images glob.glob(calib_images/*.jpg) 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) objpoints.append(objp) imgpoints.append(corners2) # 标定返回内参矩阵、畸变系数、旋转和平移向量 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None) print(内参矩阵:\n, mtx) print(畸变系数:\n, dist)这段代码的逻辑是用已知物理尺寸的棋盘格建立三维点和二维点的对应关系再解算相机参数。pattern_size必须和实际棋盘内角点一致写错一位角点检测直接失败。square_size的单位要和后续重建单位统一用毫米就全程毫米。cornerSubPix的窗口 (11,11) 是经验值图像分辨率高时可以放大到 (15,15)。标定完成后建议用cv2.projectPoints把三维点重投影回图像看平均误差超过 0.5 像素就要重新采集标定图。2.2 单目深度反投影把深度图变成点云拿到内参后单目重建最直接的做法是接一个深度估计模型把每个像素的深度值反投影成三维点。这里不依赖具体模型只讲反投影这一步因为无论深度从哪来公式都一样。import numpy as np import open3d as o3d def depth_to_pointcloud(depth_map, color_img, mtx, depth_scale1000.0): depth_map: HxW 的深度图单位毫米 color_img: HxWx3 的彩色图 mtx: 3x3 内参矩阵 depth_scale: 深度缩放因子把深度值转成米 h, w depth_map.shape fx, fy mtx[0, 0], mtx[1, 1] cx, cy mtx[0, 2], mtx[1, 2] # 生成像素坐标网格 u, v np.meshgrid(np.arange(w), np.arange(h)) z depth_map / depth_scale # 反投影公式x (u - cx) * z / fx x (u - cx) * z / fx y (v - cy) * z / fy points np.stack((x, y, z), axis-1).reshape(-1, 3) colors color_img.reshape(-1, 3)[:, ::-1] / 255.0 # BGR 转 RGB 并归一化 # 过滤无效深度 valid (z.reshape(-1) 0) (z.reshape(-1) 10.0) pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(points[valid]) pcd.colors o3d.utility.Vector3dVector(colors[valid]) return pcd # 假设已有深度图和彩色图 # pcd depth_to_pointcloud(depth, color, mtx) # o3d.visualization.draw_geometries([pcd])反投影的核心就是那两行除法x (u - cx) * z / fx。depth_scale取决于深度图来源用毫米存就填 1000用米存就填 1。valid过滤掉深度为 0 和超过 10 米的点这两个阈值按场景调室内 5 米就够室外要放宽。点云出来后用 Open3D 的voxel_down_sample降采样不然几十万个点可视化会卡。2.3 运动恢复结构多视角三角化的最小实现如果只有普通相机没有深度模型就走 SfM 路线。核心步骤是特征匹配、本质矩阵求解、三角化。下面是最小可跑版本用 ORB 特征加recoverPose。import cv2 import numpy as np def sfm_two_view(img1, img2, mtx): orb cv2.ORB_create(nfeatures2000) kp1, des1 orb.detectAndCompute(img1, None) kp2, des2 orb.detectAndCompute(img2, None) # 暴力匹配加比率测试 bf cv2.BFMatcher(cv2.NORM_HAMMING, crossCheckFalse) matches bf.knnMatch(des1, des2, k2) good [] for m, n in matches: if m.distance 0.75 * n.distance: good.append(m) pts1 np.float32([kp1[m.queryIdx].pt for m in good]) pts2 np.float32([kp2[m.trainIdx].pt for m in good]) # 求本质矩阵阈值 1.0 像素 E, mask cv2.findEssentialMat(pts1, pts2, mtx, methodcv2.RANSAC, prob0.999, threshold1.0) _, R, t, mask_pose cv2.recoverPose(E, pts1, pts2, mtx) # 三角化 P1 mtx np.hstack((np.eye(3), np.zeros((3, 1)))) P2 mtx np.hstack((R, t)) pts4d cv2.triangulatePoints(P1, P2, pts1.T, pts2.T) pts3d (pts4d[:3] / pts4d[3]).T return pts3d, R, t # pts3d, R, t sfm_two_view(img1, img2, mtx)nfeatures设 2000 是速度和精度的折中纹理少的场景要加到 5000。比率测试的 0.75 是 Lowe 论文的经验值调小匹配更严但点更少。findEssentialMat的 threshold 是 RANSAC 内点阈值单位像素图像噪声大就放宽到 2.0。三角化出来的点要做重投影误差筛选误差大的点直接丢不然点云会有一堆飞点。3. 双目三维重建标定、校正、匹配三步走双目比单目多出来的核心是视差。两个相机看同一个点在左右图上的横坐标差就是视差深度等于焦距乘基线除以视差。所以双目的精度直接取决于标定质量和匹配质量。整个流程是双目标定拿内外参立体校正把左右图对齐到同一极线立体匹配算视差图最后反投影。3.1 双目标定内外参一起解双目标定要同时拿到两个相机的内参、畸变以及它们之间的旋转和平移。平移向量的模就是基线这个值直接进深度公式错一点深度就错一片。import cv2 import numpy as np import glob pattern_size (9, 6) square_size 25.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) objp * square_size objpoints [] imgpoints_l [] imgpoints_r [] left_images sorted(glob.glob(left/*.jpg)) right_images sorted(glob.glob(right/*.jpg)) for lf, rf in zip(left_images, right_images): img_l cv2.imread(lf, 0) img_r cv2.imread(rf, 0) ret_l, corners_l cv2.findChessboardCorners(img_l, pattern_size, None) ret_r, corners_r cv2.findChessboardCorners(img_r, pattern_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(img_l, corners_l, (11, 11), (-1, -1), criteria) corners_r cv2.cornerSubPix(img_r, corners_r, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints_l.append(corners_l) imgpoints_r.append(corners_r) # 双目标定 ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F cv2.stereoCalibrate( objpoints, imgpoints_l, imgpoints_r, None, None, None, None, img_l.shape[::-1], flagscv2.CALIB_FIX_INTRINSIC) print(基线(毫米):, np.linalg.norm(T)) print(右相机相对左相机的旋转:\n, R)stereoCalibrate的flags很关键。如果两个相机单独标定过且内参可信用CALIB_FIX_INTRINSIC只优化外参速度快且稳定。如果没单独标定就去掉这个 flag 让它一起优化但需要更多标定图至少 15 对以上。基线np.linalg.norm(T)是深度公式的分母来源标定完一定要打印出来核对和实际卷尺量的值差太多说明标定有问题。3.2 立体校正与视差图让匹配只在水平方向找校正的目的是把左右图重投影到同一平面让对应点只在同一行上这样匹配就从二维搜索降到一维速度和准确率都上来了。import cv2 import numpy as np # 基于上一步的标定结果做校正 ret_l, mtx_l, dist_l, ret_r, mtx_r, dist_r, R, T, E, F \ cv2.stereoCalibrate(objpoints, imgpoints_l, imgpoints_r, None, None, None, None, img_l.shape[::-1], flagscv2.CALIB_FIX_INTRINSIC) R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, img_l.shape[::-1], R, T, alpha0, flagscv2.CALIB_ZERO_DISPARITY) # 生成映射表 map1_l, map2_l cv2.initUndistortRectifyMap( mtx_l, dist_l, R1, P1, img_l.shape[::-1], cv2.CV_16SC2) map1_r, map2_r cv2.initUndistortRectifyMap( mtx_r, dist_r, R2, P2, img_l.shape[::-1], cv2.CV_16SC2) # 对每一帧做校正 img_l_rect cv2.remap(img_l, map1_l, map2_l, cv2.INTER_LINEAR) img_r_rect 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, modecv2.STEREO_SGBM_MODE_SGBM_3WAY) disparity stereo.compute(img_l_rect, img_r_rect).astype(np.float32) / 16.0alpha0表示校正后只保留有效区域alpha1保留全部但会有黑边。numDisparities决定能测的最近距离值越大能测越近但计算越慢必须是 16 的倍数。blockSize是匹配窗口5 适合纹理丰富场景纹理弱就加到 7 或 9但边缘会更糊。uniquenessRatio是唯一性检查10 到 15 之间比较稳调低会引入误匹配。disp12MaxDiff是左右一致性检查设 1 能滤掉大部分错误视差。3.3 视差转深度Q 矩阵与点云生成视差图出来后用reprojectImageTo3D配合 Q 矩阵直接生成三维点这是 OpenCV 封装好的反投影。import cv2 import numpy as np import open3d as o3d # 视差转三维 points_3d cv2.reprojectImageTo3D(disparity, Q) # 过滤无效视差 mask disparity disparity.min() mask mask (disparity num_disp) points points_3d[mask] colors cv2.cvtColor(img_l_rect, cv2.COLOR_BGR2RGB)[mask] / 255.0 pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(points) pcd.colors o3d.utility.Vector3dVector(colors) # 降采样和去噪 pcd pcd.voxel_down_sample(voxel_size0.005) pcd, _ pcd.remove_statistical_outlier(nb_neighbors20, std_ratio2.0) o3d.visualization.draw_geometries([pcd])Q 矩阵是stereoRectify返回的 4x4 矩阵它把视差和像素坐标映射到三维。reprojectImageTo3D输出的点单位跟标定时的square_size一致用毫米就全是毫米。voxel_size按场景尺度调室内 0.005 米合适大场景要放大。remove_statistical_outlier的nb_neighbors和std_ratio是去飞点的关键std_ratio 调小去得更狠但可能削掉真实细节。4. 避坑与排查双目重建里最容易翻车的五件事这一章全是血泪经验每条都按现象、原因、解决写遇到问题直接对号入座。4.1 视差图大片空洞点云缺一块现象视差图里物体边缘和弱纹理区域全是黑色点云对应位置没有点。原因SGBM 在纹理不足时匹配失败或者numDisparities设小了导致近处视差超出范围。解决先把numDisparities加到 16 的倍数上限比如 256看空洞是否减少再调blockSize到 7 或 9弱纹理场景可以加一个前置的cv2.bilateralFilter平滑图像但会损失边缘。4.2 深度值整体偏大或偏小现象重建出来的点云尺度不对物体比实际大一圈或小一圈。原因基线 T 的模和实际不符或者square_size单位不统一。解决打印np.linalg.norm(T)和卷尺量的基线对比差超过 5% 就重新标定检查标定和重建是否用同一套单位毫米和米混用是新手最常见的翻车点。4.3 校正后左右图不对齐现象校正后的左右图同一物体不在同一行视差图完全乱掉。原因标定图数量不够或姿态太单一导致外参不准。解决标定图至少 15 对棋盘格要覆盖图像各个区域包括四角和中心倾斜角度要有变化。标定完用cv2.stereoRectify后画水平线检查对应点不在一条线上就重标。4.4 点云有大量飞点现象点云里到处是离群的噪点主体结构被淹没。原因视差图的错误匹配没有过滤干净。解决开启disp12MaxDiff和uniquenessRatio再加speckleWindowSize和speckleRange做斑点过滤后处理用remove_statistical_outlierstd_ratio从 2.0 往下调但别低于 1.0否则真实点也被删。4.5 单目重建尺度漂移现象单目 SfM 跑出来点云形状对但尺度每次都不一样。原因单目本身没有绝对尺度三角化出来的尺度是任意的。解决要么在场景里放一个已知尺寸的参照物用它的重建尺寸反推全局尺度要么接受相对尺度只用于形状分析不做测量。要绝对尺度就上双目或加 IMU。5. 进阶技巧用 Open3D 做点云配准与网格化点云出来只是半成品真正能用往往要配准多帧和转网格。多帧配准用 ICP网格化用泊松重建这两个是 Open3D 里最实用的进阶操作。import open3d as o3d import numpy as np # 假设有两帧点云 pcd1 和 pcd2 # 粗配准用 FPFH 特征 def preprocess(pcd, voxel_size): pcd_down pcd.voxel_down_sample(voxel_size) pcd_down.estimate_normals( o3d.geometry.KDTreeSearchParamHybrid(radiusvoxel_size * 2, max_nn30)) fpfh o3d.pipelines.registration.compute_fpfh_feature( pcd_down, o3d.geometry.KDTreeSearchParamHybrid(radiusvoxel_size * 5, max_nn100)) return pcd_down, fpfh voxel_size 0.01 pcd1_down, fpfh1 preprocess(pcd1, voxel_size) pcd2_down, fpfh2 preprocess(pcd2, voxel_size) # 全局粗配准 result_ransac o3d.pipelines.registration.registration_ransac_based_on_feature_matching( pcd1_down, pcd2_down, fpfh1, fpfh2, True, max_correspondence_distancevoxel_size * 1.5, estimation_methodo3d.pipelines.registration.TransformationEstimationPointToPoint(False), ransac_n4, checkers[o3d.pipelines.registration.CorrespondenceCheckerBasedOnEdgeLength(0.9), o3d.pipelines.registration.CorrespondenceCheckerBasedOnDistance(voxel_size * 1.5)], criteriao3d.pipelines.registration.RANSACConvergenceCriteria(100000, 0.999)) # 精配准用 ICP result_icp o3d.pipelines.registration.registration_icp( pcd1_down, pcd2_down, voxel_size * 0.5, result_ransac.transformation, o3d.pipelines.registration.TransformationEstimationPointToPlane()) # 合并后做泊松重建 pcd1.transform(result_icp.transformation) combined pcd1 pcd2 combined, _ combined.remove_statistical_outlier(nb_neighbors20, std_ratio2.0) combined.estimate_normals( o3d.geometry.KDTreeSearchParamHybrid(radiusvoxel_size * 2, max_nn30)) mesh, densities o3d.geometry.TriangleMesh.create_from_point_cloud_poisson( combined, depth9) # 按密度裁剪低置信区域 densities np.asarray(densities) vertices_to_remove densities np.quantile(densities, 0.01) mesh.remove_vertices_by_mask(vertices_to_remove) o3d.visualization.draw_geometries([mesh])voxel_size是配准的基准尺度设成点云平均点距的 2 到 3 倍比较稳。max_correspondence_distance在粗配准里设 1.5 倍 voxel_size精配准里设 0.5 倍这个比例关系比绝对值更重要。泊松重建的depth9控制细节层次9 适合中等细节11 以上会非常慢且容易过拟合噪声。np.quantile(densities, 0.01)裁掉密度最低的 1%这是去泊松重建边缘伪影的常用手法。参数速查表参数典型值作用调整方向numDisparities16 的倍数80-256视差搜索范围近处测不到就加大blockSize5-9SGBM 匹配窗口纹理弱加大边缘糊减小uniquenessRatio10-15唯一性检查误匹配多就加大voxel_size0.005-0.02 米降采样和配准尺度按场景尺度调depth (泊松)8-10网格细节层次细节不够加大噪声多加小我自己的习惯是每次重建完先看视差图的直方图如果大部分视差挤在最小值附近说明numDisparities或基线有问题先别急着调后处理。双目这套东西标定占七成匹配占两成后处理只占一成标定没做好后面全是白费功夫。希望帮到你。本文还有配套的精品资源点击获取
返回列表