ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

单目双目三维重建Python源码拆解:从标定到SGBM点云生成

单目双目三维重建Python源码拆解:从标定到SGBM点云生成 简介这是一份面向计算机视觉学习者的单目/双目三维重建算法Python源码包聚焦从二维图像序列恢复场景三维结构这一核心问题。代码按单目与双目两条技术路线组织配有可直接运行的入口脚本、算法实现与多组牛奶、苹果、深度等实拍图像同时包含说明文档和示例结果图便于对照代码理解相机标定、特征匹配、三角测量等关键环节。资源共41个文件以3个Python脚本、34张JPG样本图、2个TXT说明、1个Markdown笔记和1张演示PNG为主压缩包约80MB。已有188人学习浏览整体定位贴近高校实践教学适合计算机、电子信息、数学等专业学生用于课程设计、期末大作业或毕业设计。下载后可直接运行调试也可以替换为自有图像数据或在此基础上扩展立体匹配与点云生成模块深入体验完整三维重建流程。1. 为什么我把这份源码拆了三遍才看懂拿到单目双目视觉三维重建算法python源码.zip的时候我第一反应是又一份「课程设计大礼包」——扁平目录、一堆IMG_20200730_*.jpg手机照片、pan*.jpg、milk*.jpg、apple*.jpg这种命名典型的学生自采数据。但真正跑起来才发现这份资源最值钱的地方不在算法多前沿而在「单目与双目两条重建路径共用一套标定框架」从相机内参标定到 SGBM 立体匹配再到mono.txt/bino.txt里的特征点对应关系全链路能串起来。对于准备做三维重建课程设计、毕设或者想从图像拼接过渡到深度估计的人这是一份可以当脚手架用的工程包——但需要先看懂它的数据组织方式否则连stereoimages这个文件夹是给谁用的都会搞错。接下来的拆解会直接进源码把binocular_v1.py、mono.py和img_mosaic.py的分工讲清楚。2. 三维重建的几何基础从对极约束到视差计算2.1 为什么单目和双目都要先做相机标定三维重建的本质是从二维像素坐标反推三维空间坐标这里绕不开针孔相机模型。像素坐标(u, v)和相机坐标系下的点(X, Y, Z)满足Z * [u, v, 1]^T K * [R|t] * [X, Y, Z, 1]^T矩阵K包含焦距fx, fy和主点cx, cy也就是相机内参。单目重建时需要靠多帧图像之间的相对位姿[R|t]来三角化双目重建则利用两个相机固定基线b的约束直接把深度Z和视差d挂钩Z f * b / d两个公式里的K必须精确否则后面算出的深度图全是系统性偏移。这个项目里binocular_v1.py用棋盘格标定板走的是 OpenCV 官方calibrateCamera流程标定图片就是那十几张IMG_20200730_*.jpg——手机绕着棋盘格转着拍OpenCV 会自动提取角点并计算内参。2.2 对极几何与视差图的搜索策略双目匹配的核心是对极约束左图上的一个像素点在右图上只可能出现在对应的极线上。OpenCV 里stereoBM和StereoSGBM都是沿极线搜索匹配点区别在于匹配代价的计算方式。BMBlock Matching用的是 SAD像素灰度绝对差求和窗口小、速度快但纹理稀疏区域容易出条纹噪声。SGBMSemi-Global Block Matching则是在 BM 基础上加了多方向路径聚合用一个平滑项惩罚视差突变对弱纹理区域的处理好得多。这个项目里binocular_v1.py默认走 SGBM因为手机拍的白墙、桌面这类大面积低纹理区域BM 基本没法看。2.3 立体匹配参数里的三个关键旋钮SGBM 的核心参数如下参数作用项目默认值调整方向numDisparities最大视差范围必须是 16 的倍数64视差范围不够会出现大片黑色空洞加大blockSize匹配窗口边长奇数 3~119窗口越大越平滑但边缘被抹掉P1, P2平滑惩罚系数P2 P18*3*blockSize^2/32*3*blockSize^2纹理弱、噪声大时调大P2uniquenessRatio匹配唯一性阈值10数值越大匹配越严格错误点越少代码里如果出现大片横条纹我一般先看numDisparities是不是太小再看P2是不是太大——前者导致搜索范围不够后者把视差过度平滑。3. 源码拆解binocular_v1.py 的完整双目重建流水线3.1 标定阶段的棋盘格角点提取binocular_v1.py的第一步是读入左右目标定图片提取棋盘格角点。这里有个项目里容易被忽略的细节代码里用的是cv2.findChessboardCorners棋盘格尺寸写死为(7, 6)。如果你的标定板是 9x6 或者 8x6这里不改的话所有标定图片都会被跳过输出全是False。我拆这份源码时第一处改动就发生在这里——把pattern_size改成自己手里的棋盘格并且保证拍摄时棋盘格尽量占满画面角点提取成功率会高很多。提取完角点后用cv2.calibrateCamera计算内参矩阵K和畸变系数dist。手机镜头边缘畸变明显尤其是广角端dist里会包含不小的径向畸变参数k1, k2不校正的话后续立体匹配的极线约束会歪掉。3.2 双目校正与极线对齐立体匹配的前提是左右图像行对齐这一步靠cv2.stereoRectify完成。传入左右内参、畸变系数和两相机间的旋转矩阵R、平移向量T输出左右视图的校正映射矩阵。代码里核心调用是R1, R2, P1, P2, Q, roi1, roi2 cv2.stereoRectify( K1, d1, K2, d2, img_size, R, T, flagscv2.CALIB_ZERO_DISPARITY, alpha0 )alpha0表示校正后裁剪掉黑色边框视差图的有效区域变小但不会有黑边干扰匹配。生成的Q矩阵是重投影矩阵后面从视差图恢复三维坐标直接靠它。校正映射用cv2.initUndistortRectifyMap生成查表再用cv2.remap作用到每一帧图像上。3.3 SGBM 匹配与视差图生成这是整个文件里最核心的一段我把它精简后贴出来def compute_disparity(left_img, right_img): # 转灰度SGBM 在单通道上计算 gray_l cv2.cvtColor(left_img, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(right_img, cv2.COLOR_BGR2GRAY) stereo cv2.StereoSGBM_create( minDisparity0, numDisparities64, # 必须是16的倍数 blockSize9, # 必须是奇数 P18 * 3 * 9 * 9, P232 * 3 * 9 * 9, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32 ) disparity stereo.compute(gray_l, gray_r).astype(np.float32) / 16.0 return disparity最后除以 16 是因为 SGBM 内部用固定点精度视差值放大 16 倍存储还原成像素单位必须除回来。这一步漏掉的话后面所有深度计算结果都会差 16 倍这是这份源码里最容易踩的坑。3.4 从视差到三维点云的坐标系变换得到视差图后用cv2.reprojectImageTo3D结合Q矩阵就能恢复每个像素对应的三维坐标points_3d cv2.reprojectImageTo3D(disparity, Q)输出的points_3d是(H, W, 3)的数组第三维是(X, Y, Z)。但注意无效视差黑色区域的 Z 值是极大值通常需要用np.isfinite()过滤掉。我在这个项目里为了减少重复计算加了一个 maskmask (disparity disparity.min()) np.isfinite(points_3d[:, :, 2]) valid_points points_3d[mask]4. mono.py 与数据文件单目重建的另一条路线4.1 mono.py 在做什么——运动恢复结构的最简实现mono.py走的是单目序列重建路线核心逻辑是对连续两帧图像提取特征点用本质矩阵E估计相对位姿然后三角化出三维点。这里用到的特征提取器是 ORB因为项目自带图片pan1.jpg、pan3.jpg这些是手机绕物体旋转拍摄的ORB 的尺度不变性足够应付这种小幅度的视角变化。关键代码逻辑如下# 提取 ORB 特征 orb cv2.ORB_create(nfeatures2000) kp1, des1 orb.detectAndCompute(img1, None) kp2, des2 orb.detectAndCompute(img2, None) # BFMatcher 暴力匹配用 Hamming 距离 bf cv2.BFMatcher(cv2.NORM_HAMMING, crossCheckTrue) matches bf.match(des1, des2)注意这里用的是BFMatcher而不是FLANN因为 ORB 描述子是二进制特征Hamming 距离计算比欧氏距离快得多crossCheckTrue可以滤掉大量不对称的误匹配。4.2 mono.txt 和 bino.txt 到底存了什么这是理解整个项目数据流的关键。解压后看到mono.txt和bino.txt我一开始以为是匹配结果实际打开才发现是标定过程中记录的关键点像素坐标。bino.txt保存的是双目标定板左右视图的角点像素坐标格式是u_left, v_left, u_right, v_right每行一组mono.txt则是单目序列里相邻帧匹配到的 ORB 特征点对。为什么要单独存这两个文件因为标定和特征匹配的耗时较长把中间结果落盘后调试重建算法本体时不必每次都重新跑一遍匹配——这个设计在课程设计里不多见但对迭代调参非常友好。我把mono.txt读取后直接喂给cv2.findFundamentalMat计算基础矩阵验证了数据格式pts np.loadtxt(mono.txt, dtypenp.float32) pts1 pts[:, :2] # 前一帧特征点 pts2 pts[:, 2:] # 后一帧特征点 F, mask cv2.findFundamentalMat(pts1, pts2, cv2.FM_RANSAC)4.3 为什么单目重建会缺尺度——本质矩阵的局限用cv2.findEssentialMat恢复出的R和t有一个致命问题平移向量t是归一化的真实尺度丢失。也就是说重建出的三维点云形状正确但物体的实际大小和距离未知。mono.py里没有做尺度恢复所以在最终输出点云前需要用参考物体的真实尺寸去反向缩放。项目里的apple*.jpg、milk*.jpg这类图片中如果有已知尺寸的物体就可以用物体像素高度换算出比例因子这是单目重建绕不开的一步。5. 从零复现环境配置、运行顺序与参数调优5.1 一套能跑通的环境配置这份源码依赖项很轻requirements.txt里只涉及 OpenCV、NumPy 和 Matplotlib。我用的配置是Python 3.8 opencv-python 4.5 numpy 1.21 matplotlib 3.5注意不要装opencv-contrib-python和opencv-python共存两个包会冲突SGBM 创建时会报module cv2 has no attribute StereoSGBM_create。如果你用的是 4.x 版本这行调用没问题但网上有些老教程用的是cv2.StereoSGBM()那是 OpenCV 2.x 的旧 API新版本已经移除了。5.2 运行顺序和文件路径不要乱动源码默认从当前目录读图片运行前需要保持原始目录结构# 1. 先用单目标定图片做一次相机内参标定 python mono.py # 2. 用 stereoimages 目录下图片做双目标定 SGBM 深度估计 python binocular_v1.py # 3. 用 pan*.jpg / apple*.jpg 等拼图用于单目序列重建前的预处理 python img_mosaic.py路径写死是这一类源码包的常见问题。binocular_v1.py里图片路径大概率是相对当前工作目录的如果你在别的目录下执行会直接报No such file or directory。我的做法是新建一个data/目录把stereoimages和IMG_20200730_*.jpg复制进去然后改代码里的路径前缀。5.3 深度图质量差时的调整顺序表跑完binocular_v1.py之后视差图可能出现各种问题下面是调试顺序的优先级表现象优先检查调整手段大片黑色空洞numDisparities太小增大到 128 或 256物体边缘毛刺blockSize太小增大到 9 或 11视差图横向条纹P2过大减小到16*3*blockSize^2重复纹理区域错位uniquenessRatio过低提高到 15~20噪点太多speckleWindowSize过小增大到 150~200提示SGBM 在重复纹理区域比如棋盘格、百叶窗几乎无解uniquenessRatio调再高也会有误匹配。遇到这种场景建议换用深度学习匹配方法但本项目守住了传统算法的边界已经够毕业设计用了。6. 进阶玩法把这份源码变成自己的毕设脚手架6.1 用 img_mosaic.py 增强单目重建的输入质量img_mosaic.py本质是一个图像拼接工具把pan1.jpg到pan6.jpg这组围绕物体拍摄的序列图拼成一张大图。对单目重建来说拼接后的宽视角图能提取到更多稳定特征点ORB 匹配的误匹配率明显下降。运行前记得把你的序列图按拍摄顺序重命名因为拼接算法默认按文件名排序决定拼接次序——我一开始用apple1.jpg, apple2.jpg, apple3.jpg顺序没问题但deep1.jpg和deep2.jpg这种双位数命名在排序时会出问题deep10.jpg会排在deep2.jpg前面。6.2 深度图后处理中值滤波加空洞填充binocular_v1.py输出的原始视差图带噪声直接可视化会看到很多胡椒盐噪点。我在复现时加了一段中值滤波和空洞填充# 中值滤波去除孤立噪点 disparity_filtered cv2.medianBlur(disparity.astype(np.uint8), 5) # 空洞填充用周围有效视差的中值替代无效区域 mask (disparity_filtered 0) kernel np.ones((5, 5), np.uint8) dilated cv2.dilate(disparity_filtered, kernel) disparity_filled np.where(mask, dilated, disparity_filtered)中值滤波的窗口不要超过 7否则边缘会严重变粗——近距离物体的轮廓会「膨胀」对后续点云测量精度影响很大。6.3 把点云导出成 PLY 文件用 MeshLab 查看cv2.reprojectImageTo3D得到的points_3d是 NumPy 数组为了直观验证重建效果我把它导出为 PLY 格式。PLY 是 MeshLab 和 CloudCompare 都能直接打开的轻量点云格式代码非常简单def export_ply(points, colors, filename): 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[0]} {c[1]} {c[2]}\n)导出的点云在 MeshLab 里如果看到物体像是「被压扁」的说明Q矩阵的平移基线T符号有误回到stereoRectify的参数检查T[0]的正负号——左相机相对右相机朝左还是朝右直接决定深度是正还是负。沿着这个方向你可以把深度图替换成 RAFT-Stereo 这类深度学习方法或者向后端接一个 TSDF 融合完成从稀疏点云到稠密网格的完整三维重建流程这就是另一个工作量饱和的毕设课题了。本文还有配套的精品资源点击获取
返回列表