news 2026/9/28 7:12:42

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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], flags=cv2.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, flags=cv2.CALIB_ZERO_DISPARITY, alpha=0 ) # 生成映射表 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( minDisparity=0, numDisparities=128, # 必须是16的倍数 blockSize=5, # 匹配块大小,奇数 P1=8 * 3 * 5 ** 2, P2=32 * 3 * 5 ** 2, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=32, preFilterCap=63, mode=cv2.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.0

numDisparities决定搜索范围,物体越近、基线越长,需要的值越大,但计算量也越大。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, key=cv2.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算均值。

还有一个容易被忽略的点:相机同步。如果左右相机不是硬件同步,拍运动物体会产生错位,视差计算直接崩。毕业设计里测静态物体,用软件触发同时拍就行;如果要测运动物体,必须用硬件同步线或者支持同步的工业相机。

最后说一个我自己的习惯:每次换场景,先拍一张标定板,检查重投影误差有没有变化。如果误差突然变大,说明相机被碰过或者镜头松了,重新标定比硬调参数靠谱。双目测量这件事,标定是地基,匹配是墙,尺寸换算是屋顶,地基歪了,后面怎么修都是白费。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 7:12:33

Prompt Engineering实战:用TaoToken统一Key打通结构化Prompt工程化链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 7:11:50

用Dify打造hindsight复盘助手:从后见之明偏差到自动化经验重放

hindsight这个词我一直觉得直接翻译成“事后诸葛亮”有点委屈它。英文里的hindsight,本意是“回看过去时的理解”,它是一面镜子,让你看清自己当时究竟漏掉了什么、哪里被认知盲区遮住了。真正的问题在于,这面镜子大多数人不会主动…

作者头像 李华
网站建设 2026/9/28 7:11:17

S7-1500博图产线例程精读:从OB/FB架构到通信报警实战

第一次真正看懂西门子S7-1500生产线例程,是在一个汽车零部件焊装项目上。那会儿我已经写了两三年单机设备程序,自认为对博图(TIA Portal)熟得很,结果打开总控程序还是被震了一下——不是指令用得有多花哨,而…

作者头像 李华
网站建设 2026/9/28 7:10:52

MSPM0G3507 GPIO控制实战:VS Code+逐飞库快速上手

1. 项目概述:为什么选MSPM0G3507配逐飞库做GPIO控制?TI的MSPM0G3507不是一块“新贵”,而是被很多老工程师悄悄盯上的“性价比黑马”。它属于MSPM0系列,是TI在2023年主推的超低功耗、高集成度Cortex-M0 MCU,主频48MHz&a…

作者头像 李华
网站建设 2026/9/28 7:10:50

FMT飞控移植RT-Thread实战:内核适配、SCons构建与协同调试

1. 为什么飞控移植不能只靠“抄代码”:FMT RT-Thread 的真实协作逻辑FMT 开源飞控、RT-Thread 实时操作系统、SCons 构建系统——这三个词凑在一起,不是简单拼凑的关键词堆砌,而是一条正在被越来越多嵌入式开发者验证的高效开发路径。我第一…

作者头像 李华
网站建设 2026/9/28 7:10:21

微信小程序云开发实战:个人事务物品备忘录系统设计

你手机里现在躺了几个备忘录?我数过自己手机,待办应用两个、笔记应用三个、相册里还存着几十张“这玩意儿当时放在这儿”的照片。问题是:事务提醒和物品位置记录互相割裂,真到找东西或者赶截止时间的时候,信息散得根本…

作者头像 李华