☰
Ultralytics YOLO11单目视觉测距实战:从相机标定到距离计算完整链路
2026/10/21 3:04:46 网站建设 项目流程

做视觉测距这个需求,十个里有八个是从“检测到框”开始,到“算出距离”结束。YOLO系列模型发展到Ultralytics YOLO11,检测精度和推理速度已经非常成熟,但很多人把模型跑通了才发现,框出来容易,距离难算。这篇博文就围绕Ultralytics YOLO11做距离计算这件事,把像素坐标到物理距离的完整链路拆开讲清楚,从相机成像原理、焦距标定,到检测结果换算、误差排查,给你一条可以直接落地的实现路径。

1. 距离计算的整体思路与方案选型

先说结论:YOLO11本身只负责“在图像里找到目标并画出边界框”,它输出的是像素坐标,不包含任何深度信息。想从二维像素算出三维物理距离,必须在模型之外再搭一套几何换算逻辑,整套系统才算闭环。

1.1 为什么不能直接使用检测框的像素大小算距离

很多人第一反应是“物体在画面里越小,距离越远”,然后试图用一个线性函数拟合边框高度和距离的关系。这个思路在固定场景、固定相机角度下勉强能用,换到别的场景就彻底失效。根本原因在于相机成像是一个透视投影过程,物体在图像上的大小与距离之间是倒数关系,不是线性关系。距离10米和距离20米时,物体在画面里缩小的幅度完全不同。

真正可靠的做法,是走针孔相机模型加相似三角形换算。这个方案的前提是你知道目标的实际物理尺寸(比如人的平均身高、车牌的宽度),然后利用相机焦距这个内部参数,把像素高度换算成物理距离。

1.2 三种主流测距方案的取舍

方案一:单目视觉测距(本项目采用的核心方案)

只用一个普通摄像头,借助几何模型和已知尺寸先验来估算距离。优点是成本低、部署简单、算力要求小,适合大部分安防和工业场景。缺点是精度依赖目标尺寸先验的准确性,且无法应对目标遮挡。

方案二:双目立体视觉测距

通过两个相机拍摄同一场景,利用左右视图的视差计算深度。精度高,不受目标尺寸先验限制,但需要严格的相机同步和标定流程,硬件成本和计算开销都翻倍。适合对精度要求极高、场景可控的室内环境。

方案三:深度相机直接测距

用深度传感器直接获取每个像素的深度值,原理最简单,精度也稳定。但深度相机普遍视场角有限,户外强光下性能急剧下降,设备成本也高于普通网络摄像头。

综合来看,单目方案在“快速落地验证 + 中远距离估算”这个需求上性价比最高。本项目选它,不是因为它能做到毫米级精度,而是因为它能在普通摄像头和YOLO11的基础上,用最小的改动换来可用的距离估计能力。

1.3 完整技术链路

整个系统分为四个模块,串起来就是一条完整链路:

  • 目标检测模块:基于Ultralytics YOLO11,加载预训练权重,对视频帧执行推理,输出边界框、类别和置信度。
  • 相机标定模块:计算相机内参矩阵和畸变系数,核心是获取焦距像素值。
  • 几何换算模块:基于三角形相似原理,将检测框高度转换为物理距离。
  • 结果可视化与输出模块:在帧上叠加距离标注,并输出结构化数据。

2. 相机成像模型与焦距标定实操

测距的精度上限,一半取决于相机标定做得好不好。这一节不讲虚的,直接给标定思路和关键参数。

2.1 针孔成像模型的核心关系

在针孔模型下,相机拍摄目标时,物理世界中的目标高度、相机焦距、目标到相机的距离、目标在图像传感器上成像的像素高度,四者满足一个极其简洁的关系:

物理高度 / 像素高度 = 物理距离 / 焦距

这个式子整理之后,就能得到距离的计算公式:

距离 = 目标物理高度 × 焦距 / 目标像素高度

这里的焦距不是镜头包装盒上写的毫米数,而是以像素为单位的等效焦距。这个数值会随相机分辨率变化,所以在实际项目中必须通过标定来确定,不能靠猜,也不能直接用传感器尺寸手工推算,因为镜头实际视角往往和标称值有偏差。

2.2 基于棋盘格的标定流程

棋盘格标定是目前最稳定、最常用的方法,核心原理是让相机从不同角度拍摄一个图案尺寸已知的棋盘格,通过检测角点来求解相机内参和畸变系数。操作步骤如下:

  1. 打印一张A4纸大小的棋盘格,建议使用7x10的角点布局,格边长打印后用游标卡尺测量精确值,别轻信打印设置里的理论值。
  2. 固定好相机,手持棋盘格在相机前变换姿态,拍摄15到20张照片。要覆盖不同距离、不同倾斜角度、画面四个角落,确保棋盘格在每张图里完整可见。
  3. 对每张照片,用OpenCV的cv2.findChessboardCorners提取角点坐标,并配合cv2.cornerSubPix做亚像素级精化。
