☰
单目双目视觉三维重建Python源码:深度估计、视差计算与点云生成一次跑通
2026/10/1 11:46:18 网站建设 项目流程

简介:一套聚焦单目与双目三维重建的Python源码实现,面向计算机视觉课程设计、毕业设计或期末大作业场景,也适合作为算法起步者的参考项目。代码梳理出两种重建路径:单目部分(mono.py)利用单张图像推算深度线索,双目部分(binocular_v1.py)则基于立体图像对计算视差,另有图像拼接脚本(img_mosaic.py)用于多视角数据预处理。压缩包共41个文件,主体为3个Python源程序、34张JPG测试图像(覆盖苹果、牛奶、深景、全景等典型场景)、2个说明文本、1份README文档和1张演示效果图,整体约80.22MB,结构清晰、便于对照学习。目前已有188人浏览学习。源码保留了完整工程路径,适合有基础、愿意自行调试的读者;通过阅读代码并结合图像数据,可以逐步理解特征匹配、视差计算、深度恢复等关键环节,是课程实践与科研入门的实用参考。

1. 单目双目视觉三维重建:一套 Python 源码把深度估计、视差计算和点云输出一次跑通

做课程设计最怕的不是不懂单目双目视觉三维重建的原理,而是拿到一份源码后不知道从哪下手。这份单目双目视觉三维重建算法 Python 源码包,走的正是大多数课设项目需要的那条路:一份代码同时覆盖单目和双目两条重建路线。

包里 mono.py 管单目,配合 mono.txt 里的相机参数和牛奶盒、面包、苹果的多角度测试照片,完成单目测距和多视角三角化;binocular_v1.py 管双目立体视觉,配合 stereoimages 目录和 bino.txt,完成视差计算与点云生成;img_mosaic.py 负责把结果拼成对比图,方便写进报告。demo1.png 就是作者留的效果样例。

这包东西能解决的是“三维重建课设从零到出图”这件事,适合计算机、数学、电子信息等专业做课程设计、期末大作业和毕设参考,也适合想透过源码理解深度估计到底怎么落地的初学者。下面按拆文件、跑单目、跑双目、排错、换自己数据的顺序讲。

2. 先拆文件:mono.py、binocular_v1.py 与 20 张标定图的角色分工

2.1 从文件名反推工程流程:谁是主程序,谁是数据,谁是辅助脚本

拿到压缩包,第一件事不是急着开 IDE,而是先解压看一眼文件布局。这个包解压后的顶层结构不算复杂:mono.py、binocular_v1.py、img_mosaic.py 三个 Python 脚本排在最前面,mono.txt、bino.txt 两个文本参数文件紧随其后,然后是 stereoimages 目录、demo1.png 效果图、一批 IMG_20200730 开头的手机照片,以及 milk、pan、apple、deep 四组物体测试图。这种命名方式在课程设计源码里很典型:主程序按功能拆文件,参数和数据平铺在根目录,不像工程化项目有严格的 src、data 分层。

按功能分,mono.py 吃单目数据,输入是 milk1.jpg、pan3.jpg 这种多视角物体照片,配合 mono.txt 里的相机内参做特征匹配和三角化,输出的是稀疏三维点;binocular_v1.py 走双目立体视觉路线,输入是 stereoimages 里的左右视图,输出视差图和稠密点云;img_mosaic.py 不参与重建主链路,干的是图像拼接和马赛克拼图,常见用途是把多视角测试图或者重建前后的结果排成一张对比图,最后放进课程设计报告里当效果图。demo1.png 就是这种对比图的现成样例。

稍微琢磨一下文件名还能看出工程痕迹:binocular_v1.py 带 v1 后缀,说明作者改过至少一版;IMG_20200730 系列带精确时间戳,说明是同一时间段内用手机连续拍摄的素材;带“副本”字样的文件名是 Windows 下复制文件时系统自动加的。这些信息对后续跑代码很重要,尤其“副本”文件会干扰按目录遍历的脚本,我一般会先把它过滤掉再谈运行。

2.2 数据目录说明:stereoimages 与 IMG_20200730 序列的使用边界

