Ultralytics YOLO26-depth:向廉价、低算力、只买得起单目摄像头的机器人提供的测距新选择

最近被提出了一个任务,需要研究视觉测距方案。具体地,需要为算力不算很高的 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 打开不了你的电脑摄像头:

模型文件 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:摄像头索引

本作品(《Ultralytics YOLO26-depth:向廉价、低算力、只买得起单目摄像头的机器人提供的测距新选择》)采用知识共享署名 4.0 国际许可协议进行许可。转载需注明来源(作者 氧化某人,原文链接 https://www.yang1120.com/archives/785)。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