news 2026/9/8 6:03:22

OpenCV单目测距实战:从相机标定到实时距离计算

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenCV单目测距实战:从相机标定到实时距离计算

简介:这是一份基于OpenCV与Python实现相机到物体距离测量的极简项目资源,面向计算机视觉初学者与需要快速实现单目测距功能的开发者。资源核心采用三角形相似度原理,使用前需要先标定两个关键参数——标记物体的真实宽度(或高度)以及相机到该物体的参考距离,然后通过图像处理自动识别物体在图像中的像素尺寸,推算出焦距,之后只需检测到目标物体即可实时估算距离。压缩包仅含2个文件,包括一个可直接执行的Python脚本和一个Markdown格式的说明文档,整体体积仅2KB,结构非常精简,脚本中留有参数初始化位置,便于对照代码理解完整流程并基于自身场景修改扩展。目前已有757人学习浏览,资源虽小但完整演示了从参数标定到实际测距的算法闭环,适合作为学习OpenCV测距原理的入门示例,也可作为后续开发视觉测距功能的基础模板。 做机器视觉项目的朋友应该都有过类似的经历:相机明明能把物体拍得清清楚楚,但系统并不知道它离自己有多远。实验室里量好距离、在代码里写死一个参数,现场环境一变就彻底失灵。今天聊的这套方案,就是基于OpenCV和Python,把“相机到物体的距离”从手工估计变成实时计算。这个需求在工业抓取、机器人避障、智能安防里非常常见,核心思路其实不复杂——初中几何的相似三角形就够用,真正麻烦的是工程落地时,如何把二维图像里的像素点和真实世界里的毫米距离对应起来。

这篇文章我会从原理到代码,把整个实现过程拆开讲清楚。内容包括单目测距的数学基础、相机标定的完整流程、基于已知物体尺寸的距离计算、以及实时视频流里的稳定测距实现。适合有Python基础、想快速在自己的摄像头项目里加入测距能力的读者,也适合正在做机器人、安防、工业检测方向的朋友参考。最终代码量不大,但每一步背后的“为什么”我会尽量说明白,免得你跑完一个demo还是不知道参数该怎么调。

1. 测距原理与方案选型:先搞清楚数学再写代码

1.1 单目测距的三角形相似原理

单目测距最常用的数学模型是三角形相似。先看最基础的关系式:

假设有一个物体实际宽度为 W(单位:毫米),它在相机传感器上成像的宽度为 w(单位:像素),相机焦距为 f(单位:像素)。物体到相机的距离 D 满足:

D = (W × f) / w

这个公式的前提是光轴基本垂直于物体表面,物体表面也相对平整。你看,这和初中物理里的凸透镜成像规律本质是一回事,只不过把像距近似等于焦距,因为物距远大于焦距时这个近似非常准确。

实际项目中,我们通常会测量物体在图像中的宽度或高度,但有个细节容易被忽略:检测到的宽度必须扣除镜头畸变带来的偏移,否则像素宽度本身就不准,后面算出来的距离自然也是错的。这也是为什么后面一定要做相机标定。另外,如果物体是旋转的、倾斜的,或者相机和物体之间有一个夹角,那公式里的 W 就不再是简单的物理宽度,你需要根据夹角做投影修正。这个坑我在做机械臂抓取时踩过好几次,后面会详细说解决办法。

1.2 双目视觉与单目方案的取舍

既然单目测距有那么多限制,那是不是直接用双目视觉更省心?双目方案通过两个相机同时拍摄同一场景,利用左右图像的视差计算深度,原理上更“硬核”,也确实不需要提前知道物体尺寸。但它也有明显的门槛:

对比维度单目测距双目测距
硬件成本单个普通相机需要两个同步相机,结构要求高
标定复杂度只需单相机内参标定需要双目标定、极线校正
对已知尺寸的依赖依赖已知物体尺寸不依赖,直接算视差
精度范围近距离精度高,远距离下降快基线和分辨率决定精度,算法复杂
性价比适合快速落地适合对精度和通用性要求高的场景

我做过的不少项目里,其实单目方案已经能覆盖大多数需求,尤其是固定安装、检测对象尺寸已知的场景。举个例子,仓库门口检测货箱距离,箱子规格是固定的,单目完全够用。双目更适合要求“任意物体”都能测距的场景,但算法复杂度、调试成本和计算资源都会明显上升。如果你刚开始做,我建议先用单目跑通流程,搞清楚精度瓶颈在哪,再决定要不要升级到双目。