import cv2 import numpy as np CHECKERBOARD = (7, 10) criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32) objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2) objp = objp * square_size # square_size为实测格边长,单位米 objpoints = [] imgpoints = [] images = [f"calib_{i}.jpg" for i in range(1, 21)] for fname in images: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None) if ret: corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners2) ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) fx = mtx[0, 0] # x轴等效焦距,像素单位 fy = mtx[1, 1] # y轴等效焦距

标定完成后,fx和fy就是换算公式里的焦距。注意在测距公式中,水平方向和垂直方向的像素尺寸通常不同,具体用哪个焦距取决于你测量目标是靠水平宽度还是垂直高度。

2.3 标定实操中的关键注意事项

  • 棋盘格不要与相机平面平行,要故意倾斜一个角度,否则角点匹配会退化,标定结果会虚高。
  • 标定照片要覆盖变焦范围。定焦镜头可以一次标定长期使用,变焦镜头如果改变了焦距,必须重新标定。
  • 畸变校正对远距离测距影响巨大。实测中,未校正畸变的画面边缘误差能到15%以上,靠近画面中心时误差相对小。如果只测画面中心区域目标,可以忽略畸变校正,否则务必对每帧先做cv2.undistort再送入检测模型。

3. 基于YOLO11的检测结果提取与坐标处理

标定拿到焦距后,下一步就是让YOLO11在视频流或图片中输出边界框,并从输出里提取出测距所需的像素高度和类别信息。

3.1 模型加载与推理流程

Ultralytics YOLO11的使用已经非常工程化,几行代码就能跑通。以下代码基于实测稳定版本:

from ultralytics import YOLO model = YOLO("yolo11n.pt") # 可以用n/s/m/l/x不同尺寸 results = model.predict( source="camera_in.mp4", imgsz=640, conf=0.4, stream=True, classes=[0], # 0对应COCO数据集中的person类 verbose=False, )

这里的stream=True很关键,它让模型对视频流逐帧处理,而不是一次性加载整段视频,能显著降低内存占用。imgsz=640是常见的平衡点,尺寸越大精度越高,但推理延迟也随之上升,边缘设备上建议保持640,服务端有GPU可以尝试768或更高。

3.2 提取目标的高度与类别信息

YOLO11的返回对象中,核心信息封装在result.boxes里。手动遍历每个检测框,拿到归一化的坐标和类别:

