ur5_camera_bringup.launch.py
PY 7.94 KB
object_detector.py
PY 5.94 KB
🎯 **第10周任务确认完成!**
你已经实现了**物体识别节点**,能够从 `/camera/image_raw` 稳定提取红色方块的像素坐标。这是视觉系统的"眼睛",下一步要赋予它"空间感知能力"——**将2D像素转换为3D空间位姿**。
---
## 📍 **第11周任务:位姿估计 + 手眼标定简化版**
### **核心目标**
实现 `pose_estimator.py` 节点,完成 **像素坐标 → 相机坐标 → 机械臂基坐标** 的转换,输出物体在 `base_link` 系下的 (x, y, z, qx, qy, qz, qw)。
### **关键挑战**
```
pixel (u,v)
↓ [相机内参 + 深度假设]
camera_optical_frame (Xc, Yc, Zc)
↓ [TF2变换]
base_link (Xb, Yb, Zb) ← MoveIt!需要这个!
```
---
### **1. 数学基础(必须理解)**
#### **1.1 相机成像模型**
```
u = fx * Xc/Zc + cx
v = fy * Yc/Zc + cy
```
**反推相机坐标系**(假设物体在桌面,Z已知):
```python
# 已知:像素坐标 (u,v),相机内参 (fx, fy, cx, cy)
# 假设:物体高度 Zc = 1.0m(桌面高度,后续会优化)
Xc = (u - cx) * Zc / fx
Yc = (v - cy) * Zc / fy
Zc = 1.0 # 预设值
```
#### **1.2 坐标系变换**
```
P_base = R_base_camera * P_camera + t_base_camera
```
- `R`: 旋转矩阵(3x3)
- `t`: 平移向量(3x1)
在ROS中用 `tf2_ros.Buffer.lookup_transform()` 自动完成。
---
### **2. 创建位姿估计节点**
```bash
cd ~/vision_ws/src/vision_grasp_demo/src/
touch pose_estimator.py
chmod +x pose_estimator.py
```
**完整代码实现**:
```python
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PoseStamped, Pose
from sensor_msgs.msg import CameraInfo
import tf2_ros
import numpy as np
class PoseEstimator(Node):
def __init__(self):
super().__init__('pose_estimator')
# 参数:物体高度(桌面到物体中心的距离)
self.declare_parameter('object_height', 0.05) # 5cm高的方块
self.declare_parameter('camera_frame', 'camera_link_optical')
self.declare_parameter('target_frame', 'base_link')
# TF2
self.tf_buffer = tf2_ros.Buffer()
self.tf_listener = tf2_ros.TransformListener(self.tf_buffer, self)
# 相机内参(从CameraInfo话题获取)
self.camera_info = None
self.create_subscription(
CameraInfo, '/camera/camera_info', self.camera_info_callback, 10
)
# 订阅物体检测结果
self.create_subscription(
PoseStamped, '/detection/object_pose_pixel', self.pixel_pose_callback, 10
)
# 发布3D位姿
self.pose_pub = self.create_publisher(PoseStamped, '/detection/object_pose_3d', 10)
self.get_logger().info('PoseEstimator节点已启动 - 等待相机参数和物体像素坐标...')
def camera_info_callback(self, msg):
"""缓存相机内参"""
if self.camera_info is None:
self.camera_info = msg
self.get_logger().info(f'相机参数已获取: fx={msg.k[0]:.2f}, fy={msg.k[4]:.2f}')
def pixel_pose_callback(self, msg):
"""核心:像素坐标 → 3D位姿"""
if self.camera_info is None:
self.get_logger().warn('等待相机参数中...')
return
# 1. 提取像素坐标
pixel_u = msg.pose.position.x # 复用x,y字段存储像素坐标
pixel_v = msg.pose.position.y
# 2. 相机内参
fx, fy = self.camera_info.k[0], self.camera_info.k[4]
cx, cy = self.camera_info.k[2], self.camera_info.k[5]
# 3. 预设深度(关键简化:物体在桌面,高度已知)
object_height = self.get_parameter('object_height').value
# 相机离地高度约0.8m,物体高度0.05m,所以Zc≈0.75m
Zc = 0.75 # 根据你的场景调整
# 4. 像素 → 相机坐标系
Xc = (pixel_u - cx) * Zc / fx
Yc = (pixel_v - cy) * Zc / fy
self.get_logger().debug(f'相机坐标: ({Xc:.3f}, {Yc:.3f}, {Zc:.3f})')
# 5. 相机坐标系 → base_link
try:
# 获取 TF: base_link ← camera_optical_frame
transform = self.tf_buffer.lookup_transform(
self.get_parameter('target_frame').value,
self.get_parameter('camera_frame').value,
rclpy.time.Time()
)
# 提取旋转和平移
t = transform.transform.translation
q = transform.transform.rotation
# 四元数 → 旋转矩阵
R = self.quaternion_to_rotation_matrix([q.x, q.y, q.z, q.w])
# 应用变换: P_base = R * P_camera + t
P_camera = np.array([Xc, Yc, Zc])
P_base = R @ P_camera + np.array([t.x, t.y, t.z])
self.get_logger().info(
f'物体在base_link下的位姿: x={P_base[0]:.3f}, y={P_base[1]:.3f}, z={P_base[2]:.3f}'
)
# 6. 发布PoseStamped
pose_msg = PoseStamped()
pose_msg.header.frame_id = self.get_parameter('target_frame').value
pose_msg.header.stamp = self.get_clock().now().to_msg()
pose_msg.pose.position.x = P_base[0]
pose_msg.pose.position.y = P_base[1]
pose_msg.pose.position.z = object_height # 物体中心高度
# 朝向:默认朝上(可根据物体方向调整)
pose_msg.pose.orientation.x = 0.0
pose_msg.pose.orientation.y = 0.0
pose_msg.pose.orientation.z = 0.0
pose_msg.pose.orientation.w = 1.0
self.pose_pub.publish(pose_msg)
except tf2_ros.LookupException as e:
self.get_logger().error(f'TF变换失败: {str(e)}')
self.get_logger().warn('请确认: 1) URDF中定义了camera_link_optical 2) robot_state_publisher已运行')
@staticmethod
def quaternion_to_rotation_matrix(q):
"""四元数 → 3x3旋转矩阵"""
x, y, z, w = q
# 归一化
norm = np.sqrt(x*x + y*y + z*z + w*w)
x, y, z, w = x/norm, y/norm, z/norm, w/norm
# 计算旋转矩阵
R = np.array([
[1 - 2*y*y - 2*z*z, 2*x*y - 2*z*w, 2*x*z + 2*y*w],
[2*x*y + 2*z*w, 1 - 2*x*x - 2*z*z, 2*y*z - 2*x*w],
[2*x*z - 2*y*w, 2*y*z + 2*x*w, 1 - 2*x*x - 2*y*y]
])
return R
def main(args=None):
rclpy.init(args=args)
estimator = PoseEstimator()
try:
rclpy.spin(estimator)
except KeyboardInterrupt:
estimator.get_logger().info('节点已停止')
finally:
estimator.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
```
---
### **3. 修改 `object_detector.py` 以发布像素坐标**
在 `object_detector.py` 的 `image_callback` 中,识别成功后发布:
```python
from geometry_msgs.msg import PoseStamped, Pose
# 在 __init__ 中添加
self.pixel_pose_pub = self.create_publisher(PoseStamped, '/detection/object_pose_pixel', 10)
# 在 detect_object() 返回后,添加:
if detection:
# ... 原有绘制代码 ...
# 新增:发布像素坐标
pixel_msg = PoseStamped()
pixel_msg.header.frame_id = 'camera_optical_frame'
pixel_msg.header.stamp = self.get_clock().now().to_msg()
pixel_msg.pose.position.x = float(detection['center'][0])
pixel_msg.pose.position.y = float(detection['center'][1])
pixel_msg.pose.position.z = 0.0 # 像素坐标无深度
self.pixel_pose_pub.publish(pixel_msg)
```
---
### **4. 更新 CMakeLists.txt**
```cmake
install(PROGRAMS
src/object_detector.py
src/pose_estimator.py # 新增
DESTINATION lib/${PROJECT_NAME}
)
```
---
### **5. 创建配置文件**
`config/pose_estimation.yaml`:
```yaml
pose_estimator:
ros__parameters:
object_height: 0.05 # 物体高度5cm
camera_frame: 'camera_link_optical'
target_frame: 'base_link'
```
---
### **6. 更新启动文件**
在 `ur5_camera_bringup.launch.py` 中添加:
```python
# 在 launch_setup 中添加
pose_estimator_node = Node(
package='vision_grasp_demo',
executable='pose_estimator.py',
name='pose_estimator',
output='screen',
parameters=[PathJoinSubstitution([
FindPackageShare('vision_grasp_demo'), 'config', 'pose_estimation.yaml'
])]
)
# 在 nodes_to_start 中追加
pose_estimator_node,
```
---
### **7. 编译 & 测试**
```bash
cd ~/vision_ws
colcon build --packages-select vision_grasp_demo --symlink-install
source install/setup.bash
# 启动完整系统
ros2 launch vision_grasp_demo ur5_camera_bringup.launch.py
# 新终端:验证位姿输出
ros2 topic echo /detection/object_pose_3d
# 在Rviz中可视化
# 添加 PoseArray 或 Marker 显示 /detection/object_pose_3d
```
---
### **8. 验证与调优**
**验证TF树是否正确:**
```bash
ros2 run tf2_tools view_frames.py
# 确认存在: base_link → ... → camera_link_optical 的链路
```
**手动测量验证:**
```bash
# 在Gazebo中,右键点击红色方块 → View → Link Inspector
# 记录真实位置 (x, y, z)
# 对比你的 /detection/object_pose_3d 输出
# 误差应在 ±2cm 内
```
**调参策略:**
```python
# 如果X/Y方向偏差大:调整 Zc 值
Zc = 0.75 # 尝试 0.7 ~ 0.8 之间调整
# 如果深度方向偏差大:检查TF变换
# 确认 camera_link_optical 的朝向是否正确(Z轴向前)
```
---
### **9. 本周交付物清单**
- [ ] `src/pose_estimator.py` **核心位姿计算代码**(含四元数转旋转矩阵)
- [ ] `config/pose_estimation.yaml` **参数配置文件**
- [ ] **Rviz截图**(显示 `/detection/object_pose_3d` 的Marker在方块上)
- [ ] **误差分析表**(至少3个不同位置,记录真实值 vs 估计值)
---
### **10. 技术亮点(简历素材)**
```markdown
## 技术报告摘录:位姿估计简化方案
**问题**:完整手眼标定需9点法,项目周期不允许。
**创新**:采用"预设高度+静态TF"的轻量化方案:
1. 利用Gazebo已知环境,假设物体在Z=0.75m平面
2. 通过tf2_ros自动获取相机到基座的变换
3. 四元数转旋转矩阵实现坐标变换
**效果**:定位误差±2cm(满足抓取容差)
**代价**:无法适应物体高度变化(未来改进方向)
```
---
## 🎯 **下周预告:第12周 · MoveIt!抓取闭环**
你将实现:
- **MoveIt 2集成**:配置UR5运动规划组
- **碰撞场景**:添加桌面和障碍物
- **抓取策略**:预抓取 → 接近 → 闭合 → 提升
- **完整闭环**:识别 → 规划 → 执行 → 放置
**下周交付物将包含完整的视频演示**,这是你简历上最硬核的证明。
---
💪 **现在,去实现坐标变换节点吧!记住:四元数转旋转矩阵的代码要理解透,面试99%会问。**
**完成后请回复:"第11周任务完成,位姿估计误差±2cm内",并附上Rviz可视化截图。**
以上是我的教程,请给我讲解:1. 数学基础(必须理解)
ROS2视觉抓取教程数学基础讲解
用户要求我讲解教程中的"1. 数学基础(必须理解)"部分。这是一个ROS 2视觉抓取项目教程,用户需要我详细解释其中的数学原理。
我需要:
- 仔细理解教程中提到的数学概念
- 用清晰、易于理解的方式讲解
- 确保解释准确且深入
- 使用中文进行讲解,因为教程是中文的
教程中数学基础部分包含两个关键内容:
- 相机成像模型(针孔相机模型)
- 坐标系变换(旋转和平移)
让我逐部分分析并准备讲解内容:
1.1 相机成像模型
- 给出了前向投影公式:u = fx * Xc/Zc + cx, v = fy * Yc/Zc + cy
- 给出了反推公式(已知像素和深度求3D坐标)
- 涉及相机内参:fx, fy, cx, cy
- 涉及坐标系:像素坐标系和相机坐标系
1.2 坐标系变换
- 给出了变换公式:P_base = R_base_camera * P_camera + t_base_camera
- 涉及旋转矩阵和平移向量
- 提到ROS TF2工具可以自动完成这个变换
我需要:
- 用图示或形象比喻帮助理解
- 解释每个参数的物理意义
- 说明为什么需要这些变换
- 补充必要的背景知识
- 保持与教程一致但更深入的理解
用户没有存储记忆的必要,这只是知识讲解请求。
我来为你深度拆解这两块核心数学,这是整个视觉抓取系统的理论根基。
📐 1.1 相机成像模型(从3D到2D的投影)
物理意义:小孔成像的现代数学表达
想象相机是一个黑盒子,前方有个小孔(镜头光心),光线穿过小孔在底片(图像传感器)上成像。现代相机用数学精确描述这个过程:
plain
复制
(Xc, Yc, Zc) 物体在相机坐标系
↓
光心O
↘
sensor平面 (u, v)正向投影公式:
plain
复制
u = fx * Xc/Zc + cx
v = fy * Yc/Zc + cy每个参数的物理意义:
(Xc, Yc, Zc):物体在相机坐标系下的3D坐标(单位:米)- Zc:物体到相机平面的深度(最关键!)
- 为什么Zc在分母?近大远小的数学体现:物体越远,成像越小
fx, fy:焦距(单位:像素)- fx = 焦距(mm) × 传感器宽度(像素/mm)
- 控制缩放比例:fx越大,视野越窄,放大倍率越高
cx, cy:主点偏移(图像中心)- 理想情况下是图像中心 (image_width/2, image_height/2)
- 由于传感器装配误差,会有微小偏移
(u, v):投影到图像上的像素坐标
直观理解:
u = fx * Xc/Zc + cx 这个公式可以拆解为:Xc/Zc:将3D坐标归一化到Z=1的平面(单位化深度)fx * ...:按焦距缩放+ cx:平移到图像中心
逆向推导:从2D像素恢复3D坐标(我们的核心任务)
plain
复制
已知:像素坐标 (u,v) + 假设深度 Zc
求解:相机坐标系下的 (Xc, Yc, Zc)反推公式:
Python
复制
Xc = (u - cx) * Zc / fx
Yc = (v - cy) * Zc / fy为什么必须假设Zc?
这是单目相机的天生缺陷:2D图像丢失深度信息。一个远处分和近处小物体可能投影到完全相同的像素位置。
这是单目相机的天生缺陷:2D图像丢失深度信息。一个远处分和近处小物体可能投影到完全相同的像素位置。
我们的简化策略:
Python
复制
Zc = 0.75 # 预设物体在桌面,距离相机0.75米这个值怎么来的?
Python
复制
相机离地高度 ≈ 0.8m
物体高度 ≈ 0.05m
Zc = 0.8 - 0.05 = 0.75m误差分析:
- 如果真实Zc=0.75m,估计的Xc/Yc完全准确
- 如果物体高度变化±2cm,会导致Zc误差±2cm,进而导致Xc/Yc误差约 3%(与fx/fy有关)
- 抓取容差通常是±5cm,所以可以接受
🔄 1.2 坐标系变换(从相机到机械臂)
为什么需要坐标系变换?
相机看到物体在
(Xc, Yc, Zc),但机械臂需要的是相对于自己基座的 (Xb, Yb, Zb)。两者不在同一个"语言体系"。plain
复制
base_link (机械臂基座)
↑ [R, t] 变换
camera_link_optical (相机光心)
↑ 相机成像模型
像素平面 (u, v)数学公式:刚体变换
plain
复制
P_base = R_base_camera * P_camera + t_base_camera- P_base:物体在基座坐标系下的坐标 (3×1向量)
- P_camera:物体在相机坐标系下的坐标 (3×1向量)
- R_base_camera:从相机到基座的 3×3旋转矩阵
- t_base_camera:从相机到基座的 3×1平移向量
旋转矩阵 R 的物理意义
R是一个正交矩阵(R^T * R = I),每一列代表一个轴的方向:
plain
复制
R = [ [r11, r12, r13],
[r21, r22, r23],
[r31, r32, r33] ]
其中:
[r11, r21, r31]^T = 相机X轴在基座坐标系下的单位向量
[r12, r22, r32]^T = 相机Y轴在基座坐标系下的单位向量
[r13, r23, r33]^T = 相机Z轴在基座坐标系下的单位向量为什么用四元数?
ROS TF2内部用四元数
ROS TF2内部用四元数
[qx, qy, qz, qw] 表示旋转,因为:- 避免万向节死锁(Gimbal Lock)
- 比旋转矩阵更紧凑(4个数 vs 9个数)
- 插值更平滑
四元数 → 旋转矩阵的代码解析
Python
复制
@staticmethod
def quaternion_to_rotation_matrix(q):
x, y, z, w = q
# 归一化:确保四元数是单位四元数
norm = np.sqrt(x*x + y*y + z*z + w*w)
x, y, z, w = x/norm, y/norm, z/norm, w/norm
# 计算公式(源自数学推导)
R = np.array([
[1 - 2*y*y - 2*z*z, 2*x*y - 2*z*w, 2*x*z + 2*y*w],
[2*x*y + 2*z*w, 1 - 2*x*x - 2*z*z, 2*y*z - 2*x*w],
[2*x*z - 2*y*w, 2*y*z + 2*x*w, 1 - 2*x*x - 2*y*y]
])
return R这个公式是怎么来的?
四元数
四元数
q = w + xi + yj + zk,旋转矩阵R可以通过四元数的乘法规则推导得出。记住几个关键性质:w控制旋转角度:cos(θ/2)(x, y, z)控制旋转轴:sin(θ/2) * axis- 当
w=1, x=y=z=0时,R = 单位矩阵(无旋转)
🔧 整合流程:从像素到基座坐标
完整的数据流:
Python
复制
# 输入: 像素坐标 (u, v)
# 步骤1: 相机反投影
Xc = (u - cx) * Zc / fx
Yc = (v - cy) * Zc / fy
Zc = 0.75 # 假设
# 步骤2: TF2获取变换
transform = lookup_transform('base_link', 'camera_link_optical')
t = transform.translation
q = transform.rotation
# 步骤3: 四元数转旋转矩阵
R = quaternion_to_rotation_matrix([q.x, q.y, q.z, q.w])
# 步骤4: 刚体变换
P_camera = np.array([Xc, Yc, Zc])
P_base = R @ P_camera + np.array([t.x, t.y, t.z])
# 输出: 基座坐标系下的3D位置⚠️ 常见面试考点(必背)
- 为什么单目相机不能直接从单张图像恢复深度?
- 答:尺度歧义性(Scale Ambiguity),2D投影丢失了深度信息。
- 四元数相比欧拉角的优点?
- 答:避免万向节死锁,计算更高效,插值更平滑。
- 如果估计的X/Y坐标误差很大,可能原因?
- 答:Zc假设错误 → 导致
Xc = (u-cx)*Zc/fx计算错误;或者相机标定不准(fx/fy, cx/cy误差)。
- TF2中
lookup_transform(target, source)的含义?- 答:返回的是
source → target的变换,即P_target = R * P_source + t。
理解这套数学,你就掌握了计算机视觉三维重建的入门钥匙。下周的MoveIt!抓取闭环,就是把这些坐标发给机械臂控制器,让它动起来。有任何公式推导的细节疑问,随时追问!