在计算机视觉项目中我们常常需要同时理解场景中的“是什么”和“在哪里”。目标检测YOLO告诉我们物体是什么、在哪里而单目深度估计则告诉我们物体离我们有多远。将两者结合就能从一张普通的RGB图片中构建出带有距离信息的3D感知世界。最近在尝试将YOLOv8的目标检测能力与Depth Anything V2的单目深度估计模型进行集成测试过程中遇到了环境配置、模型融合、结果可视化等一系列工程化问题。本文将完整复盘这次“YOLO-DEPTH”单目深度测试的全过程从核心概念、环境搭建、代码实现到结果分析与优化手把手带你实现一个可运行的联合推断Demo无论是想了解多任务学习还是需要在机器人、自动驾驶等领域进行技术预研本文都能提供直接的参考。1. 背景与核心概念为什么需要YOLO深度估计在开始动手之前我们有必要厘清几个关键概念以及它们组合在一起能解决什么问题。1.1 目标检测YOLOYOLOYou Only Look Once是一种单阶段目标检测算法其核心思想是将输入图像划分为网格每个网格负责预测边界框和类别概率。YOLOv8作为当前流行的版本在速度与精度上取得了很好的平衡它不仅能做目标检测还扩展了实例分割、姿态估计等任务。简单来说YOLO的输出是图像中每个物体的“类别标签”和“二维包围框Bounding Box”。1.2 单目深度估计深度估计旨在获取图像中每个像素点到相机的距离。单目深度估计特指仅使用一张RGB图像来估计深度这是一个不适定问题ill-posed problem因为从2D图像反推3D信息本身具有多义性。近年来基于大规模数据训练的深度学习模型如MiDaS、Depth Anything在此任务上取得了惊人进展。Depth Anything V2是当前SOTA模型之一它能够从单张图像预测出相对稠密且准确的深度图。1.3 YOLO-DEPTH联合任务的价值单独的目标检测结果缺乏空间尺度信息。例如检测到一个“人”但我们不知道这个人距离相机是1米还是10米。单独的深度图提供了距离信息但没有语义标签。将两者结合我们可以得到带距离的目标检测不仅知道有什么物体还知道它有多远。简单的3D包围框结合2D检测框和框内像素的平均深度可以粗略估计物体在3D空间中的位置和大小。场景理解为后续的路径规划、避障、SLAM同步定位与地图构建等高级任务提供丰富的感知输入。 这种技术组合在自动驾驶、无人机导航、AR/VR、机器人抓取等领域有广泛的应用前景。2. 环境准备与版本说明本次实战基于Python主要依赖PyTorch深度学习框架。为了保证复现性以下是经过测试的环境配置。2.1 基础环境操作系统Ubuntu 20.04 / Windows 10/11 (WSL2推荐) 或 macOSPython版本3.8 - 3.10推荐3.9包管理工具pip 或 conda2.2 核心依赖库及版本创建一个新的虚拟环境是良好的实践。以下是requirements.txt文件内容# 深度学习框架 torch1.12.0, 2.0.1 torchvision0.13.0, 0.15.2 # YOLOv8 (Ultralytics 官方库) ultralytics8.0.196 # Depth Anything V2 (从官方仓库安装) # 需要提前安装一些依赖 opencv-python4.5.0 matplotlib3.3.0 scipy1.5.0 timm0.9.2 # 注意版本Depth Anything V2 依赖特定版本 # 其他工具 numpy1.19.0 Pillow9.0.0 requests2.25.02.3 安装步骤创建并激活虚拟环境以conda为例conda create -n yolo_depth python3.9 conda activate yolo_depth安装PyTorch请根据你的CUDA版本前往 PyTorch官网 获取安装命令。例如对于CUDA 11.8pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118安装其他依赖pip install ultralytics opencv-python matplotlib scipy Pillow requests pip install timm0.9.2克隆并安装Depth Anything V2git clone https://github.com/LiheYoung/Depth-Anything-V2.git cd Depth-Anything-V2 pip install -e . cd ..-e参数代表以可编辑模式安装方便后续查看源码。3. 核心原理与模型拆解在编写代码前理解两个模型如何协同工作至关重要。3.1 YOLOv8 推理流程YOLOv8的推理接口非常简洁。加载预训练模型如yolov8n.pt后输入一张图像它会返回一个结果对象其中包含了boxes检测框的坐标xyxy格式即左上右下。confidences每个检测框的置信度。class_ids每个检测框对应的类别ID。names类别ID到类别名称的映射字典。 我们需要提取这些信息并为每个检测框计算一个代表性的深度值。3.2 Depth Anything V2 推理流程Depth Anything V2模型接收一张图像输出一个与输入同宽高的深度图Depth Map。深度图中的每个像素值代表该点的“相对深度”或经过尺度缩放后的“视差”值越小通常表示越远值越大表示越近。需要注意的是单目深度估计输出的是相对深度而非绝对的米制单位。要得到绝对距离通常需要额外的传感器如IMU、激光雷达或已知的相机内参进行标定。3.3 融合策略如何为检测框分配深度这是本项目的关键。一个直观且常用的策略是运行YOLOv8得到N个物体的检测框[x1, y1, x2, y2]。运行Depth Anything V2得到整张图的深度图depth_map(H, W)。对于第i个检测框在深度图上裁剪出对应的矩形区域depth_map[y1:y2, x1:x2]。对该区域内的深度值进行统计如取中位数或平均值作为该物体的代表深度。取中位数对深度噪声和异常值如背景误入更鲁棒。取平均值计算简单但在物体内部深度变化大或包含背景时可能不准。可选根据相机内参和深度值将2D框反投影到3D空间估算物体的3D位置。4. 完整实战构建YOLO-DEPTH联合推断Pipeline我们将创建一个完整的Python脚本实现图像读取、模型加载、联合推理、结果可视化全流程。4.1 项目结构建议按如下方式组织代码和资源yolo_depth_test/ ├── main.py # 主程序 ├── utils.py # 工具函数可视化等 ├── requirements.txt # 依赖列表 ├── input_images/ # 存放测试图片 │ └── demo.jpg ├── output_results/ # 存放输出结果 └── weights/ # (可选)存放下载的模型权重4.2 编写核心工具函数 (utils.py)这个文件负责可视化将检测框和深度信息叠加显示。# utils.py import cv2 import numpy as np import matplotlib.pyplot as plt from matplotlib import cm def apply_color_map(depth_map, colormapcv2.COLORMAP_INFERNO): 将单通道深度图转换为彩色热力图便于可视化。 Args: depth_map (np.ndarray): 深度图形状 (H, W)值域通常为[0, 1]或已归一化。 colormap: OpenCV 色图。 Returns: colored_depth (np.ndarray): 彩色深度图形状 (H, W, 3)值域[0, 255]。 # 归一化到0-255 if depth_map.dtype ! np.uint8: depth_normalized cv2.normalize(depth_map, None, 0, 255, cv2.NORM_MINMAX) depth_uint8 np.uint8(depth_normalized) else: depth_uint8 depth_map colored cv2.applyColorMap(depth_uint8, colormap) return colored def plot_results(original_img, detections, depth_map, save_pathNone): 绘制并排对比图原图检测框以及彩色深度图。 Args: original_img (np.ndarray): 原始BGR图像。 detections (list): 每个元素为字典包含‘bbox’, ‘cls_id’, ‘conf’, ‘depth’。 depth_map (np.ndarray): 深度图。 save_path (str): 结果保存路径。 img_with_boxes original_img.copy() h, w img_with_boxes.shape[:2] # 1. 在原图上绘制检测框和深度信息 for det in detections: x1, y1, x2, y2 det[bbox] cls_id det[cls_id] conf det[conf] depth det[depth] # 画矩形框 color (0, 255, 0) # 绿色 cv2.rectangle(img_with_boxes, (x1, y1), (x2, y2), color, 2) # 准备标签文本 label f{cls_id}:{conf:.2f}, d:{depth:.2f} # 计算文本大小 (text_width, text_height), baseline cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1) # 绘制文本背景 cv2.rectangle(img_with_boxes, (x1, y1 - text_height - baseline), (x1 text_width, y1), color, -1) # 绘制文本 cv2.putText(img_with_boxes, label, (x1, y1 - baseline), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) # 2. 生成彩色深度图 colored_depth apply_color_map(depth_map) # 3. 创建画布并排显示 fig, axes plt.subplots(1, 2, figsize(15, 7)) axes[0].imshow(cv2.cvtColor(img_with_boxes, cv2.COLOR_BGR2RGB)) axes[0].set_title(YOLOv8 Detection with Depth) axes[0].axis(off) axes[1].imshow(cv2.cvtColor(colored_depth, cv2.COLOR_BGR2RGB)) axes[1].set_title(Depth Anything V2 Output) axes[1].axis(off) plt.tight_layout() if save_path: plt.savefig(save_path, dpi150, bbox_inchestight) print(f结果已保存至: {save_path}) plt.show() def calculate_box_depth(depth_map, bbox): 计算一个检测框内的代表性深度值使用中位数。 Args: depth_map (np.ndarray): 深度图形状 (H, W)。 bbox (list/tuple): [x1, y1, x2, y2]。 Returns: median_depth (float): 框内深度中位数。 x1, y1, x2, y2 map(int, bbox) # 确保坐标在图像范围内 h, w depth_map.shape x1, x2 max(0, x1), min(w, x2) y1, y2 max(0, y1), min(h, y2) if x2 x1 or y2 y1: return 0.0 # 无效框 box_region depth_map[y1:y2, x1:x2] if box_region.size 0: return 0.0 median_depth np.median(box_region) return median_depth4.3 编写主程序 (main.py)这是联合推理的核心。# main.py import cv2 import torch import numpy as np from ultralytics import YOLO from depth_anything_v2.dpt import DepthAnythingV2 from utils import plot_results, calculate_box_depth import warnings warnings.filterwarnings(ignore) class YOLODepthTester: def __init__(self, yolo_model_pathyolov8n.pt, devicecuda if torch.cuda.is_available() else cpu): 初始化YOLO和Depth Anything V2模型。 Args: yolo_model_path (str): YOLOv8模型权重路径可以是本地文件或官方模型名如‘yolov8n.pt’。 device (str): 运行设备‘cuda’ 或 ‘cpu’。 self.device device print(f使用设备: {device}) # 1. 加载YOLOv8模型 print(正在加载YOLOv8模型...) self.yolo_model YOLO(yolo_model_path) self.yolo_model.to(device) # 预热模型可选 _ self.yolo_model.predict(np.zeros((640, 640, 3), dtypenp.uint8), verboseFalse) # 2. 加载Depth Anything V2模型 print(正在加载Depth Anything V2模型...) # 这里使用‘vits’小模型平衡速度和精度。可选 ‘vitb’, ‘vitl’ encoder vits self.depth_model DepthAnythingV2(encoderencoder, features64, out_channels[48, 96, 192, 384]) # 加载预训练权重 checkpoint torch.load(fcheckpoints/depth_anything_v2_{encoder}.pth, map_locationdevice) self.depth_model.load_state_dict(checkpoint[model]) self.depth_model.to(device) self.depth_model.eval() print(模型加载完毕。) def preprocess_depth_image(self, image): Depth Anything V2 的预处理 from torchvision import transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) return transform(image).unsqueeze(0).to(self.device) def predict(self, image_path, conf_threshold0.25): 对单张图片进行联合预测。 Args: image_path (str): 输入图片路径。 conf_threshold (float): YOLO检测置信度阈值。 Returns: original_img (np.ndarray): 原始图像。 detections (list): 检测结果列表。 depth_map (np.ndarray): 深度图。 # 读取图像 original_img cv2.imread(image_path) if original_img is None: raise FileNotFoundError(f无法读取图像: {image_path}) img_rgb cv2.cvtColor(original_img, cv2.COLOR_BGR2RGB) h, w original_img.shape[:2] # --- Step 1: YOLOv8 目标检测 --- print(正在进行YOLOv8目标检测...) yolo_results self.yolo_model.predict(sourceoriginal_img, confconf_threshold, verboseFalse)[0] boxes_xyxy yolo_results.boxes.xyxy.cpu().numpy() # [N, 4] confidences yolo_results.boxes.conf.cpu().numpy() # [N,] class_ids yolo_results.boxes.cls.cpu().numpy().astype(int) # [N,] class_names yolo_results.names # 类别字典 # --- Step 2: Depth Anything V2 深度估计 --- print(正在进行深度估计...) # 预处理 input_tensor self.preprocess_depth_image(img_rgb) with torch.no_grad(): depth_pred self.depth_model(input_tensor) # 后处理调整尺寸并转到CPU depth_pred torch.nn.functional.interpolate(depth_pred, (h, w), modebilinear, align_cornersFalse) depth_map depth_pred.squeeze().cpu().numpy() # [H, W] # 深度图归一化到[0, 1]以便可视化 depth_map_normalized (depth_map - depth_map.min()) / (depth_map.max() - depth_map.min() 1e-8) # --- Step 3: 融合结果为每个检测框计算深度 --- print(正在融合检测与深度信息...) detections [] for i in range(len(boxes_xyxy)): bbox boxes_xyxy[i] cls_id class_ids[i] conf confidences[i] cls_name class_names[cls_id] # 计算该框的代表深度使用中位数 median_depth calculate_box_depth(depth_map_normalized, bbox) detections.append({ bbox: bbox.tolist(), cls_id: cls_name, conf: float(conf), depth: float(median_depth) # 这里是归一化后的深度值 }) return original_img, detections, depth_map_normalized def main(): # 初始化测试器 tester YOLODepthTester(yolo_model_pathyolov8n.pt) # 会自动下载模型 # 指定测试图片路径 image_path ./input_images/demo.jpg # 请确保图片存在 # 进行预测 original_img, detections, depth_map tester.predict(image_path, conf_threshold0.3) # 打印结果 print(f\n检测到 {len(detections)} 个目标) for i, det in enumerate(detections): print(f [{i}] {det[cls_id]}: 置信度 {det[conf]:.2f}, 归一化深度 {det[depth]:.3f}) # 可视化并保存结果 output_path ./output_results/result.jpg plot_results(original_img, detections, depth_map, save_pathoutput_path) if __name__ __main__: main()4.4 运行与验证在项目根目录下创建input_images文件夹并放入一张测试图片如demo.jpg。确保weights/checkpoints/目录下存在Depth Anything V2的预训练权重。如果没有需要从官方仓库或Hugging Face Model Hub下载depth_anything_v2_vits.pth等文件并放入对应路径。YOLOv8的权重会自动下载。在终端运行主程序python main.py观察控制台输出会显示模型加载、推理进度和检测结果。程序会自动弹出结果对比图并将图片保存至output_results/result.jpg。4.5 结果说明运行成功后你会得到一张并排对比图。左侧是原始图像上面绘制了YOLOv8的检测框框的标签格式为类别:置信度, d:深度值。这里的深度值是归一化后的相对深度0-1之间数值越大代表离相机越“近”。右侧是Depth Anything V2生成的彩色深度热力图暖色红、黄代表近处冷色蓝、紫代表远处。5. 常见问题与排查思路在实际运行中你可能会遇到以下问题问题现象可能原因解决思路ModuleNotFoundError: No module named ‘depth_anything_v2’Depth Anything V2库未正确安装。1. 确保在Depth-Anything-V2目录下执行了pip install -e .。2. 检查Python环境是否正确激活。FileNotFoundError: [Errno 2] No such file or directory: ‘checkpoints/depth_anything_v2_vits.pth’预训练权重文件缺失。1. 从官方GitHub仓库的Release页面或Hugging Face下载权重文件。2. 创建checkpoints目录并将权重文件放入。或修改代码中的权重路径。YOLO检测框数量为01. 图片中确实没有目标。2. 置信度阈值(conf_threshold)设置过高。3. 模型加载出错。1. 换一张包含明显物体如人、车的图片测试。2. 降低conf_threshold例如设为0.1。3. 检查YOLO模型是否下载成功。深度图全黑或全白深度图数值范围异常归一化出错。1. 检查depth_map计算后是否包含NaN或Inf。2. 在归一化前打印depth_map.min()和depth_map.max()查看范围。3. 确保输入图像预处理符合Depth Anything V2的要求RGB归一化。程序运行非常慢1. 在CPU上运行。2. 图片分辨率过高。3. 模型版本过大如使用了YOLOv8x和Depth Anything V2-ViTL。1. 确认CUDA和PyTorch GPU版本已安装代码中device设置为‘cuda’。2. 在推理前将图片缩放到固定尺寸如640x640。3. 换用轻量级模型YOLOv8n, Depth Anything V2-ViTS。RuntimeError: CUDA out of memoryGPU显存不足。1. 降低输入图像分辨率。2. 使用更小的模型。3. 使用torch.cuda.empty_cache()清理缓存。4. 在CPU上运行。检测框深度值不合理如背景物体深度值比前景还小1. 深度估计模型在该场景下失效。2. 检测框包含了过多背景统计值被污染。3. 单目深度估计本身的局限性。1. 尝试使用框内深度值的中位数而非平均值。2. 考虑使用更精确的实例分割如YOLOv8-seg获取物体掩膜仅在掩膜区域内计算深度。3. 理解这是相对深度可能需要进行尺度对齐。6. 最佳实践与工程建议将研究原型转化为稳定可用的模块还需要考虑以下工程实践6.1 模型选择与优化速度与精度权衡对于实时应用如无人机视频流优先选择YOLOv8n和Depth Anything V2-ViTS。对于离线分析可以选择更大的模型以获得更好精度。模型量化与加速考虑使用ONNX Runtime或TensorRT对两个模型进行量化、融合图优化可以大幅提升推理速度。专用模型如果你的应用场景固定如室内机器人可以在特定场景数据上对深度估计模型进行微调以提升在该场景下的精度。6.2 深度信息后处理深度归一化与尺度恢复单目深度输出是相对的、无量纲的。要获得米制单位常见方法有相机标定法已知相机内参和地面假设可以通过几何关系恢复尺度。传感器融合与IMU或稀疏激光雷达点云匹配进行尺度对齐。先验知识法假设场景中某个已知尺寸的物体如车牌、行人平均身高来恢复全局尺度。深度滤波原始深度图通常有噪声特别是物体边缘和纹理缺失区域。可以使用简单的双边滤波、引导滤波或基于学习的方法进行平滑。6.3 代码结构与部署异步推理如果处理视频流可以将目标检测和深度估计放在两个独立的线程或进程中进行利用流水线提高整体吞吐量。批处理对于图片批量处理尽量将数据组成Batch进行推理能更充分利用GPU并行计算能力。服务化考虑使用FastAPI或Flask将整个Pipeline封装成REST API服务方便其他系统调用。日志与监控在生产环境中加入详细的日志记录推理耗时、检测数量、异常情况和性能监控。6.4 结果评估与验证定性评估通过可视化直观判断检测框和深度图的合理性。定量评估如果有带真实深度如激光雷达的数据集可以计算深度估计的指标如绝对相对误差AbsRel、平方相对误差SqRel、RMSE等。关联性检查检查检测框的深度值是否符合场景常识例如前方的车应该比远处的树深度值更大。通过本次“YOLO-DEPTH”单目深度测试我们成功搭建了一个将目标检测与深度估计相结合的基础框架。从环境配置、模型原理理解到代码实现、问题排查我们走完了整个流程。关键在于理解两个独立模型的输出如何有效融合——通过检测框在深度图上截取区域并计算统计量。虽然当前方案还有局限性如依赖检测框精度、深度为相对值但它为许多需要粗略3D感知的应用提供了一个快速起步的解决方案。下一步你可以尝试集成YOLOv8的实例分割模型来获取更精确的物体掩膜或者探索如何将相对深度转换为绝对距离甚至可以尝试端到端的、联合训练的目标检测与深度估计网络。