ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Ultralytics YOLO11单目视觉测距实战:从相机标定到距离计算完整链路

Ultralytics YOLO11单目视觉测距实战:从相机标定到距离计算完整链路 做视觉测距这个需求十个里有八个是从“检测到框”开始到“算出距离”结束。YOLO系列模型发展到Ultralytics YOLO11检测精度和推理速度已经非常成熟但很多人把模型跑通了才发现框出来容易距离难算。这篇博文就围绕Ultralytics YOLO11做距离计算这件事把像素坐标到物理距离的完整链路拆开讲清楚从相机成像原理、焦距标定到检测结果换算、误差排查给你一条可以直接落地的实现路径。1. 距离计算的整体思路与方案选型先说结论YOLO11本身只负责“在图像里找到目标并画出边界框”它输出的是像素坐标不包含任何深度信息。想从二维像素算出三维物理距离必须在模型之外再搭一套几何换算逻辑整套系统才算闭环。1.1 为什么不能直接使用检测框的像素大小算距离很多人第一反应是“物体在画面里越小距离越远”然后试图用一个线性函数拟合边框高度和距离的关系。这个思路在固定场景、固定相机角度下勉强能用换到别的场景就彻底失效。根本原因在于相机成像是一个透视投影过程物体在图像上的大小与距离之间是倒数关系不是线性关系。距离10米和距离20米时物体在画面里缩小的幅度完全不同。真正可靠的做法是走针孔相机模型加相似三角形换算。这个方案的前提是你知道目标的实际物理尺寸比如人的平均身高、车牌的宽度然后利用相机焦距这个内部参数把像素高度换算成物理距离。1.2 三种主流测距方案的取舍方案一单目视觉测距本项目采用的核心方案只用一个普通摄像头借助几何模型和已知尺寸先验来估算距离。优点是成本低、部署简单、算力要求小适合大部分安防和工业场景。缺点是精度依赖目标尺寸先验的准确性且无法应对目标遮挡。方案二双目立体视觉测距通过两个相机拍摄同一场景利用左右视图的视差计算深度。精度高不受目标尺寸先验限制但需要严格的相机同步和标定流程硬件成本和计算开销都翻倍。适合对精度要求极高、场景可控的室内环境。方案三深度相机直接测距用深度传感器直接获取每个像素的深度值原理最简单精度也稳定。但深度相机普遍视场角有限户外强光下性能急剧下降设备成本也高于普通网络摄像头。综合来看单目方案在“快速落地验证 中远距离估算”这个需求上性价比最高。本项目选它不是因为它能做到毫米级精度而是因为它能在普通摄像头和YOLO11的基础上用最小的改动换来可用的距离估计能力。1.3 完整技术链路整个系统分为四个模块串起来就是一条完整链路目标检测模块基于Ultralytics YOLO11加载预训练权重对视频帧执行推理输出边界框、类别和置信度。相机标定模块计算相机内参矩阵和畸变系数核心是获取焦距像素值。几何换算模块基于三角形相似原理将检测框高度转换为物理距离。结果可视化与输出模块在帧上叠加距离标注并输出结构化数据。2. 相机成像模型与焦距标定实操测距的精度上限一半取决于相机标定做得好不好。这一节不讲虚的直接给标定思路和关键参数。2.1 针孔成像模型的核心关系在针孔模型下相机拍摄目标时物理世界中的目标高度、相机焦距、目标到相机的距离、目标在图像传感器上成像的像素高度四者满足一个极其简洁的关系物理高度 / 像素高度 物理距离 / 焦距这个式子整理之后就能得到距离的计算公式距离 目标物理高度 × 焦距 / 目标像素高度这里的焦距不是镜头包装盒上写的毫米数而是以像素为单位的等效焦距。这个数值会随相机分辨率变化所以在实际项目中必须通过标定来确定不能靠猜也不能直接用传感器尺寸手工推算因为镜头实际视角往往和标称值有偏差。2.2 基于棋盘格的标定流程棋盘格标定是目前最稳定、最常用的方法核心原理是让相机从不同角度拍摄一个图案尺寸已知的棋盘格通过检测角点来求解相机内参和畸变系数。操作步骤如下打印一张A4纸大小的棋盘格建议使用7x10的角点布局格边长打印后用游标卡尺测量精确值别轻信打印设置里的理论值。固定好相机手持棋盘格在相机前变换姿态拍摄15到20张照片。要覆盖不同距离、不同倾斜角度、画面四个角落确保棋盘格在每张图里完整可见。对每张照片用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 [fcalib_{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( sourcecamera_in.mp4, imgsz640, conf0.4, streamTrue, classes[0], # 0对应COCO数据集中的person类 verboseFalse, )这里的streamTrue很关键它让模型对视频流逐帧处理而不是一次性加载整段视频能显著降低内存占用。imgsz640是常见的平衡点尺寸越大精度越高但推理延迟也随之上升边缘设备上建议保持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(sourcecamera_in.mp4, streamTrue, verboseFalse) 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 fcls{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_distancealpha取0.3到0.5之间比较合适。太小会让反应变迟钝目标急停时距离变化显示有拖影太又完全滤不掉抖动。另外人的行走速度大约在每秒1.2到1.5米可以根据这个参考值做目标跟踪时的速度合理性校验连续多帧距离跳变超过这个范围就可以判定为检测异常。4. 距离公式的推导与误差边界分析看到这里公式已经用起来了但你还是要知道这个公式到底在算什么误差从哪里来这样在结果异常时才知道该往哪个方向排查。4.1 三角形相似的完整推导设相机光心为O感光元件平面在焦距f处目标顶部与底部分别为A和B对应成像点A和B。则三角形OAB与三角形OAB相似于是AB / AB OA / OA其中AB是目标物理高度HAB是成像像素高度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(maxlen3) 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, imgsz640, conf0.4, verboseFalse) 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系列设备和工业相机方案上同样的逻辑可以跑出接近实时的多路测距能力。踩过几次坑之后最大的体会是任何看似高大上的视觉算法落地时都要回归到“标定准不准、先验对不对、场景稳不稳”这三件事上来。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表