简介一套基于Python和OpenCV的普通相机图像测距系统资料包面向计算机视觉与立体成像方向的开发者和学习者解决通过普通相机拍摄图像实现精确距离测量的问题。系统涵盖单目相机标定校正、双目相机空间位置关系标定、左右视图视差计算与深度映射完整覆盖从标定板图像采集到深度图生成的流程。压缩包共41个文件大小约2.35MB以JPG标定图像、PNG结果图、Python脚本、说明文档等为主包含左右相机拍摄序列、标定板图片、立体校正与视差结果图以及配套的标定、主程序等可运行脚本便于直接对照实验。资源还附带依赖清单、说明文件和附赠文档可快速搭建环境理解各模块的调用关系。已有188人学习下载适合作为课程设计、竞赛项目或实际工程中图像测距模块的参考实现能帮助读者掌握相机标定、特征匹配、视差计算和深度图生成等关键环节从棋盘格采集到深度映射形成完整闭环利于新手入门与项目二次开发。1. 单目与双目之争普通相机测距到底怎么从一张照片里算出距离“基于Python和OpenCV的普通相机图像测距系统”这个标题说的东西简单讲就是不买深度相机、不用激光雷达只靠一个USB摄像头或手机摄像头利用标定、校正、视差计算这串OpenCV里现成的工具链把图像里的像素位置换算成真实距离。这个方向看起来是一条直路真正落地的人都知道坑全埋在标定和参数里。先把结论摆出来单目测距做的是“已知物体尺寸 → 像素占比 → 距离”的比例换算它本身并不具备测量未知物体的能力双目测距靠左右两幅图像的视差在普通相机硬件上才算是真正“自己量出距离”的方案。因此选型不是看哪个代码更短而是看你的场景固定闸机、限高杆、固定工位这类目标尺寸已知、距离不超过三四米的环境单目足够机器人避障、料位监测、连续深度输出这类需求双目才扛得住。下面按主线拆开给你能直接照着跑的流程和参数。2. 相机标定与畸变校正先把像素坐标变成物理坐标测距的前提是把图像坐标真正对应到相机坐标系里。普通相机镜头的畸变如果不处理远处的点可能在像素上偏出好几个像素这对测距来说是致命的。标定这一步做得扎实后面所有测量才有意义。2.1 张氏标定棋盘格、内参与畸变系数的最小必要概念标定要做的事是找到相机内参矩阵和畸变系数。内参矩阵把相机坐标系下的三维点投影到像素平面包含焦距以像素为单位和主点位置畸变系数描述镜头变形径向畸变让直线变弯切向畸变源于镜片装配误差。两者一起决定了一个三维点会落在图像的哪个像素上。张氏标定法的思路很直接拿一块打印出来的棋盘格在不同角度、不同位置拍多张照片检测角点把棋盘的已知物理坐标和图像像素坐标对应起来求解这组投影参数。棋盘格用A4哑光纸打印贴在硬质平板上避免纸张褶皱。角点数量要选对内角点数比如(10, 7)表示横向10个内角点、纵向7个内角点印出来实际是11乘8的格子块。拍摄数量建议20到30张关键不是多而是姿势分布远、近、上、下、左、右、旋转、倾斜都要有画面四角和中间区域都必须让角点出现过。很多人拍十几张全是画面正中央的正面照结果畸变参数完全不收敛后面测距一塌糊涂。2.2 拍摄标定照片与执行标定的完整脚本先要一张标定板图片。下面这个脚本生成一张可打印的棋盘格每格24毫米300dpi带白边方便裁切import cv2 import numpy as np # 棋盘格参数内角点数 (10, 7)每格 24 mm pattern_size (10, 7) square_size 24 # 单位毫米 dpi 300 pixels_per_mm dpi / 25.4 # 左右留白边打印后不容易裁到黑格 margin 20 # 单位毫米 board_w pattern_size[0] * square_size board_h pattern_size[1] * square_size img_w int((board_w 2 * margin) * pixels_per_mm) img_h int((board_h 2 * margin) * pixels_per_mm) img np.full((img_h, img_w), 255, dtypenp.uint8) start_x int(margin * pixels_per_mm) start_y int(margin * pixels_per_mm) cell_px int(square_size * pixels_per_mm) for i in range(pattern_size[0]): for j in range(pattern_size[1]): x start_x i * cell_px y start_y j * cell_px if (i j) % 2 0: img[y:y cell_px, x:x cell_px] 0 cv2.imwrite(chessboard.png, img)这段的要点是pattern_size是内角点数不是格子数square_size以毫米为单位它决定了后面所有距离输出的单位。如果你打印出来的实际格子不是24毫米一定要量了再填否则标定结果全部量纲错误。打印分辨率建议不低于300dpi太低会让角点检测抖动。标定主体脚本如下import cv2 import numpy as np import glob pattern_size (10, 7) square_size 24.0 # 棋盘格平面上的三维点坐标Z方向为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 obj_points [] img_points [] images sorted(glob.glob(calib_images/*.jpg)) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 粗略检测角点 found, corners cv2.findChessboardCorners(gray, pattern_size, None) if not found: print(skip:, fname) # 没检测到直接跳过 continue # 亚像素细化把角点精度从整数像素提到亚像素 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_sub cv2.cornerSubPix(gray, corners, (5, 5), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_sub) if len(obj_points) 5: # flags 里 CALIB_ZERO_TANGENT_DIST 表示只估计径向畸变 rms, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None, flagscv2.CALIB_ZERO_TANGENT_DIST) np.savez(camera_params.npz, mtxmtx, distdist, rmsrms, img_sizegray.shape[::-1]) print(RMS(px):, rms)findChessboardCorners返回的found是布尔值很多实践里检测不出来的照片会被静默忽略如果你拍到的可用照片少于15张建议重拍而不是硬凑。cornerSubPix的窗口(5, 5)对大多数棋盘格足够如果标定板分辨率很低可以改成(11, 11)。calibrateCamera返回的rms是重投影误差单位是像素经验上小于0.5可用小于0.2说明标定质量不错。这个rms只是同一批照片上的平均误差不能完全代表实测距离精度。2.3 标定结果怎么判断重投影误差与图像校正观察拿到参数后不要急着测距先做两件事。第一打印一张验证棋盘格单独放在几个位置用已保存的参数算位姿再把角点投回去看平均像素偏差是否小于0.5像素。第二直接对一张有明显直线边缘的照片做畸变校正观察画面边缘的直线是否变直绿色通道比对校正前后的像素位置。import cv2 import numpy as np data np.load(camera_params.npz) mtx, dist data[mtx], data[dist] img cv2.imread(test_rect.jpg) h, w img.shape[:2] # alpha0 表示裁掉黑边保留最多有效像素 new_mtx, roi cv2.getOptimalNewCameraMatrix( mtx, dist, (w, h), 0, (w, h)) undistorted cv2.undistort(img, mtx, dist, None, new_mtx) # roi 是去畸变后不含黑边的有效区域 x, y, w2, h2 roi undistorted undistorted[y:y h2, x:x w2] cv2.imwrite(undistorted.jpg, undistorted)getOptimalNewCameraMatrix里的alpha取值范围是0到1alpha0裁切最大但画面里没有黑边alpha1保留所有原始像素但边缘会出现弯曲区域。单目测距建议直接用0双目极线校正阶段一般取更小的裁剪策略这个在第4章会具体说。去畸变之后画面边缘原本弯曲的门框、墙体边缘线应该变成直线。如果你发现标定rms很小但边缘直线依旧弯曲多半是拍摄覆盖不全畸变系数在边缘区域没有被有效约束回炉重拍是唯一的后悔药。3. 单目测距落地相似三角形与 solvePnP 的取舍单目测距从原理上分两档。第一档是相似三角形代码最简单但约束最多第二档是solvePnP用一组已知的三维点和对应像素点直接求解相机外参能容忍一定程度的倾斜视角是实际工程里更值得用的方案。3.1 相似三角形测距公式、前提与失效边界针孔模型下的测距公式是[ Z \frac{f \cdot H}{h} ]其中 (Z) 是目标距离(f) 是焦距单位是像素(H) 是目标真实高度单位毫米(h) 是目标在图像里的像素高度。焦距来自标定内参矩阵的mtx[0, 0]目标高度需要提前量好。这个公式成立的前提有三个目标物完全正对相机没有俯仰和偏航目标高度已知且测量准确相机光轴与目标平面垂直。真实场景中这三个条件几乎不可能同时满足。比如一个行人对相机有20度的偏角像素高度会明显缩水误差直接到百分之十几。所以这个公式只适合闸机、固定工位这类目标姿态可控的场景我在这里写出来是为了让你先理解原理工程上不要直接用。3.2 用 solvePnP 做单目测距不依赖正对假设solvePnP的思路是知道目标上N个三维点在物体坐标系下的坐标也知道它们落在图像上的像素位置就可以反求出物体坐标系相对于相机坐标系的旋转和平移。平移向量tvec的模长就是目标中心到相机光心的距离。工程上最省事的做法是用ArUco码作为目标物OpenCV自带的检测器直接返回四个角点不需要自己写角点提取逻辑。代码import cv2 import numpy as np # 4x4 字典ID数量50个普通测距够用 aruco_dict cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50) params cv2.aruco.DetectorParameters() def measure_distance(frame, mtx, dist, marker_size_mm100.0): corners, ids, _ cv2.aruco.detectMarkers( frame, aruco_dict, parametersparams) if ids is None: return None # marker 真实尺寸的一半单位毫米 half marker_size_mm / 2.0 # 物体坐标系下的四个角点Z0 表示贴在平面 objp np.array([ [-half, half, 0], [ half, half, 0], [ half, -half, 0], [-half, -half, 0]], dtypenp.float32) # corners[0][0] 是第一个marker的四个角点顺序与 objp 对应 ret, rvec, tvec cv2.solvePnP( objp, corners[0][0], mtx, dist, flagscv2.SOLVEPNP_ITERATIVE) if not ret: return None # 斜距目标中心到相机光心的欧氏距离 dist_mm float(np.linalg.norm(tvec)) # 前向距离沿相机光轴方向的距离 z_mm float(tvec[2][0]) # 重投影误差评判这一帧外参解算质量 pts2, _ cv2.projectPoints(objp, rvec, tvec, mtx, dist) reproj_err float(np.mean( np.linalg.norm(pts2.reshape(4, 2) - corners[0][0], axis1))) return dist_mm, z_mm, reproj_errtvec的三个分量分别是目标原点在相机坐标系下的X、Y、Z坐标单位跟objp一致。因为你传进去的坐标单位是毫米tvec就是毫米。SOLVEPNP_ITERATIVE适合平面目标也可以用SOLVEPNP_IPPE它专门针对平面点做了加速和优化。如果画面里检测到多个marker可以把各自的tvec取中位数平均抖动会明显小于单个marker。代码里我额外算了一步重投影误差这是很多人忽略的地方。solvePnP返回的ret只是布尔成功标志不是精度指标真正看出这一帧外参解得好不好要看把rvec、tvec投回去的像素误差。实测中如果重投影误差大于1像素说明角点定位不稳这一帧测距结果直接丢弃更安全。3.3 单目测距的参数边界目标尺寸、安装角与误差放大器单目测距的误差有一个让人难受的特性距离估计误差跟目标尺寸误差近似成正比。假设marker实际是100毫米你量出来是101毫米所有测距结果都会放大1%。安装角度带来的误差更隐蔽——相机朝下倾斜安装时solvePnP勉强能解出外参但对标定误差和角点噪声更敏感。建议把工作距离控制在工作距离预期误差经验值说明0.5米以内2%以内角点像素占比大稳定性好1到2米3%到5%像素量化误差开始占主导3米以上5%到10%以上单目方案不建议超过这个范围单目的本质是外推不是测量。距离越远目标在图像中占据的像素越少角点亚像素精度带来的相对误差越大。所以单目测距的价值不在高精度而在固定场景下的低成本粗略测距。如果你需要3米以外稳定的毫米级深度直接看第4章的双目方案。4. 双目深度测量从极线校正到视差计算的完整链路双目才是普通相机硬件下真正能算出绝对距离的方案。它不需要知道目标物体的任何尺寸只要左右两幅图像中对应点匹配得上就能用几何关系推出深度。整套链路里标定、极线校正、立体匹配、参数调优每一步都可能翻车下面按顺序拆开。4.1 视差的物理意义为什么基线越大测距越远双目测距的核心公式是[ Z \frac{f \cdot B}{d} ](f) 是焦距像素单位(B) 是两个相机光心之间的距离通常叫基线(d) 是同一个空间点在左右图像中的像素横坐标之差也就是视差。深度与视差成反比这意味着近处物体视差大、容易测准远处物体视差小、一点点匹配误差都会被放大成很大的深度误差。基线选择直接影响测量范围。基线越大相同深度下视差越大对远距离测量越有利但基线过大会导致近处物体的左右视角差异过大匹配成功率下降。经验上工作距离在1到3米之间基线取15到20厘米比较均衡如果主要测5米以上基线要尽量放宽但要做好近处视野被裁掉的准备。两个相机要固定在一个刚性支架上安装后不能有丝毫松动否则标定结果会随时间报废。4.2 双目联合标定与极线校正双目标定的基础是左右相机分别完成第2章的单目标定。然后采集同一时刻的左右成对棋盘格照片进入联合标定。这里的关键是左右摄像头必须拍同一时刻的棋盘格位置不能左右分开拍否则棋盘格目标的三维位置不一致联合标定完全无意义。import cv2 import numpy as np # 假设已经分别得到左右内参 mtx_l, dist_l np.load(left_camera.npz), np.load(left_camera.npz)[dist] # 实际使用时替换为左右各自的参数文件 obj_points [] img_points_l [] img_points_r [] # pairs 左右图像路径列表两两对应 for path_l, path_r in pairs: gray_l cv2.cvtColor(cv2.imread(path_l), cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(cv2.imread(path_r), cv2.COLOR_BGR2GRAY) found_l, corners_l cv2.findChessboardCorners(gray_l, pattern_size, None) found_r, corners_r cv2.findChessboardCorners(gray_r, pattern_size, None) if not (found_l and found_r): continue criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6) corners_l cv2.cornerSubPix(gray_l, corners_l, (5, 5), (-1, -1), criteria) corners_r cv2.cornerSubPix(gray_r, corners_r, (5, 5), (-1, -1), criteria) obj_points.append(objp) img_points_l.append(corners_l) img_points_r.append(corners_r) # 联合标定固定已知内参只优化外参和基线 flags cv2.CALIB_FIX_INTRINSIC rms_stereo, M1, d1, M2, d2, R, T, E, F cv2.stereoCalibrate( obj_points, img_points_l, img_points_r, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], criteriacriteria, flagsflags) print(stereo RMS(px):, rms_stereo)CALIB_FIX_INTRINSIC的意思是沿用单目标定的内参只优化左右相机之间的旋转矩阵R和平移向量T。这个做法比从头联合优化所有参数更稳因为单目标定已经在各自照片上充分收敛了。stereo RMS的经验值同样参考小于1像素如果联合标定后RMS明显大于单目的RMS通常是左右标定板照片数量不够或者棋盘格检测有误。极线校正的目的是让左右图像中同一个三维点落在同一行上这样后续匹配只需要沿水平方向搜索# 图像尺寸左右一致 image_size (width, height) R1, R2, P1, P2, Q, valid1, valid2 cv2.stereoRectify( M1, d1, M2, d2, image_size, R, T, alpha0) map1_l, map2_l cv2.initUndistortRectifyMap( M1, d1, R1, P1, image_size, cv2.CV_32FC1) map1_r, map2_r cv2.initUndistortRectifyMap( M2, d2, R2, P2, image_size, cv2.CV_32FC1) rect_l cv2.remap(gray_l, map1_l, map2_l, cv2.INTER_LINEAR) rect_r cv2.remap(gray_r, map1_r, map2_r, cv2.INTER_LINEAR)校正完成后一定要用肉眼验证极线在左图的某一行上选一个明显特征点看右图同一点是否在相同行。如果差出好几个像素优先怀疑左右相机没有严格刚性地固定或者标定板照片太少。Q矩阵保存了重投影矩阵后面reprojectImageTo3D要直接用到它。4.3 SGBM 视差计算与深度转换极线校正之后立体匹配用StereoSGBM完成。它通过半全局匹配思想在多个方向传播匹配代价约束比简单的块匹配对弱纹理区域效果好一些但也远不是万能的。# 创建SGBM匹配器 block_size 15 stereo cv2.StereoSGBM_create( minDisparity0, numDisparities64, # 必须是16的倍数 blockSizeblock_size, # 必须是奇数 P18 * 3 * block_size ** 2, P232 * 3 * block_size ** 2, disp12MaxDiff1, preFilterCap32, uniquenessRatio10, speckleWindowSize100, speckleRange2, modecv2.STEREO_SGBM_MODE_SGBM_3WAY) # compute返回的是int16真实视差需要除以16 disp16 stereo.compute(rect_l, rect_r) disp disp16.astype(np.float32) / 16.0 # 深度转换 points3d cv2.reprojectImageTo3D(disp, Q, handleMissingValuesTrue) Z points3d[:, :, 2]这里有一个高频踩坑点compute返回的disp16不是浮点视差必须除以16才是以像素为单位的真实视差。直接拿disp16做归一化或深度计算距离会缩水16倍这是双目项目里最常见也最隐蔽的错误。numDisparities越大能测到的深度范围越近但搜索范围成倍增加误匹配概率也随之上升。SGBM 参数调整方向可以参考这张表参数经验范围调参方向numDisparities64/96/128值越大测距越近但计算量增大blockSize9/15/21越大对低纹理越稳但边缘细节丢失P1/P283block² / 323block²越大视差图越平滑过大会抹平物体边界uniquenessRatio5到15越大误匹配越少低纹理区域也更容易丢匹配speckleWindowSize50到200滤除孤立噪点值越大滤除的斑块越大preFilterCap控制输入图像的截断值对光照对比度剧烈的场景影响明显一般32或63。disp12MaxDiff做左右一致性检查值越小越严格1到2比较稳妥。5. 测距误差与常见问题排查为什么你的结果总是差十几厘米这一章把最常见的问题按“现象、原因、解决”列出来。每一条都来自实际项目里的真实翻车现场不是理论推演。5.1 现象标定RMS很小测距却偏了标定面板上的重投影误差0.3像素看着漂亮实际测距离却差出10%以上。原因往往是标定板拍摄覆盖不全。只拍了画面中央区域畸变系数在最需要约束的画面边缘没有被数据约束内参在边缘区域的投影自然不准。还有一种常见情况是标定板本身不平整打印纸贴在弯曲的纸板上角点物理坐标系统就有偏差。解决回炉重拍20到30张标定照片必须覆盖画面四角、中心、近处、远处并包含明显旋转角度。同时拿这些照片去检查单个样本的重投影误差把明显偏离平均值的照片剔除后重新标定。重投影像素偏差大于1的样本直接删掉不要心软。5.2 现象视差图上目标区域全是黑色空洞SGBM在纯色、低纹理区域没有可靠的匹配代价峰值所以白墙、白纸这类目标在视差图上会是黑洞。光线不均匀时阴影边界也会被误判成视角差异产生成片错误视差。解决先判断场景能否改进。给目标表面增加纹理是最直接的办法比如贴一张有图案的标签纸实在不能贴就调大blockSize到21左右同时降低uniquenessRatio让匹配算法对弱代价峰值更宽容。但要注意这些改动会牺牲边缘精度。如果目标本身就是毫无纹理的墙面建议放弃SGBM换成主动投影纹理的方案这不是参数能解决的问题。5.3 现象距离越远误差越大而且是平方级放大双目公式 (Z fB/d)深度误差与距离的关系是[ \delta Z \frac{Z^2 \cdot \delta d}{f \cdot B} ]距离翻倍误差大概是原来的四倍。这不是算法调参能解决的物理限制视差的量化误差在那里摆着。实测2米内还能做到3%以内放到5米就可能跳到15%。解决按这个公式反向推算自己方案的可用距离上限。如果要把5米的误差压到5%就得增大基线 (B)或者提高图像分辨率。还有一种工程做法是多帧聚合对同一个静态目标连续取几十帧视差图取中位数深度能有效压掉匹配噪声带来的随机误差。5.4 现象校正后画面被裁剪得太狠视野明显变小去畸变和极线校正都会让图像边缘产生无效区域。stereoRectify的alpha参数设为0时算法会把所有无效像素裁掉视野骤减。如果需要在视差图和RGB图上保留更多画面内容可以适当调大alpha。解决把stereoRectify的alpha改成0.1到0.3然后用cv2.getValidDisparityROI这类函数动态获取有效区域而不是死板地全图计算。需要注意alpha调大后视差图边缘会有更多无效值计算深度时要用掩码把这些点滤掉。5.5 现象左右图亮度不一致导致匹配大面积错乱两个USB摄像头各自启用自动曝光、自动白平衡时同一帧画面左右亮度会有明显差异SGBM的匹配代价直接失真。动态场景下这个问题更严重因为自动曝光是在持续变化的。解决用cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0)关闭自动曝光手动设置固定曝光值白平衡也用手动模式。如果一个相机枚举顺序不稳定可能是USB带宽或驱动的问题优先给两个摄像头分配独立的USB控制器。实在无法硬件同步时可以做逐帧直方图匹配来拉近左右亮度但这是补救手段不是根治方案。6. 让精度再进一步误差标定流程与多帧处理技巧测距系统做到能跑只是第一步能用才是目的。这里分享一个我保留了很久的习惯不管单目还是双目上线前都做一遍误差标定然后用多帧采样把随机噪声压下去。误差标定的流程很简单在0.5米到3米范围内每隔0.2到0.3米放一个已知尺寸的目标标定板或ArUco码都行记录真实距离和算法测出来的距离画出误差曲线。如果误差曲线呈现明显的系统性偏移可以拟合一阶多项式在输出端做补偿import numpy as np ground_truth np.array([500, 800, 1100, 1400, 1700, 2000], dtypenp.float32) measured np.array([512, 815, 1118, 1428, 1735, 2080], dtypenp.float32) # 一阶拟合估计值 系数 * 真实值 截距 coeff np.polyfit(ground_truth, measured, 1) compensated measured - np.polyval(coeff, ground_truth) print(原始误差(mm):, measured - ground_truth) print(补偿后残差(mm):, compensated)这段代码里的补偿系数只对当前这一套相机、镜头、基线、安装位置有效。换一个相机或者重新拆装过支架必须重新标定。把参数和当天拍摄环境的缩略图一起存档这是排查问题时最便宜的后悔药。多帧采样是另一个低成本提升手段。单帧测距结果会有随机的跳变尤其是ArUco角点或SGBM匹配在某些帧上突然失效。对静态目标取连续30帧的测量结果用中位数而不是平均值能有效滤掉极端跳变frames [] for _ in range(30): frame grab_frame() result measure_distance(frame, mtx, dist) if result is not None: frames.append(result[0]) if len(frames) 0: final_distance np.median(frames) # 中位数抗跳变能力强于均值如果要输出每一帧的深度图建议把reprojectImageTo3D得到的Z通道换成浮点视差有效的掩码区域再做中值滤波而不是直接对原始disp滤波否则边缘会被抹平。这套流程下来单目在1米内的误差能稳定到2%到3%双目在2米内做到3%左右是可以预期的。别指望5米外还能有这个精度视差量化误差在那个距离上是物理上限。看清边界、标好参数这个方向在你的项目里一定跑得稳。希望帮到你。本文还有配套的精品资源点击获取