2. 相机标定是精度的一半:棋盘格标定实战

2.1 为什么必须做标定

不标定就想测距,就像用一把没校零的游标卡尺量工件,量出来的数据只能看看,不能作为依据。相机镜头尤其是广角镜头,天然带有径向畸变切向畸变,表现就是画面边缘的直线会变弯曲,远近物体的大小比例关系也会被破坏。

标定的目的就是求出相机的内参矩阵畸变系数。内参矩阵里有 fx、fy(焦距相关参数)和 cx、cy(主点坐标),畸变系数则包括 k1、k2、k3(径向)、p1、p2(切向)。OpenCV 的calibrateCamera函数可以直接从一系列棋盘格图像中解算出这些参数。实际测试中,不做标定直接拿像素宽度算距离,在画面边缘的误差可能达到 20% 以上;做完标定并做畸变校正后,误差通常能控制在 3% 以内,这个差距是肉眼可见的。

2.2 棋盘格标定完整流程与代码

棋盘格标定的流程分四步:准备棋盘格、采集不同位姿的图像、提取角点、计算并保存参数。采集图像这一步最费时间,但也最影响最终精度。我的经验是至少采集 20~30 张,并且要覆盖画面的四角、边缘、中心等不同区域,角度要有倾斜、俯仰、旋转的变化,同时每张图之间棋盘格必须有足够的位移差。只用几张正对着镜头的图片没有太大意义。

下面是完整标定代码,我直接贴一段我自己常用的实现:

import cv2 import numpy as np board_size = (9, 6) # 内角点数量,不是格子数量 square_size = 0.025 # 每个格子的物理边长,单位:米 # 生成棋盘格内角点的三维坐标 objp = np.zeros((board_size[0] * board_size[1], 3), np.float32) objp[:, :2] = np.mgrid[0:board_size[0], 0:board_size[1]].T.reshape(-1, 2) objp *= square_size obj_points = [] # 世界坐标 img_points = [] # 像素坐标 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, board_size, None) if ret: # 亚像素级角点精化,这一步能明显提升标定精度 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners2) cv2.drawChessboardCorners(img, board_size, corners2, ret) cv2.imshow('corners', img) cv2.waitKey(100) ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print("内参矩阵:\n", mtx) print("畸变系数:\n", dist) # 保存标定结果 np.savez('camera_calib.npz', mtx=mtx, dist=dist)

findChessboardCorners提取出来的是整数像素坐标,直接用会产生不小的误差,所以必须用cornerSubPix在附近做亚像素级别的迭代搜索。这一步很多人会跳过,但它的计算量不大,对精度提升却非常明显。另外,square_size一定要填对单位,如果填错,内参里的焦距值并不会错,但实际测距时换算回物理尺寸会整体偏移。

2.3 标定结果的使用与反畸变

标定结果保存成 npz 文件后,在实际测距代码里加载它,并把图像先做一次畸变校正。OpenCV 有两种做法:一种是用undistort,适合一次性处理;另一种是用initUndistortRectifyMap生成映射表,再用remap,适合视频流的逐帧处理,因为映射表只需要计算一次。

mtx = np.load('camera_calib.npz')['mtx'] dist = np.load('camera_calib.npz')['dist'] h, w = frame.shape[:2] newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) mapx, mapy = cv2.initUndistortRectifyMap( mtx, dist, None, newcameramtx, (w, h), cv2.CV_32FC1 ) undistorted = cv2.remap(frame, mapx, mapy, cv2.INTER_LINEAR)

这里的getOptimalNewCameraMatrix带有一个参数 alpha,alpha=1表示保留所有原始像素,画面边缘会出现黑边;alpha=0表示裁剪掉畸变严重的区域,画面会更干净但视野会稍微变小。视频中实时运行时,undistort每帧都要重新计算,CPU 占用高,而先算remap映射表的方案只做查表,速度要明显更快。这个优化虽然简单,在树莓派这类低算力设备上能省下很多资源。

3. 核心代码实现:从像素坐标到物理距离

3.1 焦距计算与已知物体测距

