ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLO+深度估计实现3D目标检测:从2D框到三维位置的完整源码解析

YOLO+深度估计实现3D目标检测:从2D框到三维位置的完整源码解析 简介这是一套面向计算机视觉研究者和开发者的三维目标检测实战项目源码核心思路是将YOLO实时检测与深度估计技术结合从二维图像中推断目标物体的三维位置与边界框可应用于自动驾驶、机器人导航、安全监控等场景。压缩包共七个文件含五个Python脚本分别承担相机参数加载、检测模型构建、深度模型推理、三维边界框工具封装与主运行流程另附依赖说明和阅读文档整包仅二十KB结构精简便于快速部署。目前已有119人学习可作为二次开发或学术研究的实用起点。通过阅读与运行源码读者可掌握三维数据处理方式理解深度估计如何增强YOLO的立体检测能力并学会针对不同应用场景调整优化模型是高质量的项目参考。1. YOLO深度估计的3D目标检测这套源码是如何把2D边界框换算成三维位置的从事自动驾驶或机器人项目的人应该都有这种体会YOLO 跑得再快输出的也只是图像上一个个二维矩形框。可车辆决策、机械臂抓取、障碍物绕行真正需要的是目标在三维空间里的位置——离我多远、偏左还是偏右、高度在哪。于是 3D 目标检测成了从「看得见」到「看得懂」之间绕不开的一步。这套基于 YOLO 与深度估计结合的源码解决的正是这个问题用 YOLO 完成 2D 目标检测用深度估计网络补上距离信息再通过相机内参把两者融合成带长度、宽度、高度的三维边界框。项目包含 depth_model.py、detection_model.py、bbox3d_utils.py、load_camera_params.py、run.py 等完整 Python 源码适合已经跑通过 2D 检测、想往 3D 方向迈一步的工程师和研究者。下面我从代码链路、数学换算、运行踩坑三个层面把这个项目的实际内容和可复现步骤拆开讲。2. 先看链路再动代码检测、深度、相机参数如何组装成一套推理流程2.1 为什么单靠YOLO做不了3D检测从2D到3D缺了什么YOLO 的回归头预测的是边界框中心坐标、宽高和类别置信度这些量全部定义在图像像素坐标系里。换句话说YOLO 告诉你「目标在画面第 400 列、第 300 行宽 120 像素、高 80 像素」但它不告诉你目标离相机是 5 米还是 50 米。单目图像本身是透视投影的结果深度信息在投影过程中被丢掉了。要想从一张 2D 图像恢复三维位置必须补两样东西一是每个像素或者每个目标的深度值二是相机把三维世界投影到二维图像时的那组内参和外参。本项目里的 depth_model.py 就是用来补第一样东西的load_camera_params.py 则是用来补第二样东西的。三者在 run.py 里汇合才最终得到三维边界框。有个常见误区需要先澄清有些同学以为给 YOLO 的标签里加上 depth 通道或者在 loss 里加一个深度回归项就算「YOLO深度估计」了。但实际工程里深度估计通常是一个独立的分支网络它和 2D 检测网络并行推理然后在后处理阶段做融合而不是简单改一下 YOLO 的输出层。原因很简单深度估计需要的是全局场景信息——纹理梯度、遮挡关系、物体相对尺度这些信息在整张图上的感受野要求远大于单个目标框。把深度任务硬塞进 YOLO 的检测头里要么导致训练不稳定要么深度精度明显下降。所以这套源码采用双网络结构是符合当前主流做法的。2.2 五个核心文件各自只干一件事我把项目里的 Python 文件逐个打开看了一遍职责划分很清晰非常适合拿来当工程模板。load_camera_params.py 负责读取相机标定参数我一般把它理解为整个数据流的「度量基准」detection_model.py 负责加载 YOLO 权重并执行前向推理输出的是 2D 边界框、类别和置信度depth_model.py 负责加载深度估计网络输出的是和输入图像同分辨率的深度图bbox3d_utils.py 是核心计算模块它把 2D 边界框、深度值、相机内参三者融合计算出 3D 边界框的八个顶点坐标run.py 是编排入口把前面四个模块串成一条完整推理流水线。这个设计有一个明显好处任何一个环节想换方案都不需要动其他文件。比如你想把深度估计网络从 A 模型换成 B 模型只需要保证 depth_model.py 的输出接口不变——仍然是返回和输入图同尺寸的深度图——其他代码一行都不用改。同理YOLO 检测器从 YOLOv5 换到 YOLOv8只要 detection_model.py 对外依然输出(x_center, y_center, width, height, class_id, confidence)格式的结果后面的 bbox3d_utils.py 完全无感。2.3 三路数据流的汇合点run.py 里到底发生了什么顺着 run.py 的执行顺序可以看到整个推理流程是这样的首先 load_camera_params.py 把相机内参矩阵 K 和必要的外参信息加载进来然后检测模型和深度模型分别对同一帧图像做前向推理得到 2D 框列表和深度图接着 bbox3d_utils.py 逐个处理每个 2D 框利用框内区域的深度值估计目标距离结合相机内参反投影计算出目标在相机坐标系下的三维中心坐标、长宽高以及偏航角最后把三维框投影回图像平面叠加可视化结果。下面是一个简化版的数据流伪代码便于你理解各文件之间的调用关系import torch from load_camera_params import load_camera_intrinsics from detection_model import YOLODetector from depth_model import DepthEstimator from bbox3d_utils import build_3d_boxes_from_2d # 1. 加载相机内参 K load_camera_intrinsics(camera_calib.yaml) # 得到的 K 通常是 3x3 矩阵[[fx, 0, cx], [0, fy, cy], [0, 0, 1]] # 2. 初始化两个网络 detector YOLODetector(weightsyolo_weights.pt, conf_thres0.4) depth_net DepthEstimator(weightsdepth_weights.pth) # 3. 读取图像 image cv2.imread(sample.jpg) # 4. 分别推理 boxes_2d detector.predict(image) # 每个元素: [xc, yc, w, h, cls, conf] depth_map depth_net.infer(image) # 和 image 同分辨率的深度图 # 5. 融合为 3D 边界框 boxes_3d build_3d_boxes_from_2d( boxes_2dboxes_2d, depth_mapdepth_map, camera_matrixK )这段代码里最关键的是第 5 步。build_3d_boxes_from_2d做的事情不是简单地把 2D 框「抬高」一下而是通过相机投影模型做反算。需要注意两点第一detector 输出的中心点(xc, yc)是像素坐标而相机内参 K 里的cx, cy也是像素坐标两者天然对齐不需要额外换算第二深度图上的值如果是相对深度很多单目深度模型的输出是相对值而非真实距离那么在融合前需要一个尺度校准步骤这个我在第 4 章会展开讲因为这里往往就是输出 3D 框位置偏得离谱的根源。3. 环境配置与首次运行requirements.txt 到 run.py 的执行链路3.1 依赖安装与版本选择逻辑项目自带 requirements.txt这在复现时能省不少事。我建议用虚拟环境安装不要直接装进基础 Python 环境。实际安装时我习惯把 PyTorch 单独拿出来先装因为 requirements.txt 里往往写的是torch1.8.0这样的宽松约束而你的 CUDA 版本直接决定了能装哪个版本的 PyTorch。如果你的机器是 NVIDIA GPU先确认驱动支持的 CUDA 版本再去 PyTorch 官网选对应的安装命令如果没有 GPU就装 CPU 版但推理速度会慢一个数量级后面的实时性讨论也就无从谈起。然后是其他依赖项目里常见的是 opencv-python、numpy、matplotlib、pyyaml 以及深度模型可能用到的 timm 或 transformers。我的习惯是分批安装而不是一次性pip install -r requirements.txt——一次性装容易在某个包编译时报错时搞不清是哪个依赖引起的。分四批装第一批装 numpy、opencv-python、matplotlib 这类基础库第二批装 torch 和 torchvision第三批装模型相关的库第四批再装剩余项。下面是按这种方式执行时的命令示意# 创建虚拟环境并激活 python -m venv venv_3d source venv_3d/bin/activate # 第一批基础库 pip install numpy opencv-python matplotlib pyyaml # 第二批深度学习框架先确认自己的 CUDA 版本再调整 cu121 后缀 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 第三批安装项目 requirements 的剩余部分 pip install -r requirements.txt装完后立刻做一个验证而不是直接跑完整程序。验证方法是在 Python 里导入整个链路涉及的核心模块看有没有报缺失符号的错误import torch import cv2 print(CUDA available:, torch.cuda.is_available()) print(cuDNN enabled:, torch.backends.cudnn.enabled)这一步看着简单但能提前暴露 80% 的环境问题。很多同学一上来就python run.py报错后分不清到底是代码问题还是环境问题排查成本反而更高。先确认torch.cuda.is_available()是 True再把项目文件依次导入至少能保证没有缺依赖层面的低级错误。3.2 相机参数文件结构load_camera_params.py 读取的是什么相机内参是 3D 目标检测的度量基准它一旦错了后面所有 3D 坐标都是错的。项目里的 load_camera_params.py 通常读取一个 YAML 或 JSON 格式的标定文件里面至少包含内参矩阵 K 和图像分辨率。有时候还包含畸变系数但在这个项目的主流程里畸变矫正并不是必需环节如果图像来自合成数据或者畸变很小的相机可以暂时忽略畸变项。下面是符合项目常见读取逻辑的 YAML 文件示例camera_matrix: fx: 7.215377e02 fy: 7.215377e02 cx: 6.095593e02 cy: 1.728540e02 image_width: 1242 image_height: 375加载函数的核心逻辑是这样写的import yaml import numpy as np def load_camera_intrinsics(yaml_path): with open(yaml_path, r) as f: data yaml.safe_load(f) K np.array([ [data[camera_matrix][fx], 0, data[camera_matrix][cx]], [0, data[camera_matrix][fy], data[camera_matrix][cy]], [0, 0, 1] ], dtypenp.float32) img_size (data[image_width], data[image_height]) return K, img_size这里有一个容易搞错的地方YAML 里fx和fy的单位是像素不是毫米。它等于焦距毫米除以像元尺寸毫米/像素所以同一个相机在不同分辨率下fx, fy会不同。你这个项目如果要把模型迁移到另一台相机上必须重新标定直接沿用原有参数会让 3D 框整体偏移。另外KITTI 数据集的相机内参矩阵是 3x3 的直接按 3x3 读取没问题但如果你拿到的标定文件里写的是 4x4 的投影矩阵就需要先区分清楚是 P2 矩阵还是真正的内参矩阵 K。这一步错了后面反投影全是乱的。3.3 运行 run.py一次完整的前向推理流程环境就绪后可以直接运行入口脚本。run.py 的执行逻辑我在上一章已经用伪代码描述过这里给出更接近真实项目结构的写法import cv2 import torch from depth_model import DepthEstimator from detection_model import YOLODetector from bbox3d_utils import draw_3d_boxes_on_image from load_camera_params import load_camera_intrinsics def main(): # 参数配置 image_path data/sample.jpg calib_path config/camera.yaml yolo_weights weights/yolov5s.pt depth_weights weights/depth_model.pth # 加载模型 detector YOLODetector(yolo_weights, conf_thres0.35, devicecuda) depth_net DepthEstimator(depth_weights, devicecuda) K, img_size load_camera_intrinsics(calib_path) # 推理 image cv2.imread(image_path) image_rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) boxes_2d detector.predict(image_rgb) # boxes_2d 中的每个元素为 [x_center, y_center, width, height, class_id, score] depth_map depth_net.infer(image_rgb) # depth_map shape 为 (H, W)和原图分辨率一致 # 融合成3D框 boxes_3d build_3d_boxes_from_2d(boxes_2d, depth_map, K) # 可视化 output_image draw_3d_boxes_on_image(image, boxes_3d) cv2.imwrite(output_with_3d_boxes.jpg, output_image) print(fDetected {len(boxes_3d)} objects in 3D.) if __name__ __main__: main()这段代码里conf_thres0.35是我针对该项目调整后的经验值。默认 0.25 容易在后处理阶段带进过多低质量框导致 3D 后处理噪声变大0.5 以上又可能在目标较小或遮挡严重的场景里漏检。0.35 是个相对稳的折中值但具体项目还要根据你的样例图像实测。另一个值得注意的点是DepthEstimator.infer()的输入输出格式我统一把输入转成了 RGB因为检测模型和深度模型的预训练权重大多基于 RGB 输入训练如果你直接用 OpenCV 读进来的 BGR 图喂进去颜色通道反了会导致检测置信度明显下降深度图输出也会出现奇怪的伪影。这个坑我见过不少人踩而且错误很隐蔽——画面看起来正常但结果就是不对。4. 核心实现细节bbox3d_utils.py 的数学逻辑与参数调优4.1 从像素坐标到相机坐标系反投影公式推导先明确一个基本公式。相机将三维点(X, Y, Z)投影到像素坐标(u, v)的模型是u fx * X / Z cx v fy * Y / Z cy反过来如果知道了某个像素的深度Z就能反算出该点在前方相机坐标系下的三维坐标X (u - cx) * Z / fx Y (v - cy) * Z / fybbox3d_utils.py 核心就是在逐框应用这个反投影公式。实际操作时它不会直接用 2D 框中心点的单个深度值而是取框内有效深度值的中位数或均值。为什么取中位数更稳因为单目深度估计在物体边缘、反光区域经常给出异常值如果用最大值或最小值一个离群点就能让整个 3D 中心偏移几十厘米。用中位数对离群点不敏感是工程里更可靠的选择。源码里对应的逻辑通常长这样def estimate_object_depth(box_2d, depth_map): x_c, y_c, w, h box_2d[:4] # 取框内区域边缘收缩 10% 避免混入背景 x1 int(x_c - w * 0.4) x2 int(x_c w * 0.4) y1 int(y_c - h * 0.4) y2 int(y_c h * 0.4) roi_depth depth_map[y1:y2, x1:x2] valid_depth roi_depth[roi_depth 0] # 过滤无效值 if valid_depth.size 0: return None # 中位数比均值更能抵抗深度估计的异常点 return float(np.median(valid_depth))有两处值得调整。第一采样区域的收缩比例 0.4 是我按经验取的如果你检测的目标本身很小比如 30 像素以下收缩太多会导致采不到足够像素这时可以放宽到 0.45 或干脆不做边缘收缩第二roi_depth 0的过滤条件有些深度网络输出 0 表示无效区域有些输出的是负值或 NaN你要根据自己用的深度模型来调整过滤逻辑。不过滤的后果是 3D 中心点会突然跳到无穷远或者跑到相机背后检测框直接消失。4.2 深度尺度恢复从相对深度到真实距离的转换单目深度估计模型的通病是输出相对深度而非真实的绝对距离。这类网络通常在编码器-解码器结构上训练输出的是一个无量纲的视差图或归一化深度图。换句话说模型知道「树比人远、墙比树远」但它不知道「墙具体是 5 米还是 50 米」。如果直接把这种相对深度代入反投影公式得到的 3D 框在比例上是对的但绝对位置完全失真。项目中通常有两种手段处理这个问题。第一种是假设深度图已经通过某种方式对齐到真实尺度比如用标定板或激光雷达点云做过尺度标定第二种是在推理时给深度图乘一个全局缩放因子这个因子由数据集统计得到。我一般会写一个快速验证脚本取画面中一个已知实际大小的物体测量它在这张图里占据的像素高度然后计算当前深度模型输出的值与该物体实际距离的比值以此估算缩放系数。这种校准方法虽然粗糙但在没有激光雷达真值的情况下是唯一能快速恢复尺度的手段。具体做法可以这样组织代码# 估算深度缩放因子的简化流程 # 已知标定物距相机真实距离 Z_real米深度网络输出值 D_raw # 计算scale Z_real / D_raw Z_real 10.0 # 例如目标实际距离 10 米 D_raw 0.8 # 深度网络输出的原始值 scale Z_real / D_raw # 应用缩放对整张深度图统一乘 scale depth_map_scaled depth_map * scale这个 scale 不是一次算完就万事大吉。不同场景、不同光照下深度网络的输出分布会漂移严格的工程做法是每隔一段时间重新做一次在线校准或者采集若干帧数据离线统计出不同场景下的 scale 分布范围然后取中位数作为默认值。项目源码的 README 里如果没有明确说明 depth_model 的预训练权重是在哪个数据集上训练的、是否包含尺度恢复逻辑那么这段代码就需要你自己补上否则后续的 3D 框验证没有意义。4.3 3D框的八个顶点与显示参数从中心点、尺寸到偏航角3D 边界框不仅有位置还有朝向。自动驾驶场景里一辆车的 3D 框如果朝向错了哪怕中心和尺寸都对仍然会严重影响后续的路径规划。bbox3d_utils.py 里 3D 框的表示通常由三部分组成三维中心点(X, Y, Z)、三维尺寸(length, width, height)、以及一个绕 Y 轴的旋转角yaw。给定这三个量八个顶点就能计算出来。顶点计算可以这样实现import numpy as np def compute_3d_box_corners(center, dimensions, yaw): l, w, h dimensions x, y, z center # 半长半宽半高 hl, hw, hh l / 2, w / 2, h / 2 # 先构造局部坐标下的八个顶点未旋转 corners_local np.array([ [ hl, hw, hh], [ hl, -hw, hh], [-hl, -hw, hh], [-hl, hw, hh], [ hl, hw, -hh], [ hl, -hw, -hh], [-hl, -hw, -hh], [-hl, hw, -hh] ]) # 绕Y轴旋转 cos_yaw, sin_yaw np.cos(yaw), np.sin(yaw) rotation_matrix np.array([ [cos_yaw, 0, sin_yaw], [0, 1, 0 ], [-sin_yaw,0, cos_yaw] ]) corners_world corners_local rotation_matrix.T np.array([x, y, z]) return corners_world这里有个容易忽略的细节yaw 角的定义方式在不同数据集里还不一样。KITTI 数据集的 yaw 是目标朝向与相机光轴方向的夹角而且有个符号约定如果你直接拿另一个数据集训练好的检测头来给这个项目用yaw 的符号或者零点基准稍有差异3D 框看起来就像被「拧」了一下。我的建议是先把已知图片上的目标手动标一个粗略朝向和模型输出对比确认角度定义一致后再批量处理数据别闷头跑全量流程。4.4 地面约束与截断处理把数学结果拉回物理世界纯几何反投影出来的 3D 框在数学上自洽但在物理世界里可能不成立——比如框的中心跑到路面以下或者在图像边缘出现严重畸变。项目里虽然没有专门写地面约束模块但实际使用中我会在前面的代码基础上补一个简单的地面高度检验如果目标类别是车辆或行人3D 框底面中心的 Y 坐标相机坐标系下是高度方向应该在合理范围内比如 -1 米到 1 米之间超出就视为异常检测结果直接丢弃或者降置信度。这个约束对提升整体准确率非常有效因为深度估计在远距离时的误差会放大几十米外的车算出来的高度往往离谱。加一个先验范围能提前拦掉一批错误框。5. 避坑与常见问题复现这个项目时踩过的五个典型坑5.1 深度图全黑或全灰可视化完全看不出内容现象把 depth_model 输出的深度图用 cv2.imshow 或 plt.imshow 显示整幅图要么全黑要么全白。 原因深度网络输出的原始值是 float32取值范围可能是 0 到 1也可能是 0 到 80 米。拿来直接当 8 位图显示时数值范围远小于 0-255就会显示成一片黑如果存在 NaN 或 inf显示成纯白甚至花屏。 解决先用 np.nan_to_num 清洗非法值再对有效值做归一化显示。调试用的代码我一般这样写def visualize_depth(depth_map): depth_clean np.nan_to_num(depth_map, nan0.0, posinf0.0, neginf0.0) d_min, d_max depth_clean.min(), depth_clean.max() if d_max - d_min 1e-6: return np.zeros_like(depth_clean, dtypenp.uint8) depth_norm (depth_clean - d_min) / (d_max - d_min) return (depth_norm * 255).astype(np.uint8)优先级最高的动作是先确认这个显示问题再决定要不要继续往下调。因为如果深度图分布不正常后面 3D 框一定全错调后处理参数只会越调越乱。5.2 3D 框整体偏移半米以上位置明显不对现象目标明明在图像中央3D 框的中心却偏到图像边缘的投影位置。 原因相机内参矩阵和实际使用的图像不匹配。最常见的是两种一是标定文件里的fx, fy, cx, cy来自原始分辨率但喂给 YOLO 网络时图像被 resize 过像素坐标系已经变了二是内参矩阵本身的第一第二行写反或者 YAML 字段读错。 解决所有输入图片统一走同一条预处理通道并且用原始分辨率下的标注文件做验证。可以参考下面的检查方法# 校验相机内参是否与图像匹配的常见做法 origin_img_w 1920 resized_img_w 640 scale resized_img_w / origin_img_w # 内参也需要跟着缩放 K_resized K.copy() K_resized[0, 0] * scale # fx K_resized[1, 1] * scale # fy K_resized[0, 2] * scale # cx K_resized[1, 2] * scale # cy关键是要让内参矩阵和实际输入图像的分辨率对应。如果 resize 到 640 宽而内参还是 1920 宽下的值算出来的 X、Y 坐标直接偏掉。我现在的习惯是每换一个输入分辨率就检查一次内参缩放形成肌肉记忆。5.3 推理速度比预期慢很多达不到实时现象在 GPU 上跑单帧图像耗时远超预期甚至 CPU 和 GPU 差不多。 原因最常见的是两个——模型没有切换到 eval 模式或者推理时没有包在torch.no_grad()里。YOLO 和深度网络在训练模式下会计算梯度、更新 BatchNorm 统计量这会让前向推理慢一到两倍。另一个次要原因是输入图像没有做批次化单张图反复调用 forwardGPU 利用率上不去。 解决推理前显式调用.eval()和torch.no_grad()。这不是玄学是确实能带来可量化的提速效果。torch.no_grad() def infer(self, image): self.model.eval() # 这里的 image 已经转成 tensor 并放到 cuda output self.model(image) return output如果加了这两项速度还是不够再检查输入分辨率。一个 1242x375 的 KITTI 图和 1920x1080 的监控图推理耗时差别非常大不要盲目追求大分辨率输入。5.4 YOLO 检测漏检严重3D 框数量少得可怜现象标准 YOLO 权重在同一张图上能检出 20 个目标接进这个项目后只能检出 5 个。 原因大概率是 NMS 里 IOU 阈值设置偏严或者检测类别数和项目后处理期望的类别映射对不上。比如 YOLO 权重在 COCO 上训练有 80 个类如果你自己的类别编号映射写错nms 后剩下来的框就会变少。 解决先用原始 YOLO 单独跑一遍确认检测结果和预期一致再接入整个 3D 链路。这种「先隔离检测网络再隔离深度网络」的排查方式是我做这类复合项目时最常用的方法。5.5 显存不足稍微把 batch 调大就 OOM现象batch size 设 1 能跑设 4 直接 CUDA out of memory。 原因项目里检测模型和深度模型同时驻留在显存里每个网络还要保留中间激活值显存占用是「检测模型 深度模型」的总和而非单模型。 解决除非你的显存超过 16GB否则不建议在推理阶段调大 batch。更有效的方式是保持 batch size 为 1用 CUDA 流或者直接顺序推理两个网络把峰值显存压下来。另外一个容易被忽略的点是torch.cuda.empty_cache()在每帧处理完后调用一下能在连续处理视频时显著降低显存碎片化。6. 进阶验证把 3D 框重新投影回 2D 图像用一致性检验替代真值对比没有激光雷达真值的情况下怎么判断 3D 框算得对不对我常用的方法是「投影回路验证」把 bbox3d_utils 算出来的 8 个 3D 顶点用相机内参重新投影回图像平面然后画出来跟 2D 检测框做重叠度比较。如果深度估计准确、内参无误3D 框的 8 个投影点应该恰好包围住原始 2D 框所对应的目标区域。投影代码可以这么写def project_3d_points_to_2d(points_3d, camera_matrix): points_3d np.array(points_3d, dtypenp.float32) projected, _ cv2.projectPoints( points_3d, rvecnp.zeros((3, 1)), tvecnp.zeros((3, 1)), camera_matrixcamera_matrix, dist_coeffsnp.zeros((4, 1)) ) return projected.reshape(-1, 2)cv2.projectPoints假定 3D 点已经位于相机坐标系所以旋转和平移向量都传 0。把投影后的 8 个点连起来如果画出来的三维框和目标实际轮廓在图像上贴合说明从反投影到深度估计整条链路是自洽的。如果贴合不好就用二分定位问题只验证中心点投影投影中心和 2D 框中心偏差大就是反投影或深度尺度问题中心对但边框贴合差就是尺寸或朝向估计问题。这个技巧在没有真值标注的私有数据上特别管用能把调试成本降一个数量级。再做一步视频流上的连续性检查。单帧检测出现个别抖动是正常的但如果相邻帧的 3D 框中心点位置发生跳变大概率不是模型随机误差而是某个中间数据异常。我一般会在 run.py 里加一个简单的帧间缓冲记录前 5 帧的 3D 中心点取中位数作为当前帧输出。这个操作在消融实验里能明显降低检测框抖动非常适合工程部署场景。回看这个项目从 bbox3d_utils.py 里几千行的几何计算到 depth_model.py 里看起来毫不起眼的深度归一化每个文件都有值得深挖的细节。我复现完的感受是这类项目的难点从来不在单点技术而在把相机参数、深度尺度、坐标约定三个环节拉齐。从那以后我每次拿到新数据都会强制走一遍「先验证内参、再验证深度尺度、最后做投影回路」三个检查这套流程帮我省下了大量排查时间。希望这份拆解也能帮你在自己的项目里少走几步弯路。本文还有配套的精品资源点击获取
返回列表