好问题!其实这个项目里 YOLO 仍然只做了 **2D 检测**,所谓"3D"是通过 **YOLO 给的 2D 像素坐标 + 深度图 + 相机内参** 反投影出来的。下面把关键代码拎出来讲清楚。
核心思路:单目深度相机的"伪 3D 检测"
不是 YOLO 输出 3D,而是 YOLO 给 `(u, v)` 像素坐标,深度图给该像素的 `Z`,内参 `K` 用来反解 `X, Y`。三者合起来才是 3D 坐标。
数学公式(针孔相机反投影):
```
X\] \[u
Y = K⁻¹ · v · Z
Z\] \[1
```
三段关键代码
1) 拿到相机内参并求逆
obj_detect.py:48-49
```python
def caminfo_callback(self, msg):
K = np.array(msg.k).reshape(3, 3)
self.depth_instrinsic_inv = np.linalg.inv(K)
```
`CameraInfo.k` 是 3×3 内参矩阵(fx, fy, cx, cy 等),这里直接缓存 `K⁻¹`,后面反投影要用。
2) YOLO 给出 2D 框,取中心像素 (cx, cy)
obj_detect.py:79-86
```python
results = self.model.predict(img0, verbose=False)
res = results0
boxes = res.boxes.xyxy.cpu().numpy() # YOLO 只输出 2D 框 (x1,y1,x2,y2)
...
for (x1, y1, x2, y2), conf, cls_id in zip(boxes, confs, clses):
cx, cy = int((x1 + x2) / 2), int((y1 + y2) / 2) # 框中心像素
```
注意 YOLO 这里输出的就是 2D `xyxy`,**没有任何深度信息**。
3) 用深度图查 Z,用 K⁻¹ 反投影成 3D
obj_detect.py:99-104
```python
深度估计
Z = depcy, cx * 1e-3 # 深度图里查出来的距离,单位 mm → m
if Z <= 0 or np.isnan(Z):
continue
uv1 = np.array(cx, cy, 1.0)
XYZ = self.depth_instrinsic_inv @ uv1 * Z # 反投影成 3D
```
这就是整个"3D 检测"的核心:
-
`dep` 是 obj_detect.py:36订阅的 `/depth_registered/image_rect` 深度图(与彩色图配准过,所以像素一一对应)。
-
`depcy, cx` 是 YOLO 框中心像素对应的深度值(毫米),`* 1e-3` 转米。
-
`K⁻¹ @ u,v,1` 得到的是相机光轴方向上的单位射线(Z=1 时的 X、Y),乘以 `Z` 就还原成真实 3D 坐标。
-
`XYZ0/1/2` 分别就是相机光学坐标系下的 `X, Y, Z`。
随后塞进 `ObjectHypothesisWithPose.pose.pose.position`,发布到 `/detection` 话题,下游 det_tf.py直接拿这个 position 发 TF。
为什么这样做有意义
| 方式 | 是否真 3D 检测 | 说明 |
|------|----------------|------|
| 单纯 YOLO | ❌ | 只有 2D 像素框 |
| YOLO + 深度图 + 内参反投影 | ⚠️ **半 3D** | 有 3D 位置,但没有 3D 朝向/姿态 |
| 真正的 3D 检测网络(PointNet、VoxelNeXt、SMOKE) | ✅ | 直接从点云或图像回归 6DoF 位姿 |
| YOLO + 深度 + ICP/PnP 配准 | ✅ | 有完整 6DoF 位姿 |
这个项目走的是第二种「半 3D」路线------**只要 3D 位置(XYZ)就够抓取了**,因为:
-
抓取的物体是方块,对称性高,姿态不重要。
-
det_tf.py:46里 `t.transform.rotation.w = 1.0`------发布的 TF 姿态是单位四元数,机器人末端姿态由 MoveIt 自己定,不需要物体姿态。
-
简单可靠,不需要训练 3D 检测网络,复用成熟的 YOLO 模型即可。
关键依赖:深度图必须与彩色图对齐
obj_detect.py:25默认订阅的是 **`/depth_registered/image_rect`**(注意 "registered" 这个词):
```python
self.declare_parameter("depth_topic", "/depth_registered/image_rect")
```
也就是说,**深度图必须先被配准到彩色相机的视角**,让深度图与彩色图像素一一对应。这正是项目里 register_depth.launch.pyy#L64-L74) 中的 `depth_image_proc::RegisterNode` 做的事:
```python
launch_ros.descriptions.ComposableNode(
package='depth_image_proc',
plugin='depth_image_proc::RegisterNode',
name='register_node',
remappings=[('depth/image_rect', '/depth/image_raw'),
('depth/camera_info', '/depth/camera_info'),
('rgb/camera_info', '/color/camera_info'),
('depth_registered/image_rect', '/depth_registered/image_rect'), # ← obj_detect 订阅这个
('depth_registered/camera_info', '/depth_registered/camera_info')]
),
```
如果深度图没配准:
-
YOLO 在彩色图上找到框中心 `(cx, cy)`。
-
用这个 `(cx, cy)` 去深度图里查深度 → 查到的可能是另一个物体的深度,**3D 坐标完全错位**。
-
所以"registered"这步是这个 2D→3D 反投影方案能成立的前提。
整条链路总结
```
彩色图 ──► YOLO ──► 2D 框 (cx, cy) ┐
深度图 ──► RegisterNode ──► 配准深度图 │ depcy, cx → Z
├──┐
CameraInfo (K) ──► K⁻¹ │ │
┘ │
▼
XYZ = K⁻¹ · u,v,1 · Z
│
▼
/detection (ObjectHypothesisWithPose)
│
▼
det_tf.py 发布 TF: cube1/cube2/...
│
▼
MoveIt 抓取
```
所以严格讲,这个项目并没有做"3D 目标检测",而是做了 **"2D 检测 + 深度反投影"的工程化组合**------这是机器人抓取场景里最常用、性价比最高的方案,因为:
-
不需要标注 6DoF 位姿数据集(成本高);
-
不需要训练 3D 检测网络(算力高);
-
只要有深度相机,任何 2D 检测器都能升级成"3D 定位器"。
唯一的前提是:**深度图必须和彩色图配准**,且**深度图在该像素处必须有有效值**(这就是 obj_detect.py:101那行 `if Z <= 0 or np.isnan(Z): continue` 的意义------防止深度图空洞导致 NaN 坐标传给机器人)。