IMG_20200730 开头的那批照片一共 20 来张,文件名形如 IMG_20200730_154841.jpg,中间的时间戳覆盖了 154832 到 154858。这类素材在双目视觉项目里有两种典型用途:一是当标定素材,拍棋盘格然后算内参;二是当立体匹配的输入,把同一时刻左右相机拍的两张图喂给匹配算法。考虑到包里已经有 bino.txt,这批图更可能是已经配好参数的场景照片,而不是原始标定板。

stereoimages 目录则是整理后的标准输入。很多课程设计代码的做法是固定从该目录读 left.jpg 和 right.jpg,或者按文件名排序取前两个文件。binocular_v1.py 读取时大概率也依赖这种约定。这里有个必须注意的使用边界:IMG 原始照片和 stereoimages 整理后的照片不要混用,前者带着畸变和拍摄时间差,后者是已经按双目约定摆好的图像对。混用的结果通常是视差图全是噪声,你还要回头怀疑算法有问题。

我把这种文件结构的经验总结成一张表,方便对照:

文件/目录角色使用注意
mono.py单目重建主程序配合 mono.txt,输入为多视角物体图
binocular_v1.py双目重建主程序配合 bino.txt,输入为 stereoimages
img_mosaic.py图像拼接辅助脚本常用于报告对比图,不参与重建
mono.txt / bino.txt相机参数文件换数据必须同步考虑是否更换
stereoimages/立体图像对左右顺序易踩坑
IMG_20200730_*.jpg原始采集照片含副本文件,先过滤

目录结构理清后,接下来是环境。环境问题看着小,却是最先卡住人的地方。

2.3 依赖环境与跑通顺序:OpenCV 版本决定重建结果

这份源码依赖的东西很常规:OpenCV、NumPy、Matplotlib 三件套。关键点是 OpenCV 的版本。binocular_v1.py 里如果用了 cv2.StereoSGBM_create,OpenCV 需要 3.4 以上;如果用了 cv2.SIFT_create,需要 4.4 以上才能直接调。我在多个机器上跑过类似代码,最省事的组合是 Python 3.8 配 opencv-python 4.5.5 和 numpy 1.21,这个组合对课程设计源码的兼容性最好。Python 3.10 以上有时会遇到 numpy 旧 API 报错,特别是三维重建代码里常见的 np.float 写法,在新版 numpy 里已经移除。

跑通顺序建议按数据依赖从简到繁排:先跑 img_mosaic.py,它只做图像拼接,出了环境问题最容易定位;再跑 mono.py,单目重建只吃单张图和内参,数据规模小,出问题好排查;最后跑 binocular_v1.py,双目链路长,视差参数多,放到最后调。不要一上来就调 SGBM 参数,视差图一花,你会分不清是代码问题还是参数问题,浪费一个晚上。

提示:如果 README.md 里写明了 Python 版本,就按它的版本建虚拟环境;没写明时就按 opencv-python >= 4.2、numpy >= 1.19 的组合来,这个组合兼容性最稳。

2.4 README 里的路径约定与两个易忽略的细节

README.md 在源码包里的作用,就是告诉你这个工程怎么运行。我会先看三处:Python 版本要求、运行命令的示例、是否提到路径是绝对还是相对。课程设计源码最常见的问题是代码里用了相对路径,而 README 没写清楚,结果你的当前工作目录不在项目根目录时,图片路径全部失效。解决办法是运行前把命令行的工作目录切到源码所在目录,或者在代码入口处加一条路径定位逻辑。

第二个易忽略的细节是中文路径和文件名编码。Windows 下解压如果带中文目录名,OpenCV 的 imread 经常静默返回 None,不报错但后续计算全崩。加上“副本”字符串的文件也是同样的道理。我习惯在遍历图片前先做一遍过滤,把包含“副本”的文件名剔除,顺便打印读入成功的图片数量,确认数据没丢。

import os import cv2 image_dir = "stereoimages" valid = [f for f in os.listdir(image_dir) if f.lower().endswith((".jpg", ".png")) and "副本" not in f] assert len(valid) >= 2, "有效图片不足,请检查目录" left = cv2.imread(os.path.join(image_dir, valid[0])) right = cv2.imread(os.path.join(image_dir, valid[1])) assert left is not None and right is not None, "图片读取失败,检查路径"