标定完成后,你就知道了相机的焦距 f(单位是像素)。如果你用的是标定得到的 mtx 矩阵里的 fx,通常直接用 fx 当作公式里的 f 就可以。但如果你用的摄像头没有标定数据,还有一种临时办法:先在已知距离 D 处放一个宽度为 W 的物体,测出它在图像中的像素宽度 w,反推焦距:

f = (D × w) / W

这个方法不需要棋盘格,但前提是物体和光轴垂直、距离测量准确,否则反推出的 f 本身就是带误差的。

先看静态图片测距的完整例子。假设我有一张已经做过畸变校正的图,图中的目标物体是 A4 纸盒,已知宽度 0.21 米,我用轮廓检测找到它在图像中的宽度:

import cv2 import numpy as np KNOWN_WIDTH = 0.21 # 物体实际宽度,单位:米 FOCAL_PIXEL = 850.0 # 标定得到的 fx frame = cv2.imread('box.jpg') frame = cv2.remap(frame, mapx, mapy, cv2.INTER_LINEAR) # 畸变校正 # 假设你已经用颜色/轮廓提取到了物体的 bounding box # 这里用 HSV 分割 + 轮廓外接矩形做示范 hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask = cv2.inRange(hsv, (10, 60, 60), (20, 255, 255)) # 根据实际物体调色 contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area = cv2.contourArea(cnt) if area < 5000: # 过滤小噪点 continue x, y, w, h = cv2.boundingRect(cnt) distance = (KNOWN_WIDTH * FOCAL_PIXEL) / w cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.putText(frame, f"dist: {distance:.2f} m", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)

这段代码里最关键的一行就是distance = (KNOWN_WIDTH * FOCAL_PIXEL) / w。它的意思是,物体在图像里占据的像素越多,距离就越近,像素越少,距离就越远,而且距离和像素宽度是反比例关系。你可以自己验证一下:如果距离翻倍,像素宽度正好减半,公式算出来的距离也正好翻倍,这个线性反比关系就是单目测距的全部秘密。

如果你要检测的不是矩形物体,而是圆形的,那可以用最小外接圆,把w换成圆的直径,公式完全一样。如果你要检测的是不规则形状,建议用轮廓的外接矩形宽度而不是轮廓面积,因为面积和距离之间是平方关系,换算起来容易出错。实际上,我也见过有人用轮廓面积反推距离,数学上也能算,但面积对形变太敏感,稍微挡一点计算值就会飘,实战中不如宽度稳定。

3.2 实时视频流测距实现

静态图片测距跑通之后,把它扩展到视频流里非常自然,只需要加上逐帧处理的循环。但视频场景里有两个新的问题:一是怎么稳定地检测目标,二是怎么让距离输出不抖动。针对检测,如果你控制不了场景里物体的颜色,用颜色阈值分割很容易误检,更稳妥的做法是用ArUco 标记或者平面图案,它们天然带编码信息,识别稳定且能直接给出位姿。

ArUco 标记是 OpenCV 里一个很实用的工具,一个黑白二维码样式的方形标记,里面编码了 ID。检测到它之后,不仅能算出中心位置,还能通过estimatePoseSingleMarkers直接得到相机和标记之间的平移向量,平移向量的 z 分量就是距离。下面是实时测距的一段实现:

import cv2 import numpy as np aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50) detector_params = cv2.aruco.DetectorParameters() detector = cv2.aruco.ArucoDetector(aruco_dict, detector_params) marker_length = 0.05 # ArUco 标记实际边长,单位:米 mtx = np.load('camera_calib.npz')['mtx'] dist = np.load('camera_calib.npz')['dist'] cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break # 畸变校正 h, w = frame.shape[:2] newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) mapx, mapy = cv2.initUndistortRectifyMap( mtx, dist, None, newcameramtx, (w, h), cv2.CV_32FC1 ) frame = cv2.remap(frame, mapx, mapy, cv2.INTER_LINEAR) corners, ids, _ = detector.detectMarkers(frame) if ids is not None: rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers( corners, marker_length, newcameramtx, dist ) for i in range(len(ids)): distance = tvecs[i][0][2] # z 轴分量就是距离 cv2.aruco.drawDetectedMarkers(frame, corners, ids) cv2.drawFrameAxes(frame, newcameramtx, dist, rvecs[i], tvecs[i], 0.03) org = (int(corners[i][0][0][0]), int(corners[i][0][0][1]) - 20) cv2.putText(frame, f"id={ids[i][0]} dist={distance:.3f} m", org, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow('distance', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

