Python+OpenCV双目视觉测尺寸:从标定到三维换算的完整实战

发布时间:2026/9/28 7:12:44
Python+OpenCV双目视觉测尺寸:从标定到三维换算的完整实战 简介这是一份面向计算机、通信、人工智能、自动化等专业学生与从业者的双目视觉测量项目资料以Python结合OpenCV实现被摄物体尺寸的非接触式测量可作为毕业设计、课程大作业或期末课程设计的参考方案也适合具备一定基础后在此基础上修改扩展功能。压缩包共23个文件约2.26MB其中21张jpg为左右相机采集的标定与测试图像1个py脚本承载核心测量逻辑1个md文档说明项目结构与使用方式整体轻量、便于快速上手。目前已有200人学习浏览。项目代码经过调试测试可直接运行答辩评审分达98分读者可从中掌握双目相机标定、立体匹配、视差计算与尺寸换算的完整流程并借助配套图像与说明文档复现实验、理解各模块作用为后续视觉测量类课题提供可借鉴的实现思路与排错参考。1. 双目视觉测尺寸从「两个摄像头」到「一把卡尺」的距离用 Python OpenCV 做双目视觉测量物体尺寸本质上是给电脑装上一双能判断远近的眼睛左右两个摄像头同时拍下同一场景通过视差算出每个像素对应的真实物理尺度再在图像上框出目标就能反推出物体的长宽。这套方案最直接的价值是把「拿卡尺量」变成「拍一张照就能读尺寸」适合做毕业设计、产线抽检、物流体积估算这类场景。很多人第一次听到「双目测尺寸」会觉得玄学其实核心链路只有四步标定、校正、匹配、换算。真正让人翻车的不是算法本身而是标定板没拍好、镜头畸变没消干净、匹配点飘了半个像素。这篇笔记按我实际做项目的顺序把每一步的参数、代码和踩坑点讲清楚新手能照着跑通熟手能对照检查自己的边界条件。2. 双目测尺寸的底层逻辑视差、标定与坐标系2.1 为什么两个摄像头能算出真实尺寸单目摄像头拍一张图你只能知道物体在图像里占多少像素但不知道它离镜头多远所以像素和毫米之间没有固定换算关系。双目视觉多了一个维度左右相机对同一个点成像时水平方向会有偏移这个偏移量叫视差。物体越近视差越大越远视差越小。根据相似三角形关系深度 Z 等于焦距乘以基线距离再除以视差。拿到深度之后再用相机的内参把像素坐标映射到物理坐标就能算出物体在三维空间里的实际长度。这里有一个容易被忽略的点双目测尺寸不是直接测「物体表面」而是测「物体上被匹配到的特征点」。如果物体表面纹理很弱比如纯白纸盒、金属反光面匹配算法找不到稳定的对应点深度图就会出现空洞尺寸自然测不准。常见做法是在被测物表面贴一些随机纹理贴纸或者用结构光投影仪打散斑。毕业设计里如果只做演示选一个有花纹的纸盒或书本封面就够用。焦距、基线、视差这三个量里焦距和基线来自标定视差来自匹配。标定不准后面全错匹配不稳尺寸跳动。所以整个项目的精度上限在标定阶段就已经定死了。2.2 相机标定把内参、外参和畸变系数一次拿全标定是双目项目里最枯燥但最不能省的一步。我一般用张正友标定法OpenCV 里对应cv2.calibrateCamera和cv2.stereoCalibrate。需要准备一块黑白棋盘格标定板打印在平整的硬板上格子尺寸用尺子量准比如 25mm。左右相机同时拍摄 15 到 20 组不同角度的棋盘格图像覆盖画面中心、四角、远近不同位置。下面是我常用的标定代码骨架先分别标定左右相机再做双目标定import cv2 import numpy as np import glob # 棋盘格内角点数量比如 9x6 表示每行9个内角每列6个内角 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_left [] # 左图二维点 imgpoints_right [] # 右图二维点 left_images sorted(glob.glob(calib/left/*.jpg)) right_images sorted(glob.glob(calib/right/*.jpg)) for l_img, r_img in zip(left_images, right_images): img_l cv2.imread(l_img) img_r cv2.imread(r_img) gray_l cv2.cvtColor(img_l, cv2.COLOR_BGR2GRAY) gray_r cv2.cvtColor(img_r, cv2.COLOR_BGR2GRAY) ret_l, corners_l cv2.findChessboardCorners(gray_l, chessboard_size, None) ret_r, corners_r cv2.findChessboardCorners(gray_r, chessboard_size, None) if ret_l and ret_r: # 亚像素级角点优化窗口大小 11x11迭代终止条件 criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_l cv2.cornerSubPix(gray_l, corners_l, (11, 11), (-1, -1), criteria) corners_r cv2.cornerSubPix(gray_r, corners_r, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints_left.append(corners_l) imgpoints_right.append(corners_r) # 单目标定 ret_l, mtx_l, dist_l, _, _ cv2.calibrateCamera(objpoints, imgpoints_left, gray_l.shape[::-1], None, None) ret_r, mtx_r, dist_r, _, _ cv2.calibrateCamera(objpoints, imgpoints_right, gray_r.shape[::-1], None, None) # 双目标定 ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F cv2.stereoCalibrate( objpoints, imgpoints_left, imgpoints_right, mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], flagscv2.CALIB_FIX_INTRINSIC ) print(基线长度(mm):, np.linalg.norm(T)) print(左相机内参:\n, mtx_l) print(右相机内参:\n, mtx_r)这段代码里几个参数直接决定标定质量。chessboard_size必须和实际打印的棋盘格内角点一致数错一个角点后面全部报错。square_size是格子物理边长单位是毫米量的时候用游标卡尺不要用直尺估。cornerSubPix的窗口(11,11)是搜索窗口图像分辨率高可以适当加大但别超过 21否则会把邻近角点带偏。stereoCalibrate里的CALIB_FIX_INTRINSIC表示固定单目标定得到的内参只优化外参这样更稳。如果标定重投影误差超过 0.5 像素建议重新拍标定图不要硬调参数。标定完成后把mtx_l、dist_l、mtx_r、dist_r、R、T保存成 npz 文件后面每次测量直接加载不用重复标定。2.3 立体校正与视差计算让左右图像行对齐标定拿到的是原始参数但左右相机安装时不可能完全平行图像之间存在旋转和平移。立体校正的作用是把左右图像重投影到同一个平面上让同一个物点在左右图中处于同一行这样匹配时只需要在同一行搜索速度快且不容易错。OpenCV 用cv2.stereoRectify计算校正映射再用cv2.initUndistortRectifyMap生成映射表最后用cv2.remap应用到每一帧。校正之后可以用cv2.StereoSGBM_create计算视差图。SGBM 的参数比较多我一般先按下面这套起步# 立体校正 R1, R2, P1, P2, Q, _, _ cv2.stereoRectify( mtx_l, dist_l, mtx_r, dist_r, gray_l.shape[::-1], R, T, flagscv2.CALIB_ZERO_DISPARITY, alpha0 ) # 生成映射表 map_lx, map_ly cv2.initUndistortRectifyMap(mtx_l, dist_l, R1, P1, gray_l.shape[::-1], cv2.CV_32FC1) map_rx, map_ry cv2.initUndistortRectifyMap(mtx_r, dist_r, R2, P2, gray_r.shape[::-1], cv2.CV_32FC1) # SGBM 视差计算 stereo cv2.StereoSGBM_create( minDisparity0, numDisparities128, # 必须是16的倍数 blockSize5, # 匹配块大小奇数 P18 * 3 * 5 ** 2, P232 * 3 * 5 ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange32, preFilterCap63, modecv2.STEREO_SGBM_MODE_SGBM_3WAY ) # 对校正后的图像计算视差 left_rect cv2.remap(img_l, map_lx, map_ly, cv2.INTER_LINEAR) right_rect cv2.remap(img_r, map_rx, map_ry, cv2.INTER_LINEAR) disparity stereo.compute(left_rect, right_rect).astype(np.float32) / 16.0numDisparities决定搜索范围物体越近、基线越长需要的值越大但计算量也越大。blockSize影响匹配平滑度纹理弱就调大但边缘会变糊。uniquenessRatio是唯一性检查值越大越严格能过滤掉误匹配但太小会留下噪点。speckleWindowSize和speckleRange用来剔除小连通区域的噪点视差图花斑多就加大这两个值。校正后一定要肉眼检查左右图中同一个特征点是否在同一水平线上。如果偏差明显说明标定或校正有问题不要继续往下做。3. 从视差图到尺寸读数分割、拟合与物理换算3.1 用深度阈值把目标从背景里抠出来视差图本身是一张灰度图亮的地方近暗的地方远。要测某个物体的尺寸先得把它从背景里分离出来。最简单的方法是用深度阈值先测一下背景的视差值然后设定一个范围只保留在这个范围内的像素。比如背景在 1.2 米左右物体在 0.8 米左右就可以用视差范围把物体抠出来。# 将视差转为深度单位毫米 f P1[0, 0] # 校正后的焦距 baseline np.linalg.norm(T) # 基线 depth f * baseline / (disparity 1e-6) # 深度阈值分割保留 600mm 到 1000mm 之间的区域 mask cv2.inRange(depth, 600, 1000) # 形态学去噪 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 找最大轮廓 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: target max(contours, keycv2.contourArea) rect cv2.minAreaRect(target) box cv2.boxPoints(rect) box np.int0(box) cv2.drawContours(left_rect, [box], 0, (0, 0, 255), 2)cv2.inRange的上下限要根据实际场景调太窄会切掉物体边缘太宽会把背景带进来。形态学开运算去掉小噪点闭运算填补空洞。minAreaRect得到的是最小外接旋转矩形适合测矩形物体如果物体形状不规则可以用cv2.boundingRect取正矩形或者用轮廓拟合。3.2 像素尺寸到物理尺寸的换算别再用单目那套单目测尺寸常用「像素数乘以一个固定比例」但那个比例只在特定距离下成立。双目测尺寸的正确做法是对目标轮廓上的每个点用深度值算出它在相机坐标系下的三维坐标再计算三维空间中的距离。# 将像素坐标和深度转为三维点 def pixel_to_3d(u, v, depth_val, Q): # Q 是 stereoRectify 输出的重投影矩阵 vec np.array([u, v, depth_val, 1.0]) point_3d Q vec point_3d / point_3d[3] return point_3d[:3] # 取最小外接矩形的四个角点计算三维长度 corners_3d [] for point in box: u, v point d depth[int(v), int(u)] if d 0: corners_3d.append(pixel_to_3d(u, v, d, Q)) if len(corners_3d) 4: # 计算相邻角点之间的三维距离 width np.linalg.norm(corners_3d[0] - corners_3d[1]) height np.linalg.norm(corners_3d[1] - corners_3d[2]) print(f物体宽度: {width:.2f} mm, 高度: {height:.2f} mm)这里的关键是Q矩阵它由stereoRectify输出包含了焦距、基线和主点偏移。用Q把像素坐标和视差映射到三维空间得到的是校正后相机坐标系下的点。计算距离时取的是三维点之间的欧氏距离而不是像素距离乘以比例。这样即使物体倾斜测出来的也是真实边长。如果物体表面不是一个平面比如圆柱体最小外接矩形只能给出投影尺寸。要测直径可以在轮廓上取多个点拟合圆或者取最大宽度。常见做法是对轮廓做cv2.fitEllipse然后取椭圆的长轴和短轴。3.3 精度验证用已知尺寸的物体做对照做完上面几步你得到的是一个读数但这个读数准不准必须验证。我一般用一块标准尺寸的矩形板比如 100mm × 80mm放在不同距离下测 10 次记录误差。如果误差在 ±2mm 以内对于毕业设计已经够用如果超过 ±5mm就要回头查标定和匹配。验证时注意几点物体要正对相机倾斜会引入投影误差光照要均匀避免反光导致视差空洞背景要尽量远避免深度阈值把背景带进来。每次改变距离后不需要重新标定但深度阈值范围要跟着调。4. 避坑与排查双目测尺寸最常见的 5 个翻车现场4.1 标定重投影误差大尺寸整体偏大或偏小现象标定完成后测同一个物体读数总是比实际大 3% 到 5%换距离也一样。原因棋盘格打印尺寸不准或者square_size填错。很多人用 A4 纸打印棋盘格打印机有缩放实际格子边长和设定值不一致。另外标定图像太少或角度太单一导致焦距估计有偏差。解决用游标卡尺量打印出来的格子实际边长重新填square_size。标定图至少 15 组覆盖画面四个角和远近。如果重投影误差大于 0.5 像素重新拍。4.2 视差图大面积空洞物体边缘测不到现象深度图上物体表面有很多黑色空洞轮廓不完整尺寸跳动大。原因物体表面纹理太弱SGBM 找不到匹配点。或者左右相机曝光差异大同一物点在左右图中亮度不一致。解决给物体贴随机纹理贴纸或者用投影仪打散斑。如果是曝光问题把左右相机设为手动曝光调成相同参数。SGBM 的blockSize可以适当加大到 7 或 9但不要超过 11。4.3 测出来的尺寸随距离变化越远越不准现象同一个物体放在 0.8 米测是 100mm放到 1.5 米测变成 105mm。原因视差精度随距离下降。深度误差和距离的平方成正比距离翻倍误差变四倍。另外numDisparities设小了远处物体的视差超出搜索范围匹配失败。解决根据工作距离调整numDisparities一般设为 128 或 256。如果精度要求高把相机基线加长或者换更高分辨率的相机。毕业设计里把测量距离控制在 0.5 到 1.2 米之间精度最稳。4.4 最小外接矩形把背景框进来尺寸偏大现象画出来的矩形比物体大一圈测出来的宽度明显偏大。原因深度阈值范围太宽背景和物体连在一起轮廓合并了。或者形态学闭运算核太大把物体和背景之间的缝隙填上了。解决缩小cv2.inRange的上下限让背景完全被排除。形态学核从 5×5 开始试不要一上来就用 15×15。如果物体和背景深度接近考虑用背景减除或者手动框选 ROI。4.5 代码报错 module not found 或 opencv 版本不兼容现象运行时报ModuleNotFoundError: No module named cv2或者cv2.StereoSGBM_create参数报错。原因OpenCV 没装或者装的是opencv-python但版本太老。SGBM 的参数在不同版本里有差异比如cv2.STEREO_SGBM_MODE_SGBM_3WAY在旧版本里可能不存在。解决用 pip 安装最新版opencv-python和opencv-contrib-python命令是pip install opencv-python opencv-contrib-python --upgrade。如果还报错检查 Python 环境是不是多个版本混用建议用虚拟环境。VSCode 里配置 Python 解释器时选对虚拟环境路径。5. 把测量精度再压一压亚像素匹配与多帧平均如果你已经跑通了上面的流程但觉得精度还不够可以试试两个进阶技巧。第一个是亚像素级视差优化。SGBM 输出的视差是整数或者半像素对于高精度测量不够。可以用cv2.cornerSubPix对特征点做亚像素定位或者用cv2.StereoSGBM的mode参数配合cv2.ximgproc里的滤波模块做视差细化。不过cv2.ximgproc在opencv-contrib-python里才有装的时候别漏了。第二个是多帧平均。静态测量时连续拍 10 帧对每个角点的三维坐标取平均能有效抑制随机噪声。我实测过单帧误差在 ±1.5mm 左右10 帧平均后能压到 ±0.8mm。代码很简单把corners_3d存进列表最后用np.mean算均值。还有一个容易被忽略的点相机同步。如果左右相机不是硬件同步拍运动物体会产生错位视差计算直接崩。毕业设计里测静态物体用软件触发同时拍就行如果要测运动物体必须用硬件同步线或者支持同步的工业相机。最后说一个我自己的习惯每次换场景先拍一张标定板检查重投影误差有没有变化。如果误差突然变大说明相机被碰过或者镜头松了重新标定比硬调参数靠谱。双目测量这件事标定是地基匹配是墙尺寸换算是屋顶地基歪了后面怎么修都是白费。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询