逻辑说明:先用列表推导式过滤掉非图片和非法的“副本”文件,再断言有效数量,最后读图并检查返回值。这里的关键是 imread 返回 None 时 OpenCV 不抛异常,必须手动断言,否则后面 cvtColor 会直接报错指向无关代码行。参数上的注意点是 endswith 匹配要转小写,避免 .JPG 后缀被漏掉;过滤条件里写“副本”是因为 Windows 复制的多层副本名都带这个关键字,一次能清干净。

3. 单目测距与单目重建:mono.py 的原理和参数落地

3.1 单目三维重建靠什么成立:小孔成像、相似三角形与多视角三角化

单目就只有一只眼睛,单张图没有视差,凭什么恢复三维?课程设计的答案通常是两条路线叠加:一是单目测距,利用小孔成像模型里的相似三角形,已知物体真实尺寸和它在图像里的像素尺寸,反推出距离;二是单目多视角重建,也就是 SfM 的最小实现——同一个物体在不同角度拍若干张照片,提取特征点、匹配、估计相机位姿、三角化出三维点。这份源码里的 milk、pan、apple、deep 系列图,每个物体都有三到六张不同角度的照片,就是为第二条路线准备的素材。

小孔成像模型把三维世界映射到二维像素。对图像上的每个特征点,存在关系 s * [u, v, 1]^T = K * [R|t] * [X, Y, Z, 1]^T。K 是内参矩阵,由 fx、fy、cx、cy 四个数决定,fx、fy 分别是 x、y 方向以像素为单位的焦距,cx、cy 是主点坐标,一般接近图像中心。R 和 t 是相机在该视角下的旋转和平移,合起来叫外参。单目重建的核心就是:内参已知后,用特征匹配在两个视图之间建立对应,解出 R、t,再对同名点做三角化,得到每个三维点的 X、Y、Z 坐标。

既然是课程设计规模,mono.py 不会做成完整版 SfM,大概率是 SIFT 提特征、FLANN 匹配、RANSAC 求基础矩阵、recoverPose 分解位姿、triangulatePoints 三角化的标准链路。输出的点云是稀疏的,一个物体可能只有几百到几千个三维点。这个预期要先放对,不要拿单目结果去和双目稠密点云比密度,两条路线的输出形态本来就不一样。

3.2 mono.py 的核心代码路径:读内参、提特征、三角化

mono.py 的入口通常是读取 mono.txt 里的内参。我拆过不少课程设计的单目代码,mono.txt 的格式大致几种:四个浮点数一排的;带 fx= 这类键值对的;直接排 3x3 矩阵的。拿到文件第一件事是打开看格式,不要按固定格式硬读。下面这段按最常见的四行浮点数格式解析:

import cv2 import numpy as np with open("mono.txt", "r") as f: lines = [float(line.strip()) for line in f if line.strip()] fx, fy, cx, cy = lines[:4] K = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]], dtype=np.float64) img = cv2.imread("milk1.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) sift = cv2.SIFT_create() kp, des = sift.detectAndCompute(gray, None) print(f"feature points: {len(kp)}")

逻辑说明:先用列表推导式把每行转成 float,并跳过空行;取前四个数组成内参矩阵 K。关键点在于 strip 和空行过滤,txt 文件末尾经常有换行,不处理会报 ValueError。SIFT 提特征后打印数量,如果 kp 数量少于 50,说明物体纹理太少,后面匹配根本做不起来,需要换有纹理的物体重拍。

两张图之间的位姿估计和三角化,常见做法如下:

def match_and_reconstruct(img1, img2, K): gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) kp1, des1 = sift.detectAndCompute(gray1, None) kp2, des2 = sift.detectAndCompute(gray2, None) index = cv2.FLANN_INDEX_KDTREE flann = cv2.FlannBasedMatcher(index, dict(checks=50)) matches = flann.knnMatch(des1, des2, k=2) good = [m for m, n in matches if m.distance < 0.75 * n.distance] pts1 = np.float32([kp1[m.queryIdx].pt for m in good]).T pts2 = np.float32([kp2[m.trainIdx].pt for m in good]).T F, mask = cv2.findFundamentalMat(pts1, pts2, cv2.FM_RANSAC, 1.5) pts1 = pts1[:, mask.ravel() == 1] pts2 = pts2[:, mask.ravel() == 1] E = K.T @ F @ K _, R, t, _ = cv2.recoverPose(E, pts1, pts2, K) P1 = K @ np.hstack((np.eye(3), np.zeros((3, 1)))) P2 = K @ np.hstack((R, t)) points4d = cv2.triangulatePoints(P1, P2, pts1, pts2) return points4d / points4d[3]

