最近被提出了一个任务,需要研究视觉测距方案。具体地,需要为算力不算很高的 RK3588 开发板,和没有原生硬件方案双目或红外摄像头(仅安装了1280×720单目摄像头)的小车,制作一个搭配电机驱动使用的简易障碍物识别程序。
该任务有明显的特征:其一,端侧开发板算力无法媲美 x86 主机,需要寻找一个 4~8 TOPS 下也能顺利运行的算法;其二,只使用单目摄像机,不能直接像双目或红外拉个 SDK 获取硬件传来的准确数据;其三,避障要求的精确度不是特别高(猜测的1.5m以内就应当到避让阈值)。
在与大肥鱼和吉蜜狸斗智斗勇后,找到了一个比较前沿的纯视觉单目测距方法——Ultralytics YOLO26-depth。
感兴趣可以直接开始阅读文档👉使用 Ultralytics YOLO 进行单目深度估计 https://docs.ultralytics.com/zh/tasks/depth

快速演示
注:我在 Windows 11 跑这个程序时遇到过麻烦,如果 OpenCV 打开不了你的电脑摄像头:
- Windows Defender 关闭智能应用控制(Python:此应用的一部分已被阻止,关闭Windows安全中心选项即可 – 氧化小屋)
- 卸载并重新编译 OpenCV Contrib
- 使用 Conda 全局环境,不要使用项目级 .venv
模型文件 yolo26n.pt 和 yolo26n-depth.pt 需要在代码同级目录。首次运行 Yolo-depth 会自动下载权重。也可查阅 Releases · ultralytics/assets https://github.com/ultralytics/assets/releases
requirements.txt
torch
torchvision
ultralytics>=8.4.104
opencv-python
main.py
import argparse
import sys
import time
import cv2
import numpy as np
from ultralytics import YOLO
from ultralytics.utils.plotting import colorize_depth
# 管道/重定向环境下 Windows 默认 GBK 编码会因中文打印崩溃, 统一用 UTF-8
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
# 默认测距目标类别(COCO 类别名),可用 --classes 覆盖
DEFAULT_CLASSES = {"person", "car", "bus", "truck", "bicycle", "motorcycle"}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="YOLO26-Depth 单目摄像头实时测距")
parser.add_argument("--camera", type=int, default=0, help="摄像头索引 (默认 0)")
parser.add_argument("--det-model", default="yolo26n.pt", help="检测模型路径 (默认 yolo26n.pt)")
parser.add_argument("--depth-model", default="yolo26n-depth.pt", help="深度模型路径 (默认 yolo26n-depth.pt)")
parser.add_argument("--imgsz", type=int, default=512, help="推理分辨率 (默认 512; 768 更准更慢, 384 更快)")
parser.add_argument("--conf", type=float, default=0.3, help="检测置信度阈值 (默认 0.3)")
parser.add_argument("--classes", nargs="+", default=None,
help="测距目标类别名列表, 如: person car (默认 person 及常见车辆)")
parser.add_argument("--vmin", type=float, default=0.0, help="深度着色下限/米 (默认 0)")
parser.add_argument("--vmax", type=float, default=20.0, help="深度着色上限/米 (默认 20)")
parser.add_argument("--max-frames", type=int, default=0, help="跑 N 帧后自动退出 (0=无限, 用于自动化验证)")
return parser.parse_args()
def open_camera(index: int) -> cv2.VideoCapture:
"""打开摄像头, 默认后端失败时回退 Windows DSHOW 后端。"""
cap = cv2.VideoCapture(index)
if not cap.isOpened():
cap.release()
cap = cv2.VideoCapture(index, cv2.CAP_DSHOW)
return cap
def median_distance(depth: np.ndarray, box_xyxy) -> float:
"""取目标框中心 50%x50% 区域内有效深度的中位数作为距离(米)。
用中心区域而非整框, 避免框边缘混入的背景深度污染。
"""
x1, y1, x2, y2 = box_xyxy
h, w = depth.shape
x1, x2 = max(0, x1), min(w, x2)
y1, y2 = max(0, y1), min(h, y2)
cx1, cx2 = int(x1 + (x2 - x1) * 0.25), int(x1 + (x2 - x1) * 0.75)
cy1, cy2 = int(y1 + (y2 - y1) * 0.25), int(y1 + (y2 - y1) * 0.75)
roi = depth[cy1:cy2, cx1:cx2]
values = roi[(roi > 0) & np.isfinite(roi)]
return float(np.median(values)) if values.size else float("nan")
def on_mouse(event, x, y, flags, param) -> None:
"""左键点击: 记录点击位置(画面坐标系), 主循环在下帧查询该点深度并标注。
"""
if event == cv2.EVENT_LBUTTONDOWN and 0 <= x < 2 * param["w"]:
param["pt"] = (x % param["w"], y)
def main() -> None:
args = parse_args()
det_model = YOLO(args.det_model)
depth_model = YOLO(args.depth_model)
# 类别名 -> 类别索引(用于推理时过滤)
wanted = set(args.classes) if args.classes else DEFAULT_CLASSES
known = set(det_model.names.values())
unknown = wanted - known
if unknown:
print(f"警告: 模型不认识这些类别, 已忽略: {sorted(unknown)}")
class_ids = [i for i, name in det_model.names.items() if name in wanted]
cap = open_camera(args.camera)
if not cap.isOpened():
raise SystemExit(f"错误: 无法打开摄像头索引 {args.camera}")
frame_count = 0
fps = 0.0
# 鼠标点击查询状态: {"w": 单幅画面宽, "pt": 最近一次点击坐标(画面坐标系)}
click = {"w": 0, "pt": None}
# OpenCV 5 的 Windows 后端会破坏非 ASCII 窗口标题, 标题须用纯 ASCII
window_name = "YOLO26-Depth Ranging (q quit | left-click query distance)"
# OpenCV 5 需在首个 imshow 之前显式建窗, 才能注册鼠标回调
cv2.namedWindow(window_name)
cv2.setMouseCallback(window_name, on_mouse, click)
print(f"检测模型: {args.det_model} | 深度模型: {args.depth_model} | imgsz={args.imgsz}")
print("按 q 退出, 鼠标左键点击画面任意位置查询该点距离")
try:
while True:
ok, frame = cap.read()
if not ok:
print("警告: 读取摄像头画面失败, 程序退出")
break
t0 = time.perf_counter()
# 1) 目标检测
dets = det_model.predict(frame, imgsz=args.imgsz, conf=args.conf,
classes=class_ids, device="cpu", verbose=False)[0]
# 2) 深度估计 (米制, (H, W) float32)
depth = depth_model.predict(frame, imgsz=args.imgsz, device="cpu", verbose=False)[0].depth.data
depth = depth.cpu().numpy()
h, w = frame.shape[:2]
if depth.shape != (h, w): # 深度图为模型处理分辨率, 对齐到原图
depth = cv2.resize(depth, (w, h), interpolation=cv2.INTER_LINEAR)
# 3) 目标框测距 + 标注
annot = frame.copy()
if dets.boxes is not None and len(dets.boxes):
for box in dets.boxes:
x1, y1, x2, y2 = box.xyxy[0].round().int().tolist()
cls_id = int(box.cls[0])
conf = float(box.conf[0])
dist = median_distance(depth, (x1, y1, x2, y2))
dist_text = f"{dist:.2f} m" if np.isfinite(dist) else "-- m"
label = f"{det_model.names[cls_id]} {conf:.2f} | {dist_text}"
cv2.rectangle(annot, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.putText(annot, label, (x1, max(y1 - 8, 16)),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 0), 2)
# 4) 彩色深度图 (固定范围, 颜色跨帧稳定)
depth_vis = colorize_depth(depth, cmap="inferno", mode="metric",
vmin=args.vmin, vmax=args.vmax)
# 4.5) 鼠标点击查询距离标注 (左半/右半/深度图都画, 用最新深度图取值)
if click["pt"] is not None:
px, py = click["pt"]
val = depth[py, px]
info = (f"({px},{py}) {val:.2f} m" if np.isfinite(val) and val > 0
else f"({px},{py}) -- m")
for canvas, dx in ((annot, 0), (annot, w), (depth_vis, 0)):
cv2.drawMarker(canvas, (dx + px, py), (255, 255, 0),
cv2.MARKER_CROSS, 24, 2)
cv2.putText(canvas, info, (dx + px + 14, max(py - 14, 16)),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 0), 2)
# 5) 左右并排显示
dt = time.perf_counter() - t0
fps = 0.9 * fps + 0.1 / max(dt, 1e-6)
cv2.putText(annot, f"FPS: {fps:4.1f}", (10, 28),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
combined = np.hstack([annot, depth_vis])
cv2.imshow(window_name, combined)
click["w"] = w # 供鼠标回调做坐标映射
frame_count += 1
if cv2.waitKey(1) & 0xFF == ord("q"):
break
if args.max_frames and frame_count >= args.max_frames:
break
finally:
cap.release()
cv2.destroyAllWindows()
print(f"已退出, 共处理 {frame_count} 帧")
if __name__ == "__main__":
main()
启动参数:
--imgsz:推理分辨率,测试时我选用768
--classes:框定分类,例如 person
--conf:框定置信度阈值,测试时我选用0.4
--camera:摄像头索引