ArUco 的优势在于它不仅能测距,还能测出标记相对相机的旋转角度。如果你做的是机器人导航,这个旋转信息可以直接用来判断目标是否正对相机。有一点要注意,estimatePoseSingleMarkers返回的tvecs单位和你传入的marker_length一致,如果你传的是米,出来的距离就是米;如果你传的是厘米,出来的就是厘米,最容易犯的错就是单位不统一导致距离差了 100 倍。

视频流里如果只是简单地把每帧算出来的距离直接显示,你会看到数值一直在抖动。这是因为检测到的角点坐标含有亚像素误差,图像噪声也会让检测结果轻微跳动。解决抖动的办法是把最近几帧的距离做一个滑动平均,或者用一个简单的一阶低通滤波:

smoothed = 0.9 * smoothed + 0.1 * current_distance

这个式子会让新的距离只贡献 10% 的变化量,数值会平稳很多,但响应速度会稍微变慢。实际使用时要根据目标移动速度来调整系数,移动快的目标系数可以调到 0.3~0.5,静止的目标调到 0.05~0.1 都可以。

4. 常见问题与精度调优

4.1 常见问题速查表

在实际跑项目时,我积累了一些出现过很多次的问题,整理成一张速查表,基本覆盖了初学者最容易翻车的几个点:

问题现象可能原因解决方法
ModuleNotFoundError: No module named 'cv2'Python 环境里没有安装 OpenCV在命令行执行pip install opencv-python,Anaconda 用户建议conda install opencv
findChessboardCorners找不到角点棋盘格图像太暗、反光,或角点数设置错误调整光照,换哑光纸打印棋盘格,确认board_size是内角点数量
标定出的 fx 异常大或异常小square_size单位填错,或采集图像太少统一物理尺寸单位,至少采集 20 张以上不同位姿图像
测出的距离整体偏小或偏大焦距不准,或畸变校正没做重新标定,确认代码里用的图像确实经过了remap校正
视频流里距离值剧烈跳动角点检测噪声、没有平滑滤波加滑动平均或一阶低通滤波,固定相机不要有震动
ArUco 检测不到标记marker 太小、太远、分辨率不够换更大尺寸的标记,降低检测阈值,确保光照均匀
近距离和远距离测距误差差异大忽略了对焦变化和视角倾斜固定焦距模式,尽量让相机正对目标,避免斜视

最容易翻车的其实是标定环节。比如board_size=(9, 6)指的是每行 9 个内角点、每列 6 个内角点,也就是棋盘格一共有 10×7 个格子,有人拿 9×6 的格子当内角点数填进去,代码直接报错。还有人在打印棋盘格时用了普通 A4 纸但没有贴平整,或者纸面有反光,角点检测的坐标会偏移 1~2 个像素,这些误差最终都会转化成测距误差。

4.2 提升精度的实操经验

聊完问题,再分享几条我认为对精度提升最有帮助的实战经验。

第一,相机尽量固定焦距、固定光圈。手机摄像头和很多网络摄像头都有自动对焦功能,对焦变化会让等效焦距跟着变,而公式里的 f 是一个固定值,对焦一变,测出来的距离就会偏。如果你的摄像头支持手动对焦,先把对焦环固定住;如果支持自动对焦但无法手动关掉,至少保证目标始终在同一个深度范围内,这样对焦能相对稳定。

第二,测量距离不要超过物体宽度的 20~30 倍。举个例子,一个 10 厘米宽的目标,在 3 米远时它在 100 万像素摄像头里大约只占 20~30 个像素宽度,此时单个像素的量化误差就占了 5%,测距误差会非常明显。想测更远,要么换更高分辨率的相机,要么换更大的目标。这不是算法能解决的问题,是物理分辨率的天花板。

第三,相机光轴要尽量垂直于目标表面。相似三角形公式默认物体平面是正对着相机的,如果物体偏转 30 度,它投影出来的宽度会变成原来的 cos30 倍,算出来的距离就会偏大。一般不太容易精确控制夹角,所以最好在标定和实测时都保持相近的姿态,这样误差会系统性地抵消掉一部分。