逻辑说明:FLANN 的 knnMatch 返回最近邻和前二近邻,ratio test 用 0.75 筛掉误匹配点。findFundamentalMat 用 RANSAC,阈值 1.5 表示允许的重投影误差,单位是像素。E 和 F 的关系是 E = K^T * F * K,recoverPose 解出相对旋转 R 和平移 t,但 t 只有方向没有尺度,所以单目重建的三维坐标天然没有绝对尺度,是一个按任意比例缩放的重建结果。三角化后除以齐次坐标第四行是必须的,这一步忘掉的后果是点云里全是无效值。

3.3 把参数收进 mono.txt:换相机只改四个数

换到自己的相机时,单目部分理论上只需要改 mono.txt 里的 fx、fy、cx、cy 四个值。但你的手机内参怎么来?课程设计里最实际的做法是实测反推:找一张 A4 纸或牛奶盒,量出真实高度,放在已知距离处拍照,量出图像里对应的高度像素,用公式 f = pixel_height * distance / real_height 反算焦距。用代码表示:

distance_mm = 1000 # 物体到相机距离,单位毫米 real_height_mm = 210 # 牛奶盒实际高度 pixel_height = 318 # 图像里的高度,单位像素 f_pixel = pixel_height * distance_mm / real_height_mm print(f"fx 实测值约 {f_pixel:.1f}")

逻辑说明:这是单目测距里最常用的相似三角形反推,fx 和 fy 在理想情况下相等,实际会有细微差别,所以反推时用竖直方向的 fy 更稳。这里的关键坑是手机相机的自动对焦会改等效焦距,拍完放缩照片后 fx 也变了,所以测量时要把焦距锁死在手动模式或者用主摄默认焦距,否则距离一变结果就飘。

另外,cx、cy 一般可以直接取图像中心坐标,比如 1280x720 的图就填 640、360,课程设计的精度要求下足够。真正影响重建比例的是 fx、fy 和距离的匹配关系,主点偏一点只会让点云整体平移,不影响形状。这份源码自带的 mono.txt 是针对它的测试图算好的,你用自带图跑没问题;换成自己的数据时,如果重建结果圆不圆、方不方,比例不对,优先怀疑内参文件而不是算法。

4. 双目立体视觉:binocular_v1.py 的标定、视差与点云生成

4.1 双目重建的完整链路:标定、校正、立体匹配、三角测量

双目立体视觉的原理比单目多了一只眼,也多了深度恢复的确定性。同一个三维点在左右两幅图上的成像位置不同,横向坐标差叫视差 d,深度 Z 与视差 d 的关系是 Z = f * B / d,f 是焦距,B 是左右相机光心之间的距离。因此双目重建的核心问题变成两个:怎么准确求出逐像素的视差 d,以及怎么把视差换算成三维坐标。binocular_v1.py 的处理流程也顺着这个逻辑展开。

完整链路是五步:读入左右图、去畸变、立体校正、立体匹配、重投影。前两步处理镜头畸变,第三步用 stereoRectify 把左右图像变换到极线平行状态,第四步用 SGBM 或 BM 计算视差图,第五步用 Q 矩阵把视差图转成点云。bino.txt 在这个流程里承担的就是双目相机标定的结果,它至少要包含左右相机内参、畸变系数、旋转向量 R 和平移向量 T,同时也要注意把内参和畸变分开存放,这样读代码时不容易混淆。

为什么必须先校正再匹配?因为实际搭建双目系统时,左右相机光轴不可能严格平行,哪怕一个 2 度的夹角,图像远处和近处的同名点垂直位置都会错开。立体校正就是对两张图各做一次单应变换,让同名点落到同一水平线上。这样匹配的搜索空间从二维降到一维,既能提高速度也能减少误匹配。课程设计里经常有人跳过校正直接匹配,短基线近距离可能看不出问题,换成长基线或大场景立刻翻车。

