ARTICLE DETAIL

资讯详情

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

基于Python的双目立体视觉与三维重建:从标定到点云生成

基于Python的双目立体视觉与三维重建:从标定到点云生成 简介双目立体视觉是计算机视觉的关键技术利用两个相机模拟人眼通过计算图像视差恢复场景深度。其核心原理基于三角测量依赖相机标定获取的焦距、基线等内参与外参。在工程实现中SGBM立体匹配算法在精度与速度间取得良好平衡生成视差图后结合重投影矩阵可进一步得到三维点云。这项技术广泛应用于机器人导航、AR测距、工业尺寸测量等场景能够以低成本替代深度传感器。以Python为工具可以高效实现从相机标定、立体校正、立体匹配到点云生成的完整流程同时针对参数调优和常见误差问题也有一套可复用的工程排查思路。 拿到这个基于python的双目立体视觉及三维重建.zip压缩包的时候我第一反应是——又一份看起来什么都写了跑起来什么都不是的毕设工程但解开压缩包翻了核心代码之后我得承认这确实是一份少见的、能把双目视觉完整链路串起来的工程。它里面有相机标定脚本、立体校正、SGBM匹配、点云生成整套流程不是那种只有 demo 玩具级的东西而是能真正跑出三维坐标的。这个工程解决的是什么问题呢一句话说清楚用两个普通摄像头模拟人眼通过两张有视差的图片算出场景里每个像素的深度最后把二维图像还原成三维点云。它是机器人导航、AR/VR 测距、无人驾驶、物体尺寸测量这些场景的基础功底。适合谁想入门三维视觉的学生、做毕设的研究生、以及在工业现场想用廉价方案替代深度相机的工程师这份代码都能给你一个不错的起点。开头我把话说透双目立体视觉的本质就是三角测量不需要昂贵的激光雷达不需要结构光只需要两个相机加一堆数学公式。Python 在这里的价值在于它让整个流程的原型验证变得极其高效。下面我按项目实际推进的顺序把每一环的原理、代码、踩坑点都摊开讲清楚。1. 项目整体设计与技术选型拆解1.1 双目立体视觉到底在解决什么问题人眼之所以能感知深度是因为两只眼睛看的视角有一点点不同大脑根据这个差别视差推断距离。双目立体视觉就是把这个生物学过程工程化。项目里核心的思路是先让两个相机观察同一个场景分别拿到左图和右图然后找到左右图中对应的像素点计算它们在水平方向上的坐标差——就是视差再通过相机标定出来的几何参数把视差换算成真实的三维坐标。这里有个容易混淆的概念要提前说清楚视差和深度不是线性关系是反比关系。也就是说物体离相机越近视差越大离相机越远视差趋近于零。这个反比关系决定了双目方案的测量特性——近距离精度高远距离精度衰减得非常快。这也是项目里为什么要强调基线距离两个相机光心之间的距离的原因基线拉得越长同样距离下视差越大测距越准但代价是两个相机的公共视野会变小近距离遮挡问题也更严重。工程上这是一个需要权衡的点。项目的整体流程可以用一句话串起来图像采集 - 相机标定 - 立体校正 - 立体匹配 - 视差计算 - 三维重建。每一步的输出是下一步的输入任何一个环节出问题后面全崩。我在调试项目的时候最深的感受就是不能跳步标定不准后面全是白算。1.2 为什么选 Python 而不是 C很多做工业视觉的老工程师一看到 Python 就摇头觉得性能不够。但我个人认为选 Python 做双目视觉的原型和教学是性价比极高的选择。原因有三条。第一生态成熟。OpenCV 的 Python 接口把标定、校正、匹配这些高难度算法全部封装好了你不需要从零写特征提取、极线几何、最小二乘优化这些折磨人的底层代码可以把精力聚焦在管线的理解和参数调试上。第二开发效率高。C 写一个相机标定流程光编译、链接、处理内存泄漏就够喝一壶的Python 从写代码到看到结果十几分钟就能跑通一个环节。做算法验证的时候这个速度优势是碾压性的。第三数据分析和可视化的配合好。三维重建出来之后用 NumPy 直接处理点云数据用 Matplotlib 或者 Open3D 画三维散点图比 C 里调 PCL 可视化库省太多事。当然不是没有代价。Python 的 GIL 锁和多线程效率问题在实时性要求高的场景比如实时避障会非常头疼后面我在性能优化那部分会单独讲怎么用多进程和多线程绕过这个瓶颈。结论是做研究、做原型、做教学Python 完胜做产品落地可以先用 Python 趟通流程再移植到 C。2. 环境搭建与相机准备2.1 Python 环境与依赖安装这个项目对 Python 版本不算挑剔我自己在 Python 3.8 到 3.11 上都跑过核心依赖就三个OpenCV、NumPy、Matplotlib。安装命令给到你们pip install opencv-python opencv-contrib-python numpy matplotlib open3d这里有几个细节提醒一下opencv-contrib-python 一定要装因为 SGBM 的完整实现在 contrib 包里只装基础版会有部分函数缺失。另外如果用的是 Python 3.11 以上版本注意检查 opencv-python 的版本号太老版本的 wheel 可能没有对应系统的预编译包导致安装失败升级到最新版即可。装完跑一句验证import cv2 print(cv2.__version__)能打印出版本号就说明环境没问题。我在给团队配环境时遇到过最典型的坑是conda 环境和 pip 环境混用导致cv2能 import 但版本是旧的SGBM 参数类型对不上直接报错。建议用虚拟环境隔离项目依赖virtualenv 或者 conda 都行别图省事直接装系统级的 Python 里。2.2 相机选型与固定支架的做法这个项目用的是两个普通的 USB 摄像头就是那种几十块钱的货。选相机时我建议注意三点尽量选同型号同批次的两个摄像头保证成像参数一致省去很多后期的色彩统一工作。分辨率至少 640x480最好 1280x720。分辨率太低标定板角点检测都会失败。支持手动固定曝光和白平衡的更好后面采集图像时自动曝光会导致左右图亮度不一致严重影响匹配效果。固定支架是个容易被忽略的坑。两个摄像头的光轴要尽量平行高度差越小越好。我用的是 3D 打印的支架加上一块铝板基线距离设了 60mm这个距离对于室内场景的物体测距比较合适。如果基线太长近距离物体在左右图中重叠区域太少太短远距离视差太小深度分辨力不够。采集图像时还要注意场景的纹理丰富度。我在调试时发现对着白墙拍出来的标定图像角点检测经常失败因为墙面没有纹理棋盘格的角点被模糊掉了。标定时要保证棋盘格纹路清晰、光照均匀不要有强烈的反光。3. 相机标定三维重建的第一道门槛3.1 标定背后的数学原理相机标定在很多初学者看来就是个黑盒拍一堆棋盘格照片调用函数得到矩阵。但如果你不理解里面的数学后面参数调错你根本不知道错在哪。标定要解决的核心问题是把三维世界坐标和二维图像坐标之间的关系建立起来。这个关系可以拆成两部分。第一部分是相机的内参包括焦距 fx、fy光心坐标 cx、cy还有透镜畸变系数 k1、k2、p1、p2有些镜头还要算 k3。内参是相机自身的属性换了相机就得重新标定。第二部分是相机在世界坐标系中的位姿也就是外参包括旋转矩阵 R 和平移向量 T。双目标定除了分别标定左右相机的内参还需要标定两个相机之间的相对位置关系R 和 T这是后面立体校正和深度计算的关键。标定的数学推理基础是针孔模型和单应性矩阵。棋盘格上的每个已知尺寸的角点经过相机投影到图像上投影关系可以用一个 3x3 的单应矩阵来描述s * [u, v, 1]^T K * [r1, r2, t] * [X, Y, 1]^T其中[X, Y]是棋盘格上的坐标[u, v]是图像坐标K是内参矩阵r1, r2是旋转矩阵的前两列t是平移向量。拍摄多张不同角度的棋盘格照片就能联立出足够多的方程用最小二乘法解出内参和外参。OpenCV 的cv2.calibrateCamera函数内部做的就是这件事。3.2 棋盘格标定的完整实操流程标定的实操流程我建议严格按照以下步骤走不要跳步第一步采集标定图像。打印一张 9x6 的棋盘格内部角点数是 8x5这个是 OpenCV 的默认索引方式注意别搞混贴在一个平整的硬板上。左右相机同时拍摄棋盘格在不同角度、不同距离、不同位置的图像我一般采集 20 到 30 对。关键点棋盘格要在视野的各个区域都出现过尤其是图像边缘因为畸变在边缘最明显边缘的样本不够畸变系数就拟合不准。第二步角点检测。对每张图像用cv2.findChessboardCorners找角点然后亚像素细化import cv2 import numpy as np # 棋盘格内部角点数 pattern_size (8, 5) # 棋盘格每个格子的实际边长单位mm 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 object_points [] image_points_left [] image_points_right [] left_images [...] # 左相机图像列表 right_images [...] # 右相机图像列表 for lpath, rpath in zip(left_images, right_images): img_l cv2.imread(lpath, cv2.IMREAD_GRAYSCALE) img_r cv2.imread(rpath, cv2.IMREAD_GRAYSCALE) 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_sub cv2.cornerSubPix(img_l, corners_l, (11, 11), (-1, -1), criteria) corners_r_sub cv2.cornerSubPix(img_r, corners_r, (11, 11), (-1, -1), criteria) object_points.append(objp) image_points_left.append(corners_l_sub) image_points_right.append(corners_r_sub)注意square_size参数必须填实际测量的边长单位 mm这个值直接决定三维重建的尺度。如果只是做形状重建填多少都行但如果要测真实尺寸这里必须是真实物理尺寸。第三步单目标定。分别对左右相机调用cv2.calibrateCamera得到各自的相机矩阵和畸变系数ret_l, K_l, dist_l, rvecs_l, tvecs_l cv2.calibrateCamera( object_points, image_points_left, img_l.shape[::-1], None, None ) ret_r, K_r, dist_r, rvecs_r, tvecs_r cv2.calibrateCamera( object_points, image_points_right, img_r.shape[::-1], None, None )第四步双目标定。用cv2.stereoCalibrate计算两相机之间的旋转矩阵 R 和平移向量 Tret, K_l, dist_l, K_r, dist_r, R, T, E, F cv2.stereoCalibrate( object_points, image_points_left, image_points_right, K_l, dist_l, K_r, dist_r, img_l.shape[::-1], criteria(cv2.TERM_CRITERIA_MAX_ITER cv2.TERM_CRITERIA_EPS, 100, 1e-6), flagscv2.CALIB_FIX_INTRINSIC )3.3 焦距计算公式的推导与含义项目里涉及一个三维重建里最核心的公式——深度计算公式Z f * B / d其中 Z 是物体到相机平面的距离f 是焦距B 是基线距离两相机光心的距离d 是视差。这个公式很多人会背但不理解为什么我来推导一下。假设两个相机光轴平行并且处于同一水平高度左相机光心在(0, 0)右相机光心在(B, 0)。一个三维空间点(X, Y, Z)在左相机成像平面上的坐标是(u_l, v)在右相机成像平面上的坐标是(u_r, v)。根据针孔相机的投影关系u_l f * X / Z cx u_r f * (X - B) / Z cx两式相减得到u_l - u_r f * B / Z而u_l - u_r正是视差 d。所以Z f * B / d这个推导非常直观但也非常关键——它揭示了三维重建精度的三个决定因素焦距 f、基线 B、以及视差 d 的精度。项目代码里就是这样做的标定获取焦距 f 和基线 B立体匹配计算出视差图 d然后逐像素算出深度 Z。理解了公式你在调试时就能有方向如果测距不准要么是标定的 f 和 B 有误差要么是视差图 d 在对应区域匹配错了。4. 立体校正让两张图变成一对好兄弟4.1 极线约束与重投影标定得了外参之后下一步就是立体校正。为什么要校正因为两个相机很难做到绝对平行左右图像中同一个点的纵坐标v 坐标大概率是不一致的导致在右图中搜索匹配点时你不知道该在哪个 y 坐标上去找。极线约束告诉我们左图像上的一个点在右图像上的对应点一定在一条直线上极线。如果左右图像经过校正变成行对齐的状态那么极线就是水平线匹配搜索直接从二维搜索变成一维搜索计算量大幅下降匹配准确率也上升。立体校正在 OpenCV 中用的方法是cv2.stereoRectify。它接收双目标定的 R 和 T输出两个单应性矩阵 R_l、R_r 以及投影矩阵 P_l、P_r然后把左右图像分别重投影到公共的像平面上使两幅图像的极线平行且对齐。这里有一个关键点stereoRectify之后还要用initUndistortRectifyMap和remap把原始图像重映射一遍。很多人只调了stereoRectify没做 remap结果后面视差图完全错乱。重映射的本质是对校正后图像中的每个像素根据校正映射表找到原始图像中对应的像素值填进来形成一张校正图。4.2 立体校正代码实战下面这段是项目里立体校正的完整流程我整理过可以直接用def stereo_rectify(K_l, dist_l, K_r, dist_r, R, T, image_size): # 图像尺寸 (width, height) h, w image_size # 计算校正变换 R_l, R_r, P_l, P_r, Q, roi_l, roi_r cv2.stereoRectify( K_l, dist_l, K_r, dist_r, (w, h), R, T, alpha0, # alpha0 裁剪到最小有效区域alpha1 保留全部像素 newImageSize(w, h) ) # 计算映射表 map_l_x, map_l_y cv2.initUndistortRectifyMap( K_l, dist_l, R_l, P_l, (w, h), cv2.CV_32FC1 ) map_r_x, map_r_y cv2.initUndistortRectifyMap( K_r, dist_r, R_r, P_r, (w, h), cv2.CV_32FC1 ) return (map_l_x, map_l_y), (map_r_x, map_r_y), Q def apply_rectify(img_l, img_r, maps): (map_l_x, map_l_y), (map_r_x, map_r_y) maps rect_l cv2.remap(img_l, map_l_x, map_l_y, cv2.INTER_LINEAR) rect_r cv2.remap(img_r, map_r_x, map_r_y, cv2.INTER_LINEAR) return rect_l, rect_r提醒几个细节。alpha参数是校正区域裁剪的选择alpha0会裁剪掉校正后图像中的无效区域图像会变小一点但看起来更干净alpha1会保留所有原始像素区域边缘会出现黑边。我建议调试阶段用alpha0因为黑边会影响 SGBM 匹配。校正的效果怎么验证一个很直观的方法在两张校正后的图像上用鼠标同步显示同一 y 坐标的像素值如果校正成功左右图上同一个特征点的 y 坐标应该一致。另一个快速方法是把左右图水平拼接用cv2.addWeighted叠加显示成伪彩色如果校正成功同一个物体在两张图中看起来是对齐的没有明显的倾斜。我日常调试用的是画水平参考线的方法校正后取几张特征明显的图手动观察几个特征点的 y 坐标是否一致不一致就说明 R、T 标定有问题。5. 立体匹配与视差计算5.1 SGBM 算法的核心思想立体匹配是整个三维重建链路里最耗时、也最容易出效果的环节。它做的事情是对于左图像上的每一个像素在右图像中搜索它的同名点搜索的结果就是视差图。匹配算法有很多种从最简单的块匹配BM到全局优化算法项目里选的是 SGBMSemi-Global Block Matching半全局块匹配。SGBM 的核心思想我会用一个生活化的例子讲。想象你要在一个大商场里找到你的朋友BM 算法就是只在同一个楼层当前像素的行快速扫一眼找到长得最像的但很容易认错人全局算法是把整个商场翻个底朝天找到最确定的但太耗时。SGBM 是折中方案不仅考虑当前像素周围一小块区域的相似度还把相邻扫描线上的代价信息传过来用动态规划的方式平滑地找到最优解。它比 BM 准确比全局算法快几个数量级是工程上最好的平衡点。在 OpenCV 中调用 SGBM 的代码如下def compute_disparity(img_l, img_r, num_disparities160, block_size11): # 需要灰度图 gray_l cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY) sgbm cv2.StereoSGBM_create( minDisparity0, numDisparitiesnum_disparities, # 必须是16的倍数 blockSizeblock_size, P18 * 3 * block_size ** 2, P232 * 3 * block_size ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32 ) disparity sgbm.compute(gray_l, gray_r).astype(np.float32) / 16.0 return disparity5.2 参数调优的实战经验SGBM 的参数调整是最考验经验的地方。我总结几个核心参数的影响规律贴在这里方便你们对照排查参数作用调大后的效果调小后的效果numDisparities最大视差搜索范围远距离看得更远计算量增大近距离视差被截断近处全是洞blockSize匹配窗口大小视差图更平滑但细节丢失细节保留但噪点增多P1、P2平滑惩罚系数视差变化更平滑边界容易被糊掉边界清晰但噪声更明显uniquenessRatio唯一性比例匹配更严格误匹配减少视差图更密集但错误点增多speckleWindowSize去除小斑点区域过滤掉更多小块噪声保留更多细节但噪声也多一个我踩过很多次的坑是blockSize必须是奇数numDisparities必须是 16 的倍数。这两个条件不符合就直接报错或者静默地输出全零。P1和P2的初始值我一般按上面的公式来P1 8 * 3 * blockSize^2P2 32 * 3 * blockSize^2。P2 比 P1 大很多是因为它要惩罚的是大幅度的视差跳变比如物体边缘而小幅度的变化平滑区域用较小的惩罚就够了。P2 设置太小视差图会支离破碎P2 设置太大物体内部会变成一个大平板丢失曲率变化。调参顺序我建议从大到小先把numDisparities调到接近物体最大距离对应的视差再看blockSize能不能让视差图平滑起来最后微调P1、P2和uniquenessRatio。不要一上来就全参数乱改出了问题你不知道是哪个参数引起的。6. 三维重建从视差图到点云6.1 三角测量与深度计算拿到了视差图三维重建其实就剩最后一步——逐像素计算三维坐标。这里有两种方式。第一种是用重投影矩阵 Q。在cv2.stereoRectify的输出中有一个 4x4 的矩阵 Q它的作用是把(u, v, d, 1)映射成(X/W, Y/W, Z/W, 1)。用cv2.reprojectImageTo3D一句话就能生成三维点云points_3d cv2.reprojectImageTo3D(disparity, Q)这个函数会返回一个和原图一样大小的三维坐标图每个像素存着该点在相机坐标系下的(x, y, z)坐标。第二种是根据前面推导的深度公式手算。这种方式的优点是你能完全理解每一步在干什么调试时也更容易定位问题def reproject_to_3d(disparity, K_l, B): h, w disparity.shape fx K_l[0, 0] fy K_l[1, 1] cx K_l[0, 2] cy K_l[1, 2] # 构建像素坐标网格 u, v np.meshgrid(np.arange(w), np.arange(h)) u u.astype(np.float32) v v.astype(np.float32) # 计算深度Z f * B / d # 注意SGBM输出的是16倍缩放后的值前面除以16过了 valid disparity 0 Z np.zeros_like(disparity) Z[valid] (fx * B) / disparity[valid] # 反投影到三维坐标系 X np.zeros_like(disparity) Y np.zeros_like(disparity) X[valid] (u[valid] - cx) * Z[valid] / fx Y[valid] (v[valid] - cy) * Z[valid] / fy return X, Y, Z这里有个大坑必须提醒SGBM 输出的视差是固定小数点格式像素值需要除以 16 才得到真实视差值。如果没有除以 16算出来的深度会整体缩小 16 倍三维模型看起来就像被压扁了一样。我见过很多人卡在这个问题上以为是标定错了实际上就是差了一个divide by 16.0。6.2 点云生成与可视化拿到 X、Y、Z 三个数组之后有效深度范围内的像素点就可以写成点云文件了。项目里用 Open3D 来做可视化代码非常简洁import open3d as o3d import numpy as np def disparity_to_point_cloud(disparity, K_l, B, min_depth0.3, max_depth10.0): X, Y, Z reproject_to_3d(disparity, K_l, B) # 过滤无效点和深度超范围的点 mask (Z min_depth) (Z max_depth) np.isfinite(Z) points np.stack([X[mask], Y[mask], Z[mask]], axis-1) # 创建点云对象 pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(points) return pcd # 使用 pcd disparity_to_point_cloud(disparity, K_l, baseline_mm) o3d.visualization.draw_geometries([pcd], window_name3D Reconstruction)可视化这一步是检查整个管线是否走通的试金石。如果你看到的点云轮廓与真实物体基本一致说明前面的标定、校正、匹配都是对的如果点云形变严重或者一片混乱八成是标定参数错了而不是匹配的问题。还有一个实操经验分享一下点云生成后如果不做滤波会有一层非常难看的毛刺。这些毛刺来自深度不连续区域比如物体边缘的误匹配。最简单的处理就是加一个中值滤波到视差图上或者用 Open3D 的remove_statistical_outlier函数去除离群点。我习惯先对视差图做一次cv2.medianBlur再用统计滤波去噪点云质量会提升好几个档次。7. 常见问题与排查技巧实录7.1 标定重投影误差过大的排查cv2.calibrateCamera返回值中的ret就是重投影误差单位是像素。经验上这个值小于 0.5 说明标定很好0.5 到 1.0 是可用的大于 1.0 就需要重新采集图像了。如果重投影误差一直很大我从经验上总结主要原因有三类第一类是标定板的问题。棋盘格贴得不平、表面有反光、格子尺寸测量错误都会让角点位置不准。第二类是图像质量问题。图像模糊、光照不均、棋盘格在图像中占比太小我一般要求棋盘格占图像面积的 1/4 以上都会导致角点检测精度下降。第三类是标定图像数量和质量不够。少于 15 对图像或者所有棋盘格都集中在画面中央畸变参数就拟合不准要把棋盘格摆到画面的各个位置。7.2 视差图出现大面积空洞视差图上的黑色空洞意思是某个区域 SGBM 找不到可靠的匹配。空洞出现的位置和原因各不相同排查思路也不一样低纹理区域空洞白墙、天空、纯色桌面的中央这些区域的信息量太少SGBM 找不到可靠的匹配。解决办法是接受它——这些区域本来就不适合双目重建或者用纹理投影仪打一点结构光。遮挡区域空洞物体边缘的一小条竖条因为两个相机视角不同左图能看到但右图被遮挡的区域天然没有对应点。这个无法根治但disp12MaxDiff参数设置合理可以过滤掉一些错误匹配的噪点。曝光不一致导致的空洞左右图亮度差异太大时匹配代价很高SGBM 会选择宁可不要而不是猜一个。可以尝试对图像做直方图均衡化或者自适应直方图均衡化CLAHE来统一亮度。7.3 重建点云严重变形点云整体形状不对一般不是匹配的问题而是几何参数的问题。我排错时按以下优先级检查第一步检查焦距 f 和基线 B 的单位是否一致。我在项目里用过单位混合的错误——焦距是像素单位标定直接给出的基线却是毫米算的时候忘了把基线换算成像素结果整个模型尺寸偏到离谱。第二步检查视差是否除以 16。这个前面强调过了很多人栽在这里。第三步检查校正是否有效。如果极线没有对齐SGBM 匹配到的同名点就不是真正对应的点重建结果会像是有重影。用投影变换验证是很靠谱的方法别跳步做校正。第四步检查numDisparities是否覆盖了实际视差范围。如果最大视差设置得太小近处物体的视差被截断三维模型看起来会是前面被削平的状态。8. 性能优化与真实场景应用的扩展思路8.1 单线程慢试试多进程和多线程配合Python 的双目视觉在实时性上确实吃亏。SGBM 在 640x480 分辨率下单帧大概要花 100 到 300 毫秒达不到实时要求。我之前尝试过几种优化手段实测效果最好的是把图像采集、立体匹配、点云生成分别放在不同的进程里通过队列通信。摄像头采集是一个进程SGBM 匹配和点云生成是另一个进程这样采集不会阻塞计算计算完再把结果送出来。配合 multiprocessing 模块稳定跑 15 到 20 帧每秒是没问题的。from multiprocessing import Process, Queue import cv2 import time def capture_worker(cap_left, cap_right, queue): while True: ret_l, frame_l cap_left.read() ret_r, frame_r cap_right.read() if ret_l and ret_r: queue.put((frame_l, frame_r)) def compute_worker(queue, sgbm, Q, out_queue): while True: frame_l, frame_r queue.get() gray_l cv2.cvtColor(frame_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(frame_r, cv2.COLOR_BGR2GRAY) disp sgbm.compute(gray_l, gray_r).astype(np.float32) / 16.0 points cv2.reprojectImageTo3D(disp, Q) out_queue.put((frame_l, points))这里要注意的坑是多进程里创建摄像头对象一定不能跨进程共享。每个进程里要重新cv2.VideoCapture否则视频流会莫名其妙地卡住或者黑屏。另外队列要设置合适的最大长度防止生产者太快把内存打爆。8.2 从点云到应用测距、测量、目标识别三维重建点云比深度图多了两个维度的信息可以在真实场景中做很多事情。第一个应用是任意两点距离测量。用户点击点云中的两个点代码计算出它们的三维坐标欧氏距离。我有一次测试在 1 米距离上测一个纸箱的长宽高误差控制在 5% 以内这个精度在民用场景是够用的。应用场景包括物流体积测量、工件尺寸检测等。第二个应用是目标识别与抓取定位。在点云中做平面分割RANSAC提取出目标物体所在的平面再计算目标的质心和朝向可以作为机械臂抓取的粗略定位输入。当然工业级精度还需要配合其他传感器。第三个应用是三维模型重建。对多个视角的点云做配准ICP可以拼接出物体的完整三维模型。这个方向我还在探索中但已经能用 Open3D 的registration_icp把两个视角的点云拼得比较齐。8.3 项目的可扩展方向后续还能怎么改跑通这个项目之后还有几条路可以继续深入。如果对精度有更高要求可以考虑更换更高分辨率的工业相机增加基线距离用结构光或者主动立体视觉来补充低纹理区域的匹配。如果对速度有更高要求可以考虑把 SGBM 的匹配参数调小用 CUDA 加速版 OpenCV或者降分辨率到 320x240 配合多进程。如果对鲁棒性有更高要求可以尝试引入深度学习立体匹配网络比如 RAFT-Stereo在复杂纹理场景下准确率比传统方法高一个级别代价是需要 GPU。我个人在实际操作中的体会是这个项目最难的部分不是三维重建本身而是把整个管线的各个环节都调通并理解它们之间的耦合关系。标定、校正、匹配、重建每一环的错误都会往后传导而且表现方式完全不同。建议读者在改代码时每次只动一个参数观察输出变化慢慢培养对误差的直觉。最后再分享一个小技巧所有标定和匹配参数不要只写在脚本里建议保存成 JSON 或者 YAML 配置文件。这样每次采集新场景只需要改配置文件不用动代码。还有标定结果和采集的原始图像最好打个包存档后面如果发现三维重建有问题还可以回看是标定阶段就出错了还是匹配阶段的问题。这是我踩过几次坑之后养成的习惯希望对你们也有用。本文还有配套的精品资源点击获取
返回列表