☰
普通相机测距实战:从相机标定到双目视差与三维重建
2026/10/11 22:42:05 网站建设 项目流程

简介:这份资源面向计算机视觉入门与进阶开发者、机器人导航及三维重建方向的学生与工程师,提供一套基于Python与OpenCV的普通相机图像测距完整实现,重点解决单目与双目相机标定校正、视差计算与深度测量等核心问题。压缩包共41个文件,包含4个Python脚本、26张jpg与6张png标定及左右视图图像、2个txt说明、1份docx附赠文档及README等,整体约2.35MB,目录按标定、结果与视差模块组织,便于对照代码与数据复现实验。目前已有179人学习下载。读者可获取相机内参标定、双目标定外参求解、SIFT/SURF/ORB特征匹配、视差图与深度图生成等关键环节的可运行代码与配套素材,并参考图像去噪、边缘增强等优化思路,快速搭建工业检测、机器人导航或三维重建场景下的距离测量原型。

1. 普通相机测距:从单目标定到双目视差的完整落地路径

用普通 USB 相机做精确测距,这件事在工业分拣、AGV 避障、无人机定高、机器人抓取里都是刚需。很多人第一反应是买激光雷达或深度相机,但成本和部署复杂度摆在那里。实际上,一台几十块的普通相机,配合 Python 和 OpenCV,通过相机标定、立体校正、视差计算这套流程,在 0.3 米到 5 米范围内做到厘米级测距是完全可行的。这个方案的核心逻辑是:单目相机通过标定拿到内参和畸变系数,消除镜头畸变后,用已知尺寸的参照物做单目测距;双目相机则通过两个相机之间的基线距离,计算同一物体在左右图像中的视差,再结合焦距和基线算出深度。两条路线各有适用场景,单目适合已知目标尺寸的场合,双目适合未知尺寸但需要稠密深度图的场合。下面从标定开始,一步步把这条链路走通。

2. 相机标定:从棋盘格到内参矩阵的完整流程

2.1 为什么标定是测距的第一道门槛

普通相机的镜头都存在径向畸变和切向畸变,尤其是广角镜头,边缘的桶形畸变能让直线弯成弧线。如果不做畸变校正,你拿到的像素坐标和真实世界坐标之间的映射关系就是错的,测距误差可能达到几十厘米甚至更多。相机标定的本质是求解相机的内参矩阵和畸变系数,内参矩阵描述了焦距和光心在像素坐标系下的位置,畸变系数描述了镜头非线性形变的程度。

内参矩阵的形式是:

K = [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]

其中 fx 和 fy 是焦距在 x 和 y 方向上的像素表示,cx 和 cy 是光心坐标。畸变系数通常用 5 个参数表示:k1、k2、p1、p2、k3,前两个是径向畸变,中间两个是切向畸变,最后一个是高阶径向畸变。

标定的常见做法是使用棋盘格标定板,从不同角度拍摄 15 到 20 张照片,让棋盘格覆盖图像的各个区域。棋盘格的内角点数量需要提前知道,比如 9x6 的棋盘格表示每行 9 个内角点,每列 6 个内角点。拍摄时要注意棋盘格不要离相机太远,否则角点检测精度下降;也不要太近导致畸变过大,角点提取困难。

2.2 用 OpenCV 跑通单目标定

下面这段代码是单目标定的最小可复现版本,输入是一组棋盘格图片,输出是内参矩阵、畸变系数和重投影误差。

import cv2 import numpy as np import glob # 棋盘格内角点数量,这里以 9x6 为例 chessboard_size = (9, 6) # 棋盘格方格的实际物理尺寸,单位毫米 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 = [] # 二维像素点 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, chessboard_size, None) if ret: objpoints.append(objp) # 亚像素级角点优化 corners2 = cv2.cornerSubPix( gray, corners, (11, 11), (-1, -1), criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) ) imgpoints.append(corners2) # 标定 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) print("内参矩阵:\n", mtx) print("畸变系数:\n", dist) print("重投影误差:", ret)

这段代码的逻辑是:先构造棋盘格在真实世界中的三维坐标,然后对每张图片提取角点的像素坐标,最后用calibrateCamera求解内参和畸变系数。cornerSubPix这一步很关键,它把角点定位精度从整数像素提升到亚像素级别,直接影响标定结果的准确性。