4.2 binocular_v1.py 里的 SGBM 参数:一表看清怎么调

立体匹配这一步,SGBM 是课程设计里的绝对主力,比 BM 效果好,比深度学习方案不需要训练和 GPU。SGBM 全称 Semi-Global Block Matching,在逐像素代价计算的基础上加了平滑约束,所以视差图在弱纹理区域也不容易碎。binocular_v1.py 里大概率是用 StereoSGBM_create 搭的参数配置,常见的模板和写法如下:

import cv2 import numpy as np left = cv2.imread("stereoimages/left.jpg", cv2.IMREAD_GRAYSCALE) right = cv2.imread("stereoimages/right.jpg", cv2.IMREAD_GRAYSCALE) assert left is not None and right is not None, "左右图读取失败" block_size = 11 sgbm = cv2.StereoSGBM_create( minDisparity=0, numDisparities=64, blockSize=block_size, P1=8 * 3 * block_size ** 2, P2=32 * 3 * block_size ** 2, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=32, mode=cv2.STEREO_SGBM_MODE_SGBM, ) disparity = sgbm.compute(left, right).astype(np.float32) / 16.0

逻辑说明:输入必须是单通道灰度图,compute 返回的原始视差是定点格式,所以除以 16.0 还原成像素单位。blockSize 是匹配窗口边长,必须是奇数;P1、P2 是视差平滑的惩罚系数,P2 通常取 P1 的 4 倍左右,决定了视差图平滑和细节之间的平衡。numDisparities 是允许的最大视差范围,必须是 16 的倍数。

参数怎么调,我整理成一张表,这是实际调参时最常用到的参考:

参数推荐起点偏大的表现偏小的表现
numDisparities64近处覆盖好,计算变慢近处出现空洞
blockSize11视差平滑、边缘糊噪声明显、视差碎
uniquenessRatio10匹配严格、空洞多误匹配多、表面脏
speckleWindowSize100小噪点被抹掉保留噪点
P1 / P283bs^2 / 323bs^2视差过于平滑弱纹理处断裂

调参顺序我一般是固定 blockSize 在 11,先根据场景远近调 numDisparities,再把 uniquenessRatio 从 5 到 15 来回试,最后动 P1、P2。这里最容易被忽略的一条是,视差效果差时先检查左右图是否过曝或者亮度不一致,再谈参数。SGBM 对左右图亮度差异非常敏感,先做 CLAHE 直方图均衡化比调十组参数都管用。

4.3 视差转点云:Q 矩阵与 reprojectImageTo3D 的坐标系约定

视差图算出来只是二维的,还要转成三维点云。OpenCV 用重投影矩阵 Q 来完成这一步,cv2.reprojectImageTo3D(disparity, Q) 输入视差图和 Q 矩阵,输出一个三通道矩阵,每个像素位置对应它在相机坐标系下的 X、Y、Z。Q 矩阵由 stereoRectify 在立体校正时同时给出,结构上包含了主点、焦距和基线信息,不需要你手算。

Q = np.array([ [1, 0, 0, -cx], [0, 1, 0, -cy], [0, 0, 0, f], [0, 0, -1/Tx, 0] ], dtype=np.float64) points3d = cv2.reprojectImageTo3D(disparity, Q) valid = disparity > 0 coords = points3d[valid] colors = np.stack([left[valid] for _ in range(3)], axis=-1)

逻辑说明:Q 矩阵第三行第三列是焦距 f,第四行第三列是 -1/Tx,Tx 是基线在 x 方向的平移分量。depth = f * Tx / d 由这一行体现,符号错了深度就全部反号。valid 条件用 disparity > 0 过滤掉无效视差,left 和 right 读取时分别对应左右图的灰度值,颜色从匹配参考的那张图取。最后的 coords 是 Nx3 的点集,按需要输出成 txt 或者 ply 格式。

保存点云时我习惯写成 ply 文件,用 numpy 结构化数组批量写入,避免逐点 for 循环。OpenCV 和 NumPy 本身不带 ply 导出,几十行代码就能搞定。注意一个坐标系约定:输出点云默认在左相机坐标系下,X 朝右、Y 朝下、Z 朝前。如果你的渲染软件显示出来模型是倒的或者左右翻转,不需要改算法,把点云坐标做一次 y 取反或者 x 取反即可。