第四,用多帧平滑加 Kalman 滤波。如果目标不是静止的,简单的滑动平均会有滞后,Kalman 滤波能同时预测和更新,响应更快更稳。OpenCV 里没有直接的卡尔曼类,但用filterpy或手写几行卡尔曼也不复杂。我自己在低速移动的机器人测距场景里,一开始用滑动平均老觉得距离滞后,后来换成卡尔曼,效果好了不少。

第五,不同距离段分别标定和验证。焦距是一个全局参数,但实际镜头在近距离和远距离的畸变表现不完全一致,单一焦距很难在所有距离段都做到最优。我的做法是分别在 0.5 米、1 米、2 米、3 米距离各放一个已知尺寸的目标,实测一遍,看哪个距离段误差最大,然后针对性地调整或做分段校准表,用插值的方式把中间距离的距离值再修正一遍。

5. 写在最后的几句真心话

这套方案做下来,你会发现在工业应用里,精度要求高的时候,单目测距还是有点吃力,但它在很多“够用就好”的场景里真的很有价值。比如智能垃圾桶检测人的距离、无人机定高、机器人寻找充电桩、安防摄像头锁定目标并估算远近,这些场景不需要毫米级精度,单目方案又便宜又好部署,踩过坑之后也容易调优。

最后再分享一个小技巧:如果你不知道自己的相机焦距,又懒得做完整的棋盘格标定,可以先拿一个已知尺寸的物体放在已知距离,反推焦距,然后在这个基础上再慢慢微调。这个临时焦距虽然不如标定结果精确,但能让你在半小时内跑通一整套测距逻辑,先看到效果,再决定要不要投入时间做标定。我自己做任何视觉项目的习惯都是“先用最笨的办法跑通全流程,再优化关键环节”,这样心里有底,出了问题也知道往哪个方向查。希望这篇文章能帮你少踩几个坑,把距离测得更稳一点。

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

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

用八种软件结构风格实现KWIC:设计图与代码实战

简介&#xff1a;一份面向软件工程学习者与开发者的KWIC系统实现资料&#xff0c;围绕管道过滤器、虚拟机、仓库、黑板、事件驱动、分层、面向对象、客户端-服务器八种软件结构风格&#xff0c;分别给出可运行的Java实现代码、设计图与要求文档&#xff0c;用于理解不同结构风格…

作者头像 李华
网站建设 2026/9/8 6:03:00

Kotlin中缀函数全解析:语法、优先级、性能与DSL实战

pairOf("id", 1001)和"id" to 1001之间&#xff0c;差的只是几个字符&#xff0c;但读起来的感受完全不一样。第一次在 Kotlin 代码里看到mapOf("name" to "kotlin")的时候&#xff0c;大多数人都会愣一下&#xff1a;这个to是关键字吗…

作者头像 李华
网站建设 2026/9/8 6:02:41

VOC格式中国交通数据集实战:4000张图片转YOLOv8训练全流程

简介&#xff1a;面向自动驾驶与智能交通研究的中国交通数据集&#xff0c;基于PASCAL VOC标准构建&#xff0c;收录4000张涵盖城市道路、高速路与乡村路等多种场景的交通图像&#xff0c;标注了车辆、行人、交通标志、道路等关键元素&#xff0c;可用于目标检测、实例分割、语…

作者头像 李华
网站建设 2026/9/8 6:02:21

自制准直驱执行器:行走机器人关节的力控与调参实战

很多开发者从机械臂、舵机或者普通电机转入行走机器人领域后&#xff0c;最先劝退自己的往往不是步态算法&#xff0c;而是最底层的关节执行器。关节空载响应很好&#xff0c;一带负载就发抖、发热&#xff0c;甚至一受冲击就扫齿&#xff0c;这类现象在四足、双足机器人项目里…

作者头像 李华
网站建设 2026/9/8 6:02:12

Java面试官最看重的三个基础能力,你有吗?

很多Java开发者都有这样的困惑&#xff1a;明明面试题背得滚瓜烂熟&#xff0c;项目经验写得满满当当&#xff0c;可一到面试环节就被刷下来。问题出在哪&#xff1f;最近面试了不少Java开发者&#xff0c;简历一个比一个唬人——“精通Spring Cloud Alibaba”“深入理解JVM源码…

作者头像 李华