New Chat
Ctrl
K
  • Websites
  • Docs
  • Slides
  • Sheets
  • Deep Research
Kimi Code Kimi Claw
Chat History
  • Mobile App
  • About Us
    • Visit Moonshot AI
    • Open Platform
    • Features
    • Terms of Service
    • Privacy Policy
  • Language
  • User Feedback

pose_estimator 代码解析

py

ur5_camera_bringup.launch.py

PY 7.94 KB
py

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. 仔细理解教程中提到的数学概念
  2. 用清晰、易于理解的方式讲解
  3. 确保解释准确且深入
  4. 使用中文进行讲解,因为教程是中文的
教程中数学基础部分包含两个关键内容:
  1. 相机成像模型(针孔相机模型)
  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
Copy
          (Xc, Yc, Zc) 物体在相机坐标系
                ↓
              光心O
                ↘
                  sensor平面 (u, v)
正向投影公式:
plain
Copy
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 这个公式可以拆解为:
  1. Xc/Zc:将3D坐标归一化到Z=1的平面(单位化深度)
  2. fx * ...:按焦距缩放
  3. + cx:平移到图像中心

逆向推导:从2D像素恢复3D坐标(我们的核心任务)

plain
Copy
已知:像素坐标 (u,v) + 假设深度 Zc
求解:相机坐标系下的 (Xc, Yc, Zc)
反推公式:
Python
Copy
Xc = (u - cx) * Zc / fx
Yc = (v - cy) * Zc / fy
为什么必须假设Zc?
这是单目相机的天生缺陷:2D图像丢失深度信息。一个远处分和近处小物体可能投影到完全相同的像素位置。
我们的简化策略:
Python
Copy
Zc = 0.75  # 预设物体在桌面,距离相机0.75米
这个值怎么来的?
Python
Copy
相机离地高度 ≈ 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
Copy
base_link (机械臂基座)
    ↑  [R, t] 变换
camera_link_optical (相机光心)
    ↑  相机成像模型
像素平面 (u, v)

数学公式:刚体变换

plain
Copy
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
Copy
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内部用四元数 [qx, qy, qz, qw] 表示旋转,因为:
  • 避免万向节死锁(Gimbal Lock)
  • 比旋转矩阵更紧凑(4个数 vs 9个数)
  • 插值更平滑

四元数 → 旋转矩阵的代码解析

Python
Copy
@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
Copy
# 输入: 像素坐标 (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位置

⚠️ 常见面试考点(必背)

  1. 为什么单目相机不能直接从单张图像恢复深度?
    • 答:尺度歧义性(Scale Ambiguity),2D投影丢失了深度信息。
  2. 四元数相比欧拉角的优点?
    • 答:避免万向节死锁,计算更高效,插值更平滑。
  3. 如果估计的X/Y坐标误差很大,可能原因?
    • 答:Zc假设错误 → 导致 Xc = (u-cx)*Zc/fx 计算错误;或者相机标定不准(fx/fy, cx/cy误差)。
  4. TF2中 lookup_transform(target, source) 的含义?
    • 答:返回的是 source → target 的变换,即 P_target = R * P_source + t。

理解这套数学,你就掌握了计算机视觉三维重建的入门钥匙。下周的MoveIt!抓取闭环,就是把这些坐标发给机械臂控制器,让它动起来。有任何公式推导的细节疑问,随时追问!