5. 重建常见问题与避坑:从花屏点云到坐标系错乱

这几类问题我在不同项目里都遇到过,说穿了大多不是算法玄学,而是参数和数据的约定没对上。把这类血泪经验总结成下面五条,每一条都按“现象 → 原因 → 解决”的顺序写,方便你直接对照排查。

5.1 现象一:SGBM 一跑就报错,提示 numDisparities 或 blockSize 不合法

报错信息通常是 numDisparities must be a multiple of 16 或者 blockSize must be odd,程序直接中断。原因就是参数违反了 OpenCV 的硬性约束,常见于从网上复制 SGBM 配置时没有对应改到自己的图像尺寸,或者在前面某处把 numDisparities 改成了 60 这种不是 16 倍数的值。这个报错最迷惑的地方在于它不告诉你参数在哪一行,只提示参数值不合法,第一次遇到容易误以为是数据类型问题。

解决办法是把参数校验写进代码里,在 create 之前做断言,让错误提前暴露在明确的位置:

num_disparities = 64 block_size = 11 assert num_disparities % 16 == 0, "numDisparities 必须是 16 的倍数" assert block_size % 2 == 1, "blockSize 必须是奇数"

逻辑说明:两条断言分别卡住对视差搜索范围和平滑窗口的约束,不满足直接停止并给出中文提示,比 OpenCV 抛出原生英文异常更让人安心。除了这两条硬约束,numDisparities 还应该跟图像宽度匹配,不要超过图像宽度的四分之一,否则搜索范围超出图像边界没有意义,还会拖慢速度。

5.2 现象二:视差图全是花的,物体边缘像锯齿一样散开

跑完 binocular_v1.py 出来的视差图没有清晰的深度层次,细节区域跟雪花一样。原因有三个方向需要排查:第一是输入图没有转成灰度,直接用彩色图喂给 compute,不会报错但匹配质量明显下降;第二是 blockSize 太小,单像素邻域的纹理信息不够,匹配结果噪声大;第三是左右图光照不一致,一侧亮一侧暗,SGBM 对亮度差异没有鲁棒性。三个方向里光照问题最常见,也最容易被忽略。

解决路径按先数据后参数排:先把左右图用 cv2.COLOR_BGR2GRAY 转灰度,再对灰度图做一次 CLAHE 增强,把对比度拉均匀,最后再考虑调 blockSize。CLAHE 的写法是 cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)),clipLimit 控制对比度限制,tileGridSize 是局部直方图分块。做完这两步视差图通常会有质的改善,如果还是花的,再动参数不迟。

5.3 现象三:点云前后颠倒,重建出来的物体像是反着长的

reprojectImageTo3D 输出的点云 Z 值整体为负,渲染出来物体在相机背面,或者左右镜像翻转。原因是 Q 矩阵的符号约定和视差图方向不匹配。这个问题在不同数据集之间经常发生,因为 OpenCV 的 stereoRectify 在不同版本和不同输入顺序下,输出 Q 的符号可能不一样。它不是代码写错,而是数据约定不一致。

处理方法是先打印 points3d 三个通道的统计量,看 Z 的中位数是正还是负,然后按符号决定是否翻转。如果点云整体镜像,把 X 取反;如果深度反号,把 Q[3][2] 的正负号反过来再重投影一次。这类问题的定位原则是看整体趋势,不要看单个点:中位数为正则方向大概率对,中位数为负就是整体反号。

5.4 现象四:单目重建的点云比例失真,牛奶盒变成一张薄片

mono.py 跑完,点云倒是出来了,但牛奶盒重建结果像一块薄板,完全没有盒子的厚度。原因通常是两个匹配视图之间的位姿估计不准,RANSAC 的内点比例太低,baseline 太小导致三角化病态,或者特征匹配数量太少。课程设计里最常见的是物体表面纹理弱,SIFT 只提出几十个特征点,匹配后剩余可用点不到十个,三角化出来的点自然不足以描述三维形状。