参数方面,chessboard_size必须和实际棋盘格一致,写错了角点检测直接失败。square_size的单位决定了后续测距的单位,如果你用毫米,那测出来的距离就是毫米。cornerSubPix的窗口大小(11, 11)是经验值,棋盘格在图像中较大时可以适当增大。重投影误差ret一般要求在 0.5 像素以内,超过 1 像素说明标定质量有问题,需要检查图片质量或增加标定图片数量。

2.3 畸变校正与效果验证

标定完成后,用undistort对图像做校正,验证直线是否恢复平直。

img = cv2.imread('test.jpg') h, w = img.shape[:2] # 获取优化后的相机矩阵 newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) # 畸变校正 dst = cv2.undistort(img, mtx, dist, None, newcameramtx) cv2.imwrite('undistorted.jpg', dst)

getOptimalNewCameraMatrix的作用是重新计算一个相机矩阵,让校正后的图像保留更多有效区域。参数alpha=1表示保留所有像素,alpha=0表示裁剪掉无效区域。实际使用时,如果后续要做立体匹配,建议alpha=0,避免黑边干扰匹配算法。

验证标定质量的方法很简单:拍一张包含直线的场景,校正后看直线是否笔直。如果还有明显弯曲,说明畸变系数没解好,需要重新标定。另一个方法是看重投影误差,这个数值直接反映了标定参数对角点位置的拟合程度。

3. 双目立体视觉:基线、极线校正与视差图计算

3.1 双目测距的几何原理与基线选择

双目测距的核心公式是:

Z = (f * B) / d

其中 Z 是深度,f 是焦距(像素单位),B 是两个相机光心之间的距离,也就是基线,d 是视差,即同一物体在左右图像中横坐标的差值。这个公式说明,深度和视差成反比,视差越大,物体越近;视差越小,物体越远。

基线的选择直接影响测距范围和精度。基线越大,同样深度下的视差越大,远距离测距精度越高,但近距离时视差可能超出搜索范围,导致匹配失败。基线越小,近距离测距更稳定,但远距离精度下降。常见做法是基线在 6 厘米到 12 厘米之间,对应 0.5 米到 5 米的测距范围。如果目标距离在 1 米以内,基线可以缩小到 3 厘米左右;如果目标在 5 米以上,基线需要加大到 20 厘米以上,但这时候近处盲区会明显增大。

双目相机可以是两个独立相机刚性固定,也可以是一体式双目模组。独立相机的好处是基线可调,坏处是同步触发需要额外处理。一体式模组通常已经做了硬件同步,使用更方便,但基线固定。不管哪种方式,两个相机的内参和相对位姿都需要标定。

3.2 双目标定:从单目标定到立体标定

双目标定的流程是:先分别对左右相机做单目标定,拿到各自的内参和畸变系数,然后用stereoCalibrate求解两个相机之间的旋转矩阵和平移向量。

# 假设已经分别拿到了左右相机的 objpoints 和 imgpoints # 左相机单目标定 retL, mtxL, distL, rvecsL, tvecsL = cv2.calibrateCamera( objpointsL, imgpointsL, grayL.shape[::-1], None, None ) # 右相机单目标定 retR, mtxR, distR, rvecsR, tvecsR = cv2.calibrateCamera( objpointsR, imgpointsR, grayR.shape[::-1], None, None ) # 双目标定 flags = cv2.CALIB_FIX_INTRINSIC criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5) ret, mtxL, distL, mtxR, distR, R, T, E, F = cv2.stereoCalibrate( objpointsL, imgpointsL, imgpointsR, mtxL, distL, mtxR, distR, grayL.shape[::-1], criteria=criteria, flags=flags ) print("旋转矩阵 R:\n", R) print("平移向量 T:\n", T) print("基线长度:", np.linalg.norm(T))

stereoCalibrate的输出中,R 是右相机相对于左相机的旋转矩阵,T 是平移向量,T 的模就是基线长度。CALIB_FIX_INTRINSIC表示固定单目标定得到的内参,只优化外参。如果单目标定结果不够准,可以去掉这个 flag,让双目标定同时优化内参和外参,但计算量会增大。

标定完成后,用stereoRectify做极线校正,让左右图像的极线水平对齐。这一步是立体匹配的前提,校正后同一物体在左右图像中只存在水平方向的位移,垂直方向没有位移,搜索范围从二维降到一维。