import cv2 import numpy as np KNOWN_HEIGHTS = { 0: 1.7, # person,单位米 2: 1.5, # car,典型轿车高度 7: 0.6, # truck,按小型货车估算 } results = model.predict(source="camera_in.mp4", stream=True, verbose=False) for result in results: frame = result.orig_img.copy() if result.boxes is None: continue for box in result.boxes: cls = int(box.cls[0]) conf = float(box.conf[0]) xyxy = box.xyxy[0].tolist() y_min, y_max = int(xyxy[1]), int(xyxy[3]) pixel_height = y_max - y_min if cls in KNOWN_HEIGHTS: real_height = KNOWN_HEIGHTS[cls] distance = (real_height * fx) / pixel_height cv2.rectangle(frame, (int(xyxy[0]), y_min), (int(xyxy[2]), y_max), (0, 255, 0), 2) label = f"cls={cls} conf={conf:.2f} dist={distance:.2f}m" cv2.putText(frame, label, (int(xyxy[0]), y_min - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)

这段代码会把每个目标的类别、置信度和计算出的距离实时标注在对应框上方。实测下来,在普通笔记本CPU上,yolo11n模型配合640输入分辨率,帧率能稳定在25FPS以上,完全能满足实时预览。

3.3 为什么不直接用边界框的宽度

很多人问,为什么不用边界框的宽度算距离,宽度不是也能反映目标大小吗?核心原因是:对于直立的人、车辆这类目标,检测框的高度与目标实际高度之间存在相对稳定的对应关系,受目标姿态变化影响较小。而宽度方向,一个侧身的人和一个正面的人,边界框宽度差异极大,直接算距离误差会非常离谱。用高度做换算,本质上是因为高度先验更稳定。

3.4 检测框抖动处理

YOLO11单帧检测会受运动模糊、目标姿态变化、遮挡等因素影响,导致检测框高度在相邻帧之间抖动,进而让测距结果忽远忽近。处理手段不是改模型,而是在后处理环节引入平滑算法。我实测最有效的是简单的一阶低通滤波:

smoothed_distance = alpha * current_distance + (1 - alpha) * previous_distance

alpha取0.3到0.5之间比较合适。太小会让反应变迟钝,目标急停时距离变化显示有拖影;太又完全滤不掉抖动。另外,人的行走速度大约在每秒1.2到1.5米,可以根据这个参考值做目标跟踪时的速度合理性校验,连续多帧距离跳变超过这个范围就可以判定为检测异常。

4. 距离公式的推导与误差边界分析

看到这里,公式已经用起来了,但你还是要知道这个公式到底在算什么,误差从哪里来,这样在结果异常时才知道该往哪个方向排查。

4.1 三角形相似的完整推导

设相机光心为O,感光元件平面在焦距f处,目标顶部与底部分别为A和B,对应成像点A'和B'。则三角形OAB与三角形OA'B'相似,于是:

AB / A'B' = OA / OA'

其中AB是目标物理高度H,A'B'是成像像素高度h(经过畸变校正后),OA是目标到光心的实际距离D(近似为水平距离),OA'等于焦距f。于是:

D = H × f / h

这个推导成立的前提是目标底部与成像平面近似的垂直关系,目标必须直立且相机光轴与地面平行。如果相机俯仰角度过大,底部落点与目标实际垂直脚点发生偏移,公式就会系统性地偏向某一侧。

4.2 误差来源的定量分析

实际误差来自三个主要方面:

目标物理高度先验误差

如果把人高先验设为1.7米,而检测对象实际身高是1.9米,那么求出的距离会系统性偏大约11.8%。在实际场景中,固定先验值不可避免会引入误差,这是单目测距的天然局限。

检测框像素高度的量化误差

图像在y方向的分辨率越高,相同物理距离对应的像素高度变化越敏感。以1080P视频为例,一个60米外的人,像素高度可能只有几十像素,而此时检测框上下边缘差1个像素,距离计算值就会变化大约2%。

非线性的透视失真

相机边缘区域畸变显著,即使校正后依然存在残差。当目标位于画面边缘时,误差比中心区域高出很多。实测结论是:目标靠近画面中心,误差约5%到8%;目标移到画面边缘,误差可飙升至15%以上。

4.3 影响距离计算精度的核心参数

焦距f对测距结果的影响是线性的。焦距标定误差5%,测距结果就会跟着偏差5%。而像素高度h对测距结果的影响是非线性的,目标越远、像素高度越小,同样的检测框误差导致的距离误差越大。所以在远距离场景下,提高网络输入分辨率比优化标定参数更有性价比。

5. 实操过程:从YOLO11检测到距离输出的完整代码

前面是理论与模块拆解,这里给一套可以直接运行的完整实现。环境依赖很简单,主要就是Ultralytics、OpenCV和NumPy。

5.1 完整代码框架

import cv2 import numpy as np from collections import deque from ultralytics import YOLO # 焦距通过相机标定获得,这里是示例值 FX = 980.0 # 目标类别对应的物理高度,单位为米 KNOWN_HEIGHTS = { 0: 1.7, # person 2: 1.5, # car } # 距离平滑缓存 dist_history = deque(maxlen=3) def calculate_distance(cls, pixel_height): real_height = KNOWN_HEIGHTS.get(cls) if real_height is None: return None return (real_height * FX) / pixel_height def smooth_distance(raw_dist): if abs(raw_dist - dist_history[-1]) > 2.0: dist_history.clear() dist_history.append(raw_dist) return float(np.mean(dist_history)) def main(): model = YOLO("yolo11n.pt") cap = cv2.VideoCapture(0) # 或本地视频文件路径 while cap.isOpened(): ret, frame = cap.read() if not ret: break # 去畸变处理,需要传入标定得到的dist系数 # frame = cv2.undistort(frame, mtx, dist) results = model.predict(frame, imgsz=640, conf=0.4, verbose=False) for result in results: if result.boxes is None: continue for box in result.boxes: cls = int(box.cls[0]) conf = float(box.conf[0]) x1, y1, x2, y2 = box.xyxy[0].tolist() pixel_height = y2 - y1 raw_dist = calculate_distance(cls, pixel_height) if raw_dist is None: continue final_dist = smooth_distance(raw_dist) cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) info = f"{model.names[cls]} {conf:.2f} D={final_dist:.1f}m" cv2.putText(frame, info, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow("YOLO11 Distance", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows() if __name__ == "__main__": main()

5.2 参数调整建议

  • conf建议不低于0.3,低于这个值,检测框抖动会很严重,测距结果也会跟着剧烈跳动。要识别小目标时,可以适当降到0.25,但要做好误差增大的心理准备。
  • 输入分辨率imgsz设640时,人体最小可靠检测距离约40米,超过这个距离,检测框本身就开始不稳定。需要更远测距时,把输入分辨率升到1280,代价是推理时间增加大约2.5倍。
  • 距离平滑的deque长度设为3即可,太大会造成测距延迟感明显,目标快速移动时距离显示明显滞后。

5.3 部署时的视频流接入建议

这套系统不只是能处理本地摄像头,实际项目里更常见的是接入RTSP网络摄像头。只需要把cv2.VideoCapture(0)替换成cv2.VideoCapture("rtsp://your_stream_address")。注意网络摄像头存在断流重连问题,稳妥的做法是在主循环里检测cap.isOpened(),为False时自动重建连接,避免程序跑一会儿就死掉。

6. 常见问题与排查技巧实录

这段时间我前后泡在这个项目里踩了不少坑,整理下来大概有五大类,基本覆盖了新手会遇到的所有问题。

6.1 距离数值不稳定,前后帧跳动剧烈

这个问题最常出现。检查顺序依次是:检测框是否抖动(可以加跟踪或平滑)、目标是否在画面边缘(往中心靠)、当前帧是否出现运动模糊(降低快门或提高帧率)。最终手段是加低通滤波,把短时跳变削平。

6.2 计算距离整体偏大或偏小

如果误差方向一致、幅度稳定,基本是焦距标定不准,或者目标物理高度先验和实际不符。排查方法是找一个已知距离的位置放置标定物,反推当前焦距的实际有效值,然后调整代码中的FX常数。千万别改检测阈值,改阈值只会让框变多或变少,不会修正系统性距离偏差。

6.3 目标在画面边缘时距离误差明显变大

这是畸变未有效校正的典型症状。解决方法是确保送入模型前对每帧执行cv2.undistort。实测1080P图像全帧去畸变,单帧耗时约2到3毫秒,对整体帧率影响不大,可以放心开启。

6.4 多目标场景下距离标记串位

当画面中有多个人重叠时,YOLO11可能会把多个目标合并在一个框里,导致输出的像素高度比真实高度大,距离就被算近了。这种情况没有完美的检测后处理能拯救,较优的策略是加载精度更高的模型(yolo11m或yolo11l),并调高conf过滤置信度低的重叠框。要对同一行人做稳定测距,建议在检测之上再套一层ByteTrack等多目标跟踪,让每个目标的距离信息随时间关联起来。

6.5 标定时角点检测失败

通常是棋盘格图片边缘被裁切、光照不均匀导致对比度太低,或者棋盘格反光严重。解决办法是保证棋盘格完整出现在画面中,适当调整曝光补偿,避免强光直射打印纸。另外别用太厚的塑封膜,反光会让角点响应质量骤减。

7. 进阶优化:像素高度修正与参考物等宽法

基础项目跑通之后,想让精度再上一个台阶,可以考虑下面几个优化方向,改动量都不大,但收益明显。

7.1 基于检测框底边位置的修正

一个容易被忽略的事实是:当目标在画面中的位置不同时,同样的像素高度对应的物理距离并不相同。严格修正需要用到标定得到的旋转矩阵和平移向量,将检测框底边中心点的像素坐标反投影到地面平面。这个方法改动复杂一些,但在固定相机场景下能做得很准。简化版做法:先离线采集不同画面位置与实测距离的对应关系,拟合一个修正系数,再乘到原始距离上。这个方法是我实测性价比最高的精度提升手段。

7.2 参考物等宽测距法

当目标物理高度不可靠或类别太多时,可以换一种思路:在场景中放置一个物理尺寸已知的参考标志物,用“参考物在画面里的像素宽度 对比 目标的像素宽度”的比例关系,配合两者与相机的三角关系推导距离。这种方法特别适合固定机位的施工区域或停车场入口,精度受目标姿态影响较小。

7.3 多帧置信度融合

对静态或低速目标,可以收集连续N帧的距离计算结果,剔除明显离群值后取中位数。这个方法特别适合闸机、安防门禁这类目标停留时间长的场景,能够把测距误差稳在3%以内。实时性要求高的场景慎用,延迟会叠加。

我个人在实际项目中的感受是,视觉测距做到最后,拼的不是模型多强,而是对误差来源的理解和对场景约束的把握。YOLO11负责把目标稳定找出来,剩下的精度全靠相机标定、几何修正和工程细节堆出来。这个项目后续如果要扩展,可以考虑往端侧设备迁移,把模型导出为ONNX再转TensorRT或OpenVINO格式,在Jetson系列设备和工业相机方案上,同样的逻辑可以跑出接近实时的多路测距能力。踩过几次坑之后最大的体会是:任何看似高大上的视觉算法,落地时都要回归到“标定准不准、先验对不对、场景稳不稳”这三件事上来。

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

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

立即咨询