解决方法是先打印匹配内点数量,低于 30 个就换物体或者换特征算子。ORB 在弱纹理上比 SIFT 更容易提取到角点,但匹配精度不如 SIFT;遇到表面光滑的物体,可以在物体上贴一张带纹理的纸再拍。另外一个常见做法是只取相邻视角的两张图三角化,不要把所有视角一起做全局优化,原因是代码里通常没有 bundle adjustment,参与视角越多误差累积越大。

5.5 现象五:把自己拍的图喂进去,效果比自带 demo 差一大截

同样的代码,demo1.png 里的效果像模像样,换成自己的照片后视差图和点云都惨不忍睹。这是所有视觉源码的通用教训:数据比算法更影响结果。自带的 milk、pan、IMG 系列是作者在固定焦距、固定光照下拍的,bino.txt 和 mono.txt 里的参数和这些照片是一一对应的;换成你自己的手机照片,内参、畸变、焦距全变了,参数文件却没变,结果自然就崩了。

解决办法分几步:先确定要不要沿用原相机参数,如果你的照片不是用同一台设备拍的,必须重新标定或实测内参;然后检查图像分辨率,SGBM 的 numDisparities 和 blockSize 都是相对分辨率而言的,1600x1200 的图用 64 和 11 可能偏小,建议先缩放到与 demo 接近的尺寸再跑;最后把左右图并排摆出来肉眼确认对应关系,确认左右顺序没有放反。这步做完,大部分“自己数据效果差”的问题都能解决。

6. 进阶:用你自己的数据验证这套源码,顺带调出一组能用的参数

6.1 用手机拍一组平移照片,替代 IMG 序列做验证

要确认这套源码不是只能在自带图上跑,拍一组自己的双目序列是最快的验证方式。拍摄要领:找一个纹理丰富的桌面场景,手机固定焦距,沿水平方向平移约 5 到 10 厘米,连拍两张,分别作为左右图。平移时保持相机朝向不变,不要旋转镜头;基线太长远景对不上,太短近处视差太小,5 到 10 厘米适合 0.5 到 2 米的常见桌面距离。把两张图放进 stereoimages 目录,注意文件名排序要保证左图在前,或者直接改成 left.jpg、right.jpg。

验证时先不动 bino.txt 里的内参,因为同一台手机内参可以沿用。如果点云整体深度值偏移,优先调整的是基线对应的 Tx 值,而不是内参。你可以在拍摄时用尺子量一下实际位移量,把它作为 Q 矩阵里 Tx 的参考值。跑通一次把自己的数据重建出来的点云,再回头看 demo1.png,你对这套源码的信任度会高很多。

6.2 用 img_mosaic.py 把验证结果拼成一屏,问题一眼可见

img_mosaic.py 是源码包里最容易被忽略但最实用的脚本。它不参与深度计算,却能在你调参时帮你快速对比:把左右原图、视差图、点云渲染图拼成一张图,哪个环节坏了立刻能看出来。常见实现是用 np.hstack 横向拼接,只拼两张图时够用;想多图拼进报告,可以改成按网格排版:

import cv2 import numpy as np def build_mosaic(paths, cols=2, cell=(480, 360)): imgs = [cv2.resize(cv2.imread(p), cell) for p in paths] rows = int(np.ceil(len(imgs) / cols)) canvas = np.zeros((rows * cell[1], cols * cell[0], 3), dtype=np.uint8) for i, img in enumerate(imgs): r, c = divmod(i, cols) canvas[r*cell[1]:(r+1)*cell[1], c*cell[0]:(c+1)*cell[0]] = img cv2.imwrite("mosaic_result.png", canvas)

逻辑说明:先统一把每张图缩放到 cell 尺寸,再按行优先填入画布,最后写盘。cell 用宽高比 4:3 的 (480, 360),避免比例拉伸后看不出真实几何关系。这套流程的最终价值是形成你自己的调参闭环:换数据、跑重建、拼图、看问题、改参数。

这份源码包拿到手,我的建议是先复现 demo,再带着你自己的数据走一遍这组验证流程,它能帮你把每一个参数为什么这么定都想明白。从那以后,我拿到任何三维重建的源码包,都不会先改代码,而是先跑通自带 demo,再换一组自己的数据做同样的验证。这个习惯帮我筛掉了不少“代码根本跑不通”的资源,也让我在答辩时能说清楚每一组参数是怎么定下来的。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询