R1, R2, P1, P2, Q, roi1, roi2 = cv2.stereoRectify( mtxL, distL, mtxR, distR, grayL.shape[::-1], R, T, alpha=0 ) # 生成映射表 mapLx, mapLy = cv2.initUndistortRectifyMap( mtxL, distL, R1, P1, grayL.shape[::-1], cv2.CV_32FC1 ) mapRx, mapRy = cv2.initUndistortRectifyMap( mtxR, distR, R2, P2, grayR.shape[::-1], cv2.CV_32FC1 )

Q是视差到深度的重投影矩阵,后续用reprojectImageTo3D把视差图转成三维点云时会用到。alpha=0表示校正后图像裁剪掉无效区域,适合做立体匹配。

3.3 视差计算:SGBM 参数调优与深度图生成

OpenCV 提供了多种立体匹配算法,常用的有 BM 和 SGBM。BM 速度快但精度一般,SGBM 精度好但计算量大。对于普通相机测距,SGBM 是更常见的选择。

# 校正左右图像 imgL = cv2.remap(imgL_orig, mapLx, mapLy, cv2.INTER_LINEAR) imgR = cv2.remap(imgR_orig, mapRx, mapRy, cv2.INTER_LINEAR) # 转灰度 grayL = cv2.cvtColor(imgL, cv2.COLOR_BGR2GRAY) grayR = cv2.cvtColor(imgR, cv2.COLOR_BGR2GRAY) # SGBM 参数 min_disp = 0 num_disp = 16 * 6 # 必须是 16 的倍数 block_size = 5 stereo = cv2.StereoSGBM_create( minDisparity=min_disp, numDisparities=num_disp, 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_3WAY ) disparity = stereo.compute(grayL, grayR).astype(np.float32) / 16.0 # 转深度图 points_3d = cv2.reprojectImageTo3D(disparity, Q)

numDisparities决定了视差搜索范围,必须大于最大视差。如果目标最近距离是 0.5 米,基线 6 厘米,焦距 600 像素,最大视差大约是 72,所以numDisparities至少设为 80,取 16 的倍数就是 96。设小了远处物体匹配不到,设大了计算量增加且容易引入噪声。

blockSize是匹配块大小,奇数,一般 5 到 11 之间。太小噪声多,太大边缘模糊。P1和P2是平滑惩罚项,控制视差图的平滑程度,经验公式是P1=8*通道数*blockSize^2,P2=32*通道数*blockSize^2。uniquenessRatio是唯一性检查,一般 5 到 15,值越大匹配越严格,但可能丢失弱纹理区域的视差。

视差图出来后,用reprojectImageTo3D结合 Q 矩阵就能得到每个像素的三维坐标。取目标区域中心点的 Z 值就是距离。实际使用时,通常会对目标区域做滤波,取中值或均值,避免单点噪声导致测距跳变。

4. 避坑与排查:标定和测距中最容易翻车的五个地方

4.1 棋盘格角点检测失败

现象:findChessboardCorners返回 False,或者只检测到部分角点。

原因:棋盘格尺寸写错、图像模糊、光照不均、棋盘格太小或太大、拍摄角度过于倾斜。

解决:先用cv2.imshow确认棋盘格在图像中清晰可见,检查chessboard_size是否和实际内角点数量一致。拍摄时保证棋盘格占图像面积的 1/3 以上,避免强反光和阴影。如果还是失败,可以尝试findChessboardCornersSB,它对噪声和模糊更鲁棒。

4.2 重投影误差过大

现象:calibrateCamera返回的 ret 大于 1.0。

原因:标定图片太少、棋盘格姿态不够丰富、角点提取精度低、镜头畸变过大。

解决:增加到 20 张以上标定图片,覆盖图像的四个角和中心区域,棋盘格要有不同倾斜角度。检查cornerSubPix的窗口大小是否合适,窗口太小亚像素优化效果差。如果镜头畸变特别大,可以先做一次粗略校正再标定。

4.3 双目校正后极线不对齐

现象:stereoRectify后左右图像的同一物体不在同一水平线上。

原因:双目标定时左右相机的角点顺序不一致、标定图片不同步、R 和 T 求解错误。

解决:确保左右相机拍摄的是同一时刻的同一棋盘格,角点检测的顺序要一致。可以在stereoCalibrate后检查 R 和 T 的数值是否合理,T 的模应该接近实际测量的基线长度。如果偏差很大,说明标定失败,需要重新采集数据。

4.4 视差图大面积空洞

现象:SGBM 输出的视差图中大量区域为黑色,深度图缺失严重。

原因:numDisparities太小、纹理不足、左右图像曝光差异大、uniquenessRatio过高。

