Ultralytics YOLO DepthValidator 源码全解:深度估计验证流程与指标体系剖析
【免费下载链接】ultralyticsUltralytics YOLO26, YOLO11, YOLOv8 — object detection, instance segmentation, semantic segmentation, image classification, pose estimation, object tracking项目地址: https://gitcode.com/GitHub_Trending/ul/ultralytics
单目深度估计任务在 Ultralytics YOLO 中通过DepthValidator完成验证评估。它从DetectionValidator继承而来,却彻底去掉了 NMS 与框类统计,改以 monocular depth 领域通行的 Eigen 评测协议对逐像素深度图打分。本文以 DepthValidator API 参考页 为主题,逐方法解读其在 val.py 中的实现,并延伸到底层 DepthMetrics 与 数据集 YAML,帮助你完整掌握"YOLO 深度模型如何被验证、六个深度指标如何计算、DDP 下如何汇总、验证结果如何落盘"。
1. DepthValidator 在 YOLO 任务体系中的位置
深度估计与检测、分割、姿态、OBB 等并列,是 YOLO 的原生任务之一。任务级的路由表定义在 model.py,其中"depth"映射到四件套:
"depth": { "model": DepthModel, "trainer": yolo.depth.DepthTrainer, "validator": yolo.depth.DepthValidator, "predictor": yolo.depth.DepthPredictor, },DepthValidator由 depth/init.py 统一导出(__all__ = "DepthPredictor", "DepthTrainer", "DepthValidator")。当你执行model.val(data=...)或yolo depth val ...时,框架会根据模型的任务类型自动实例化本类,因此理解深度验证只需读懂这一个类。
它的类文档言简意赅地点出了设计取向:
Validator for YOLO depth estimation models. Computes standard depth metrics: delta1, abs_rel, rmse, silog. Uses validation loss as the primary training signal.
也就是说,深度验证不产出 mAP,它的"好坏"由 delta1 / abs_rel / rmse / silog 等深度指标来衡量,训练阶段则以验证 loss 作为主监控信号。
2. 构造与初始化:init与 init_metrics
2.1 继承 DetectionValidator 并标记任务
class DepthValidator(DetectionValidator): def __init__(self, dataloader=None, save_dir=None, args=None, _callbacks=None): super().__init__(dataloader, save_dir, args, _callbacks) self.args.task = "depth"构造器把参数原样交给父类,随后强制把self.args.task置为"depth"。这一步很关键:评测时的进度条、结果表头、绘图文件命名、模型出口解析等都依赖 task 标识分发到 depth 专属逻辑。
2.2 用数据集深度上限初始化指标累加器
def init_metrics(self, model): self.metrics = DepthMetrics(max_depth=self.data.get("max_depth") or 100.0) self.metrics.clear_stats()每次验证开始时,依据数据集 YAML 里的max_depth字段实例化DepthMetrics;YAML 未声明时回退到默认值100.0(米)。随后clear_stats()清零所有累加器,保证model.val()可以重复调用而不受上一次结果污染。
max_depth同时也是评测协议的有效像素边界,在数据集配置中按场景调整。以 depth8.yaml 为例:
path: depth8-png train: images/train val: images/val nc: 1 names: 0: depth channels: 3 depth_scale: 1000 # PNG value 1000 = 1 meter其头部注释还给出了各数据集常见约定:ARKitScenes 与 NYU Depth V2 用depth_scale: 1000(1 mm 精度、上限 65.535 m),KITTI 用256,Virtual KITTI 2 用100。uint16 PNG 数值除以depth_scale才得到米制深度,其中编码0表示无效像素。
3. 一次验证迭代的数据流:preprocess → postprocess → update_metrics
3.1 preprocess:深度图保持 float32
def preprocess(self, batch): batch = super().preprocess(batch) # 移入设备 + RGB 图像归一化 batch["depth"] = batch["depth"].float() return batch父类负责把图像搬到目标设备并做归一化;深度分支在此基础上把真值深度图统一转成float32(覆盖部分 loader 输出 uint16/int 的情况),保证后续算术的数值行为一致。训练侧 train.py 的preprocess_batch采用了完全相同的"深度转 float32"约定,验证与训练的数据路径保持一致。
3.2 postprocess:不做 NMS,直接返回
def postprocess(self, preds): return preds与检测任务不同,深度输出是一整张稠密(B,1,H,W)图而非稀疏框,因此不存在非极大值抑制。postprocess只是透传模型原始输出——这也是深度任务省掉整个 NMS 链路的源码证据。
3.3 update_metrics:尺寸对齐后逐图像累计
def update_metrics(self, preds, batch): gt_depth = batch["depth"] if gt_depth.ndim == 3: gt_depth = gt_depth.unsqueeze(1) if preds.ndim == 3: preds = preds.unsqueeze(1) if preds.shape[-2:] != gt_depth.shape[-2:]: preds = F.interpolate(preds.float(), size=gt_depth.shape[-2:], mode="bilinear", align_corners=True) self.metrics.update_stats(preds, gt_depth)这里处理两类现实差异:
- 通道维度缺失:网络或 GT 可能以
(B,H,W)输出/存储,此处统一unsqueeze(1)成(B,1,H,W); - 空间尺寸不一致:当预测分辨率与真值分辨率不同(常见于 val 时
imgsz与 GT 尺寸不完全对齐),用双线性插值把预测图 resize 到 GT 尺寸,align_corners=True使角点像素对齐。之后才真正把累计工作交给DepthMetrics.update_stats()。
4. 深度指标体系:DepthMetrics 源码级拆解
指标类定义在 metrics.py 的DepthMetrics(第 1892 行起)。其类文档点出了几个设计基准:指标逐图像先归一、再对验证集平均,使每张图权重相同,与 Depth Anything V2 / Monodepth2 的 per-sample 平均一致;有效 GT 像素少于 10 的图像被整体跳过;非有限预测按深度界处理而非剔除该图。
4.1 构造参数与累加器
def __init__(self, min_depth=0.001, max_depth=100.0, align="median"): self.min_depth = min_depth self.max_depth = max_depth self.align = align self.speed = {"preprocess": 0.0, "inference": 0.0, "loss": 0.0, "postprocess": 0.0} self._totals = None self._count = 0.0 self._results = {}三个核心超参:
| 参数 | 默认值 | 语义 |
|---|---|---|
min_depth | 0.001 | 最小有效深度(米);GT 像素<= min_depth被忽略 |
max_depth | 100.0 | 最大有效深度(米);GT 像素>= max_depth被忽略,预测值被裁剪到此上限 |
align | "median" | 逐图像尺度对齐:"median"按median(gt)/median(pred)缩放预测;"none"关闭对齐、按原始输出尺度打分 |
累计器设计刻意选了CPU 上的 float64(注释说明 MPS 张量无法承载 float64),因此后续 DDP 归约只需"先求和、再 all-reduce"即可,无需精度妥协。
4.2 update_stats:Eigen 协议 + 逐图 median 对齐
def update_stats(self, preds, targets): p = preds.squeeze(1) if preds.ndim == 4 else preds g = targets.squeeze(1) if targets.ndim == 4 else targets if p.ndim == 2: p, g = p[None], g[None] # 单图 (H,W) → (1,H,W),保证对齐始终逐图进行 for pi, gi in zip(p, g): mask = (gi > self.min_depth) & (gi < self.max_depth) # Eigen 有效像素 if int(mask.sum()) < 10: # 少于 10 个有效像素则跳过 continue pv = pi[mask].float() gv = gi[mask].float() if self.align == "median": finite = torch.isfinite(pv) if finite.any(): scale = torch.median(gv[finite]) / torch.median(pv[finite].clamp_min(self.min_depth)) pv = pv * scale pv = torch.nan_to_num(pv, nan=self.max_depth, posinf=self.max_depth, neginf=self.min_depth ).clamp(self.min_depth, self.max_depth) thresh = torch.maximum(pv / gv, gv / pv) log_diff = torch.log(pv) - torch.log(gv) silog = (log_diff.pow(2).mean() - log_diff.mean().pow(2)).clamp_min(0.0).sqrt() * 100 image_metrics = torch.stack([ (thresh < 1.25).float().mean(), # delta1 (thresh < 1.25 ** 2).float().mean(), # delta2 (thresh < 1.25 ** 3).float().mean(), # delta3 (torch.abs(pv - gv) / gv).mean(), # abs_rel ((pv - gv) ** 2).mean().sqrt(), # rmse silog, ]) ... self._totals += image_metrics.cpu().double() self._count += 1.0逐行可以读出完整的评测约定:
- Eigen 掩码:只有 GT 落在
(min_depth, max_depth)开区间内的像素才参与打分,区间外像素既不当作误差也不拉低得分; - 10 像素下限:有效像素不足 10 的图像直接
continue跳过——对极稀疏 GT 做 median 对齐没有意义(与 Depth Anything V2 的下限一致); - median 对齐:对 non-finite 外的像素求
scale = median(gt) / median(pred)(分母先clamp_min(min_depth)防除零),再整图缩放。这让尺度模糊(affine-invariant)的相对深度输出也能与米制 GT 公平比较; - 非有限值兜底:
nan_to_num把 NaN/正无穷记为max_depth、负无穷记为min_depth,再裁剪进[min_depth, max_depth],避免单点脏数据毁掉整张图的平均; - 阈值判定:
thresh = max(p/g, g/p)保证误差在两个方向上对称,thresh < 1.25、1.25²、1.25³分别对应 delta1/2/3 的像素占比; - silog 公式:采用 λ=1 的方差形式(ZoeDepth/KITTI 风格)——
sqrt(E[log²] - E[log]²) × 100,即 log 域残差的(裁剪后非负)标准差。由于在单图上先完成再累计,silog 也遵循逐样本平均; - 逐图入账:
image_metrics先求整图均值、再累加进 float64 的_totals,_count每图 +1——最终取平均时每张图权重相等,与每张图有效像素多少无关。
4.3 process / keys / fitness:六个指标的定义与输出
process()用_totals / _count归一后产出带命名空间的字典(metrics.py 第 1983-1990 行):
self._results = { "metrics/delta1": d1, "metrics/delta2": d2, "metrics/delta3": d3, "metrics/abs_rel": abs_rel, "metrics/rmse": rmse, "metrics/silog": silog, }对应语义与方向:
| 指标 | 含义 | 方向 |
|---|---|---|
delta1/2/3 | 预测与真值之比(双向)落在 1.25、1.25²、1.25³ 内的像素占比 | 越高越好 |
abs_rel | 平均绝对相对误差mean(|p−g|/g) | 越低越好 |
rmse | 均方根误差,单位米 | 越低越好 |
silog | 尺度不变对数误差(×100),越低说明相对结构越一致 | 越低越好 |
DepthMetrics.keys(1999-2008 行)给出了日志顺序;fitness属性(2044-2047 行)把delta1作为整体适应度(higher is better),用于超参搜索与 checkpoint 比较;curves/curves_results返回空列表——深度任务没有 PR 曲线。summary()(2064 行起)可将结果压成单行字典,便于写入 CSV / JSON。
5. 结果汇总:get_stats、gather_stats 与 DDP 跨卡归约
5.1 get_stats:只在 rank 0 上做最终归一
def get_stats(self): self.metrics.process() return self.metrics.results_dict注释明确:跨 rank 的指标归约由gather_stats()负责(它会在所有 rank 上先执行),get_stats()只在 rank 0 上运行,此时累加器已是全局求和结果。
5.2 gather_stats:重写父类,仅归约深度所需的标量
DetectionValidator.gather_stats()会归约检测专用的统计/框属性,而DepthMetrics根本没有这些成员。因此DepthValidator必须覆写,只 all-reduce 两样东西:
def gather_stats(self): if RANK == -1 or not dist.is_initialized(): return totals = self.metrics._totals totals = (totals.to(self.device) if totals is not None else torch.zeros(6, dtype=torch.float64, device=self.device)) count = torch.tensor([self.metrics._count], dtype=torch.float64, device=self.device) dist.all_reduce(totals, op=dist.ReduceOp.SUM) dist.all_reduce(count, op=dist.ReduceOp.SUM) self.metrics._totals = totals self.metrics._count = float(count.item())原理如下:DDP 验证时数据被ContiguousDistributedSampler分片,每个 rank 只持有自己那一份的求和统计。all_reduce(SUM)后,rank 0 上得到整个验证集的总和与总图像数,随后get_stats()求平均即可得到全量指标——而不是单个分片的近似值。单卡场景(RANK == -1或分布式未初始化)则直接跳过归约。
6. 结果输出与可视化:print_results、get_desc、plot_predictions
6.1 对齐检测风格的日志表格
get_desc()与print_results()共同决定了验证结束时的终端输出格式。get_desc()定义表头:
("%22s" + "%11s" * 5) % ("Class", "Images", "delta1", "abs_rel", "rmse", "silog")print_results()用同一套宽度拼行,行标签为"depth_val"(深度没有类别概念,故不像检测那样打"all"):
pf = "%22s" + "%11i" + "%11.4g" * 4 LOGGER.info(pf % ("depth_val", n_images, r.get("metrics/delta1", 0.0), r.get("metrics/abs_rel", 0.0), r.get("metrics/rmse", 0.0), r.get("metrics/silog", 0.0)))因此你在验证日志中看到的最终一行大致形如depth_val | 654 | 0.8602 | 0.0761 | 0.3125 | ...,各列与第 4 节的指标一一对应。
6.2 finalize_metrics:回填速度与保存目录
def finalize_metrics(self): self.metrics.speed = self.speed self.metrics.save_dir = self.save_dir把验证阶段统计的 preprocess/inference/postprocess 耗时写回DepthMetrics.speed(其 4 个键的初始值见 4.1),同时让指标对象知道可视化结果要写到哪个目录。
6.3 plot_predictions:热力图叠层而非画框
def plot_predictions(self, batch, preds, ni): plot_images( labels={"depth": preds}, images=batch["img"], paths=batch["im_file"], fname=self.save_dir / f"val_batch{ni}_pred.jpg", names=self.names, on_plot=self.on_plot, )深度没有框与类别,检测版的画框可视化不可用,因此这里复用共享的plot_images路径,把预测深度图作为labels={"depth": preds}传入,生成val_batch{ni}_pred.jpg热力图叠层——与语义分割的可视化风格一致。finalize_metrics中记录的save_dir正是这些 jpg 的落盘目录。
7. 实战:如何触发与读取一次深度验证
验证器在训练过程中由 train.py 的DepthTrainer.get_validator()自动创建并注入(使用测试集 loader、训练保存目录与当前 args)。训练结束后final_eval()还会对 best/last checkpoint 做一次基于验证集的 log-affine 尺度校准(calibrate_checkpoint),使保存的权重开箱即输出"米制尺度"的深度。
独立验证则走valmode,官方推荐在训练分辨率imgsz=768下进行:
# 验证官方预训练权重(Eigen 测试切分) yolo depth val model=yolo26n-depth.pt data=nyu-depth.yaml imgsz=768 # 验证自己训练的 checkpoint yolo depth val model=path/to/best.pt data=path/to/data.yamlfrom ultralytics import YOLO model = YOLO("yolo26n-depth.pt") # 或 path/to/best.pt metrics = model.val(data="nyu-depth.yaml", imgsz=768) print(metrics.delta1) # 像素级 δ=1.25 阈值内占比,越高越好 print(metrics.delta2, metrics.delta3) print(metrics.abs_rel) # 越低越好 print(metrics.rmse) # 米制均方根误差 print(metrics.silog) # 尺度不变对数误差验证命令自动选择DepthValidator的注册依据是 checkpoint 元数据中的任务标记(对应 model.py 的"depth"路由)与-depth模型文件后缀约定。data=必须显式指向评测用数据集 YAML——它的max_depth、depth_scale直接决定哪些 GT 像素计入指标以及米制换算是否正确。
8. 小结:一图读懂深度验证的完整闭环
回顾整个流程,DepthValidator的每个覆写点都对应一个明确的工程决策:
| 方法 | 覆写动机 | 关键行为 |
|---|---|---|
__init__ | 固化任务标识 | args.task = "depth" |
init_metrics | 接入深度专属累加器 | DepthMetrics(max_depth=data.max_depth or 100) |
preprocess | 数据类型统一 | depth 转float32 |
postprocess | 无后处理需求 | 直接返回稠密预测 |
update_metrics | 稠密图而非框 | 双线性插值对齐后逐图累计 |
gather_stats | 父类归约不适配 | 仅 all-reduce 6 维 totals 与 count |
get_stats | 结果字典化 | process()后返回metrics/results_dict |
get_desc/print_results | 表格列适配 | depth_val行 + delta1/abs_rel/rmse/silog 列 |
plot_predictions | 无框可视化 | 深度热力图叠层val_batch{ni}_pred.jpg |
从数据流看,一次验证是loader 产出 (img, depth)→preprocess归一化 → 前向推理得到(B,1,H,W)预测 →update_metrics插值对齐 →DepthMetrics逐图做 Eigen 掩码 + median 对齐 + 六指标入账 → DDPgather_stats全局求和 → rank 0get_stats平均 → 表格日志 + 热力图落盘 的完整闭环。
无论你是要复现论文中的 delta1/abs_rel,还是排查自定义数据集上验证分数偏低(先检查depth_scale与max_depth是否与数据约定一致),val.py 与 metrics.py 中的这段源码都是理解 YOLO 深度验证行为最直接、最权威的入口。配套可进一步阅读 单目深度估计任务指南 与 深度数据集格式说明,以掌握数据集侧 YAML 字段与评测协议的完整对应关系。
【免费下载链接】ultralyticsUltralytics YOLO26, YOLO11, YOLOv8 — object detection, instance segmentation, semantic segmentation, image classification, pose estimation, object tracking项目地址: https://gitcode.com/GitHub_Trending/ul/ultralytics
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考