单目RGB端到端3D目标检测:YOLOv5双头结构+相机几何解算

发布时间:2026/10/18 11:06:02
单目RGB端到端3D目标检测:YOLOv5双头结构+相机几何解算
简介本资源是一套面向计算机视觉初学者与算法工程师的3D目标检测实战项目聚焦于YOLO框架与单目深度估计融合的轻量级三维检测方案适用于自动驾驶感知模块验证、机器人环境理解等实际场景。压缩包共7个文件含5个核心Python脚本如detection_model.py、depth_model.py、run.py及3D边界框工具bbox3d_utils.py、1份依赖说明requirements.txt和1份结构清晰的README.md整体仅20KB便于快速部署与代码研读。目前已有119人学习下载反映出其在轻量化3D检测入门实践中的实用价值。读者可直接运行完整推理流程掌握YOLO输出二维框后如何结合深度图回归三维位置、如何标定相机参数、以及端到端训练与评估的关键实现细节是理解从2D检测迈向3D空间建模的重要技术跳板。1. 这不是“YOLO加个深度图”就能跑通的3D检测——它真能输出带尺寸、朝向、真实世界坐标的3D框且在单张RGB图上端到端完成不依赖LiDAR、不调用Open3D可视化、不硬塞KITTI预处理脚本上周帮某高校实验室调试一个“YOLO深度”的3D检测demo对方说“就跑个效果看看”结果卡在depth_model.py加载权重时反复报size mismatch for conv1.weight——不是模型结构写错了是他们把原项目里为室内RGB-D数据集如NYUv2设计的深度估计头直接套用在了自动驾驶场景的纯RGB图像上。这项目真正落地的价值恰恰在于它没走“先检测再配准点云”的老路而是用单阶段YOLO主干同时回归2D框深度偏移量再通过相机内参几何约束反解出带物理尺度的3D框x, y, z, l, w, h, θ。它适合三类人想快速验证3D检测pipeline是否work的算法新人需要在嵌入式设备Jetson Orin上部署轻量级3D感知模块的嵌入式工程师以及正在做机器人抓取位姿估计、但苦于没有标定好的深度传感器的ROS开发者。项目不提供预训练权重但给全了从数据准备含自制标注工具说明、模型微调、到推理可视化的一整套可复现流程——连load_camera_params.py里怎么从一张标定板照片自动提取fx/fy/cx/cy都写了注释。2. 项目结构与技术栈YOLOv5s主干 深度分支双头设计 基于相机几何的3D框解算这个项目不是简单拼接两个模型而是在YOLOv5s的Neck层后分叉出两个任务头一个保持原YOLO的clsreg分支负责2D检测另一个新增的depth_head专门回归每个anchor中心点的相对深度偏移量Δd而非绝对深度值。这种设计大幅降低深度回归难度也规避了单目深度估计中常见的尺度模糊问题。整个流程链路清晰RGB输入 → YOLO主干提取特征 → PANet增强多尺度信息 → 分别送入det_head和depth_head → det_head输出2D框x,y,w,h和类别 → depth_head输出Δd → 结合load_camera_params.py加载的相机内参矩阵K → 调用bbox3d_utils.py中的project_2d_to_3d()函数将2D框四个角点反投影到归一化平面再沿光轴缩放Δd倍 → 得到8个3D顶点 → 拟合最小外接长方体 → 输出带朝向角θ的6DoF框。2.1 核心文件职责与依赖关系项目共9个文件去掉README.md和requirements.txt7个是功能代码。它们不是平级并列而是有明确的调用层级load_camera_params.py唯一可信的相机参数入口。它不读取yaml或json配置而是通过OpenCV的cv2.findChessboardCorners()自动从calib_images/目录下多张棋盘格照片中解算内参fx, fy, cx, cy和畸变系数。输出为camera_params.npz被run.py和detection_model.py共同加载。detection_model.py核心模型定义。继承自torch.nn.Module内部封装YOLOv5s backbonemodels.common.Focusmodels.common.Conv等并在SPPF后接入DepthHead类。注意它的forward()返回的是{det: [...], depth: [...]}字典不是YOLO原生的[bs, anchor, 85]张量。depth_model.py独立存在的深度估计子网络仅用于辅助训练阶段的深度监督如用NYUv2的深度图做L1 loss。它不参与推理这点常被误读——很多用户以为要先跑一遍depth_model.py生成深度图再喂给YOLO实际完全不需要。bbox3d_utils.py3D几何计算核心。包含get_3d_bbox_from_2d()主函数、rotate_3d_bbox()处理yaw角、compute_iou_3d()评估用三个关键函数。所有3D坐标均以相机光心为原点Z轴正向指向场景单位为米。run.py推理入口。它加载detection_model.py权重读取视频/图片调用detection_model.forward()获取2D框和Δd再调用bbox3d_utils.get_3d_bbox_from_2d()完成3D解算最后用cv2.polylines()在原图上绘制3D框透视投影。提示requirements.txt里torch1.12.1cu113和torchvision0.13.1cu113是硬性要求。用1.13以上版本会导致models.common.Focus的Conv层stride2时padding计算错误引发特征图尺寸错位——这是血泪经验不是玄学。2.2 模型结构关键修改点为什么不能直接套用YOLOv5官方代码原版YOLOv5的head输出是[x, y, w, h, obj_conf, cls_conf...]而本项目在detection_model.py中重写了Detect类的forward()方法# detection_model.py 中 Detect 类的 forward 方法节选 def forward(self, x): z [] # 存储检测输出 self.training | self.export for i in range(self.nl): # nl number of detection layers (usually 3) x[i] self.m[i](x[i]) # conv to output tensor bs, _, ny, nx x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85) x[i] x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2) # 关键此处拆分出 depth 分支的输出 # x[i][..., 0:4] - 2D bbox (x,y,w,h) # x[i][..., 4] - obj_conf # x[i][..., 5:-1] - cls_conf (num_classes) # x[i][..., -1] - depth_offset (Δd)单通道标量 depth_offset x[i][..., -1:] # shape: [bs, na, ny, nx, 1] det_output x[i][..., :-1] # shape: [bs, na, ny, nx, 5nc] z.append((det_output, depth_offset)) return z这段代码决定了每个anchor预测的不再是85维向量而是5nc1维最后1维专供深度偏移。如果你直接拿YOLOv5官方权重加载会因维度不匹配而崩溃。项目提供的weights/best.pt是重新训练过的其model.yaml中nc类别数默认为1只检汽车nooutput channels per anchor 5 nc 1 7。若你要加行人、自行车必须同步修改model.yaml和detection_model.py中self.no的计算逻辑。2.3 数据准备不用KITTI不用nuScenes用你手机拍的5张图就能启动项目不强制依赖大型3D数据集。README.md明确说明训练数据只需两类文件——images/下的RGB图jpg/pnglabels/下同名txt文件每行格式为class_id center_x center_y width height depth_offset归一化坐标depth_offset单位为米。其中depth_offset不是绝对深度而是该目标中心点相对于图像平均深度的偏移量。项目提供了tools/generate_pseudo_depth.py未在压缩包列出但源码含此文件它用OpenCV的cv2.solvePnP()基于你手动标注的4个目标角点像素坐标已知物理尺寸如车长4.5m反解出目标中心3D坐标再计算z值作为depth_offset。这意味着你拿手机拍一段停车场视频用labelImg标出车辆2D框再用卷尺量一辆车的真实长宽高就能生成带深度监督的训练样本。没有LiDAR没关系。没有标定板load_camera_params.py支持用任意已知尺寸的矩形物体如A4纸做简易标定。3. 快速运行指南从解压到看到旋转的3D框5分钟内完成别被“3D目标检测”吓住——这个项目最友好的地方是它把所有环境依赖、数据路径、超参都固化在代码里而不是散落在十几个配置文件中。只要你的机器有NVIDIA GPUGTX 1060和CUDA 11.3按以下步骤操作5分钟内必见3D框。3.1 环境搭建避开conda/pip混装的深坑不要用conda install pytorch必须严格按requirements.txt用pip安装# 创建干净虚拟环境推荐python3.8 python -m venv venv_3d source venv_3d/bin/activate # Linux/Mac # venv_3d\Scripts\activate # Windows # 一步到位安装指定CUDA版本的PyTorch关键 pip install torch1.12.1cu113 torchvision0.13.1cu113 --extra-index-url https://download.pytorch.org/whl/cu113 # 安装其余依赖注意opencv-python-headless在无GUI服务器上更稳定 pip install -r requirements.txt # 如果报错找不到libGL.so.1常见于Ubuntu服务器执行 sudo apt-get update sudo apt-get install -y libglib2.0-0 libsm6 libxext6 libxrender-dev libglib2.0-dev注意requirements.txt中opencv-python4.5.5.64是经过实测的稳定版本。用4.8会导致cv2.polylines()在绘制3D框透视投影时出现线条断裂——这是OpenCV的渲染bug不是代码问题。3.2 相机标定3张照片2分钟搞定项目自带标定板图片calib_images/chessboard_*.jpg但你完全可以替换成自己的。标定流程在load_camera_params.py中已封装为函数# load_camera_params.py 第42行起 def calibrate_camera(image_dir: str, pattern_size(9, 6), square_size0.025): pattern_size: 棋盘格内角点行列数如9x6表示9列6行交点 square_size: 每个方格物理边长单位米这里设为2.5cm # ...省略内部实现... # 最终保存 camera_params.npz含 keys: [mtx, dist, rvecs, tvecs] np.savez(camera_params.npz, mtxmtx, distdist, rvecsrvecs, tvecstvecs) print(✅ 相机内参已保存至 camera_params.npz) return mtx, dist # 在终端中直接运行标定 python load_camera_params.py --image_dir calib_images/ --pattern_size 9,6 --square_size 0.025运行后控制台会打印类似✅ 找到12张标定板图像 ✅ 平均重投影误差: 0.18 pixels ✅ 相机内参已保存至 camera_params.npz此时camera_params.npz中mtx数组即为[[fx, 0, cx], [0, fy, cy], [0, 0, 1]]。若你用手机拍摄建议用A4纸210mm×297mm代替棋盘格修改pattern_size(2,3)2列3行角点square_size0.21以宽为基准。3.3 推理演示用自带测试图看3D框如何旋转项目提供test_images/car_001.jpg作为样例。运行前确认两点camera_params.npz已生成weights/best.pt存在若无先运行train.py训10轮或从网盘下载预训练权重。# 单图推理默认输出带3D框的result.jpg python run.py --source test_images/car_001.jpg --weights weights/best.pt --conf 0.4 # 视频流推理需USB摄像头 python run.py --source 0 --weights weights/best.pt --view-img # 关键参数说明 # --conf 0.4 : 2D检测置信度过滤阈值太低会噪点多太高会漏检 # --view-img : 实时显示窗口Linux需X11转发服务器建议关掉 # --save-txt : 生成3D框坐标txt格式class_id x y z l w h theta成功运行后你会看到result.jpg中车辆周围有8个红色顶点连线构成的3D框并随车辆在图像中的位置变化而实时调整透视角度——这不是3D引擎渲染而是纯几何计算OpenCV绘制的结果。4. 避坑指南那些让你debug到凌晨三点的“合理”报错这个项目在GitHub上被fork过200次Issue区高频问题高度集中。以下是5条经实测的踩坑记录按“现象→原因→解决”结构整理每一条都来自真实翻车现场。4.1 现象run.py报错KeyError: conv1.weight原因detection_model.py中模型加载时代码试图从weights/best.pt中读取conv1.weight但该权重文件是用旧版PyTorch1.10保存的其state_dict键名与当前detection_model.py定义的模块名不一致。例如旧版保存为model.0.conv.weight新版期望model.0.cv1.conv.weight。解决不要手改模型定义用项目提供的tools/convert_weights.py转换python tools/convert_weights.py --src weights/old_best.pt --dst weights/best.pt --arch yolov5s_3d该脚本会自动映射键名并重写num_classes字段。4.2 现象3D框严重扭曲像被拉长的橡皮筋原因camera_params.npz中的mtx矩阵数值错误。常见于标定时square_size单位填错如把2.5cm写成2.5而非0.025导致fx/fy放大100倍进而使反投影尺度失真。解决用以下代码校验mtx合理性import numpy as np mtx np.load(camera_params.npz)[mtx] print(ffx{mtx[0,0]:.1f}, fy{mtx[1,1]:.1f}, cx{mtx[0,2]:.1f}, cy{mtx[1,2]:.1f}) # 正常值范围1280x720图像fx/fy ≈ 1000~1500, cx≈640, cy≈360 # 若fx25000立刻检查square_size是否少写了小数点4.3 现象depth_model.py训练时loss不下降始终在0.8左右原因depth_model.py的损失函数默认使用nn.L1Loss()但NYUv2深度图值域为[0,10]米而模型输出被torch.sigmoid()压缩到[0,1]。若你用自己的数据深度值域不同如[0,50]sigmoid会把大深度全部压到0.99附近梯度消失。解决注释掉depth_model.py中forward()末尾的torch.sigmoid()改用nn.Softplus()保证输出0且梯度平滑# 原代码错误 # return torch.sigmoid(x) # 修改后正确 return F.softplus(x) # 输出范围 [0, ∞)适配任意深度值域4.4 现象bbox3d_utils.py中get_3d_bbox_from_2d()返回NaN坐标原因输入的2D框坐标超出图像边界如x0或xwidth导致project_2d_to_3d()中除零Z0时无法反投影。YOLO检测头输出的归一化坐标未做clip。解决在run.py调用get_3d_bbox_from_2d()前强制裁剪2D框# run.py 第89行在 det_boxes ... 后插入 det_boxes[:, 0] np.clip(det_boxes[:, 0], 0, 1) # x det_boxes[:, 1] np.clip(det_boxes[:, 1], 0, 1) # y det_boxes[:, 2] np.clip(det_boxes[:, 2], 0, 1) # w det_boxes[:, 3] np.clip(det_boxes[:, 3], 0, 1) # h # 确保w,h不为0避免后续除零 det_boxes[:, 2] np.maximum(det_boxes[:, 2], 1e-4) det_boxes[:, 3] np.maximum(det_boxes[:, 3], 1e-4)4.5 现象多目标时3D框互相遮挡视觉混乱原因run.py中绘制3D框的顺序是按检测置信度从高到低但高置信度目标可能在远处z值大按此顺序绘制会遮挡近处目标。解决按z坐标从远到近排序再绘制z值越大越远# run.py 第125行替换原绘制循环 # 原代码for i, (box2d, box3d) in enumerate(zip(det_boxes, det_3d_boxes)): # 修改为 z_order np.argsort([b[2] for b in det_3d_boxes])[::-1] # 降序z大者优先绘制在底层 for i in z_order: box2d, box3d det_boxes[i], det_3d_boxes[i] # 绘制逻辑不变...5. 二次开发实战如何把“检测汽车”改成“检测机械臂末端夹具”并输出ROS坐标系位姿这才是项目真正的价值所在——它不是一个玩具demo而是一个可工程化的3D感知基座。上周我帮某自动化公司改造此项目目标是让机械臂在无深度相机情况下仅凭单目RGB识别夹具位置并输出geometry_msgs/PoseStamped消息。整个过程只改了3个文件不到200行代码下面拆解关键动作。5.1 数据层面从“汽车”到“夹具”的标注范式迁移原项目标注labels/car_001.txt格式为0 0.45 0.62 0.28 0.41 2.35 # class_id x y w h depth_offset但夹具是刚性物体其3D姿态x,y,z,roll,pitch,yaw比单纯深度更重要。我们扩展标注格式在每行末尾追加roll pitch yaw单位弧度0 0.45 0.62 0.28 0.41 0.15 0.02 -0.01 0.78 # 新增3个角度对应地修改detection_model.py中Detect.forward()让depth_head输出维度从1变为4# detection_model.py 第155行修改 depth_offset 的提取逻辑 # 原depth_offset x[i][..., -1:] # 改为 pose_offset x[i][..., -4:] # [Δz, Δroll, Δpitch, Δyaw] det_output x[i][..., :-4] # det head 输出维度相应减少这样模型就从回归“深度偏移”升级为回归“6DoF姿态偏移”。5.2 模型层面用旋转向量替代欧拉角规避万向节锁直接回归欧拉角roll,pitch,yaw在pitch接近±90°时会出现万向节锁Gimbal Lock导致梯度爆炸。工业场景中夹具常处于竖直状态pitch≈90°。解决方案是改用旋转向量rotation vector它用3维向量表示绕任意轴的旋转无奇点# bbox3d_utils.py 新增函数 def rotvec_to_euler(rotvec): 将旋转向量转为欧拉角用于最终输出 from scipy.spatial.transform import Rotation r Rotation.from_rotvec(rotvec) return r.as_euler(xyz, degreesFalse) # 返回 [roll, pitch, yaw] # 在 get_3d_bbox_from_2d() 中当拿到 pose_offset 后 z_pred pose_offset[0] # Δz rotvec_pred pose_offset[1:] # [Δrx, Δry, Δrz] euler_pred rotvec_to_euler(rotvec_pred) # 转为欧拉角用于显示 # 但训练时loss仍用 rotvec 计算保证数学稳定性5.3 部署层面对接ROS发布PoseStamped消息run.py本身不依赖ROS我们新建ros_bridge.py作为胶水层# ros_bridge.py import rospy from geometry_msgs.msg import PoseStamped from std_msgs.msg import Header class PosePublisher: def __init__(self): self.pub rospy.Publisher(/3d_pose, PoseStamped, queue_size10) rospy.init_node(3d_pose_publisher, anonymousTrue) def publish_pose(self, x, y, z, qx, qy, qz, qw): pose_msg PoseStamped() pose_msg.header Header() pose_msg.header.stamp rospy.Time.now() pose_msg.header.frame_id camera_link pose_msg.pose.position.x x pose_msg.pose.position.y y pose_msg.pose.position.z z pose_msg.pose.orientation.x qx pose_msg.pose.orientation.y qy pose_msg.pose.orientation.z qz pose_msg.pose.orientation.w qw self.pub.publish(pose_msg) # 在 run.py 的推理循环末尾调用 if __name__ __main__: pose_pub PosePublisher() while True: # ... 推理得到 det_3d_boxes ... for box3d in det_3d_boxes: x, y, z, l, w, h, theta box3d # 将theta转为四元数简化版实际用tf.transformations qx, qy, qz, qw 0, 0, np.sin(theta/2), np.cos(theta/2) pose_pub.publish_pose(x, y, z, qx, qy, qz, qw)编译后rostopic echo /3d_pose即可看到实时位姿流。从检测到发布端到端延迟120msRTX 3060满足机械臂闭环控制需求。从那以后我每次接到“单目3D位姿估计”需求都先跑一遍这个项目框架再按上述三步改造——它省去了从零搭模型、写数据加载、调几何反投影的80%工作量。希望帮到你。本文还有配套的精品资源点击获取