解决:增大numDisparities到覆盖最大视差,在目标区域增加纹理或投影散斑图案。左右相机的曝光参数要一致,可以用自动曝光锁定。适当降低uniquenessRatio到 5 到 10,减少匹配失败。

4.5 测距值跳变严重

现象:同一目标连续测量,距离值波动超过 10%。

原因:视差图噪声、目标区域纹理弱、匹配块太小、没有做后处理。

解决:增大blockSize到 7 或 9,对视差图做中值滤波,取目标区域中心 5x5 或 10x10 区域的深度中值。如果目标纹理弱,可以在目标上贴纹理贴纸或投影图案。另外,确保相机和目标的相对位置稳定,避免运动模糊。

5. 从测距到三维重建:用 Q 矩阵和点云做验证

5.1 用 reprojectImageTo3D 生成三维点云

视差图转深度图只是第一步,reprojectImageTo3D能直接把视差图转成三维点云,每个像素对应一个 (X, Y, Z) 坐标。这个点云可以用来做三维重建、平面拟合、体积测量等。

# 生成三维点云 points_3d = cv2.reprojectImageTo3D(disparity, Q) # 过滤无效点 mask = disparity > disparity.min() mask = mask & (disparity < disparity.max()) points = points_3d[mask] colors = imgL[mask] # 保存为 PLY 文件 def write_ply(filename, points, colors): with open(filename, 'w') as f: f.write("ply\n") f.write("format ascii 1.0\n") f.write(f"element vertex {len(points)}\n") f.write("property float x\n") f.write("property float y\n") f.write("property float z\n") f.write("property uchar red\n") f.write("property uchar green\n") f.write("property 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("pointcloud.ply", points, colors)

这段代码把视差图转成三维点云并保存为 PLY 格式,可以用 MeshLab 或 CloudCompare 打开查看。Q矩阵来自stereoRectify,它包含了基线、焦距、光心等参数,是视差到深度的桥梁。

5.2 用平面拟合验证测距精度

拿到点云后,可以拟合一个平面,然后计算点到平面的距离,验证测距精度。

# 取目标区域的点 target_points = points_3d[300:400, 400:500].reshape(-1, 3) target_points = target_points[np.isfinite(target_points).all(axis=1)] # 拟合平面 centroid = np.mean(target_points, axis=0) centered = target_points - centroid u, s, vh = np.linalg.svd(centered) normal = vh[2] # 计算距离 distances = np.abs(np.dot(centered, normal)) print("平均距离:", np.mean(distances)) print("距离标准差:", np.std(distances))

如果目标是一个平面,点到拟合平面的距离应该接近零。标准差反映了测距的稳定性,标准差越小,测距越稳定。如果标准差超过 1 厘米,说明视差图噪声较大,需要调整 SGBM 参数或增加后处理。

5.3 单目测距的补充方案

如果手头只有单目相机,也可以用已知尺寸的参照物做测距。原理是:

Z = (f * H) / h

其中 H 是物体的真实高度,h 是物体在图像中的像素高度,f 是焦距。这个方法的局限是必须知道物体的真实尺寸,而且物体要垂直于光轴。实际使用时,可以在场景中放一个已知尺寸的标记物,比如 A4 纸或二维码,先算出焦距,再测其他物体。

# 已知物体高度 H,像素高度 h,焦距 f H = 297.0 # A4 纸高度,毫米 h = 200.0 # 像素高度 f = 600.0 # 焦距,像素 Z = (f * H) / h print("距离:", Z, "毫米")

这个方法的精度取决于像素高度的测量精度,通常误差在 5% 到 10% 之间。如果对精度要求高,还是建议上双目。

5.4 我踩过的坑和现在的习惯

标定这件事,我现在的习惯是每次换镜头或调整相机位置后都重新标定,绝不偷懒用旧参数。有一次项目赶进度,用了三个月前的标定文件,结果测距误差从 1 厘米变成了 8 厘米,排查了一整天才发现是相机被碰过。双目测距的基线也要定期检查,尤其是用独立相机搭建的方案,螺丝松动一点,基线就变了,测距直接翻车。

另一个习惯是每次标定后都保存原始图片和标定结果,方便回溯。标定图片不要删,万一后面测距有问题,可以重新跑一遍标定流程对比。SGBM 的参数不要一次调太多,每次只改一个,记录效果,否则出了问题都不知道是哪个参数导致的。

希望帮到你。

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

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

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

立即咨询