欢迎光临
我们一直在努力

坐标转换与自瞄整体思路

坐标转换与自瞄整体思路

本文基于当前仓库中的实际源码和参数编写,目标是先建立直觉,再把代码串起来。
重点不是背公式,而是始终回答三个问题:目标现在在哪里、子弹到达时它会在哪里、云台应该转到哪里。

1. 先用一句话理解整套系统

自瞄是一个带反馈的闭环:

相机拍图
→ 检出装甲板二维角点
→ PnP 求装甲板在相机前方的三维位置
→ TF 转到不随云台转动的 odom 坐标系
→ 跟踪并预测目标未来状态
→ 选择要打的装甲板
→ 计算 yaw / pitch,并补偿弹道
→ 下发绝对云台角和开火建议
→ 下位机回传真实云台姿态和弹速
→ 更新 TF,进入下一帧

可以把它拆成三个问题:

  • 看见:图像里的这块板在三维空间哪里?——检测、分类、PnP。
  • 看稳:云台和敌人都在动,敌人的真实运动状态是什么?——TF、数据关联、EKF。
  • 打中:子弹飞到时打哪块板,枪口应指向哪里?——预测、选板、弹道补偿、开火判断。

  • 2. 坐标系到底是什么

    坐标系可以理解成一把带方向的三维尺子。同一个物理点没有变,换了一把尺子测量,三个坐标值就可能完全不同。

    例如,目标始终停在场地中的同一处:

    • 云台向左转时,目标会移动到图像右边,因此目标的相机坐标变了;
    • 目标在场地里没有动,因此正确变换后的 odom 坐标应该基本不变。

    这正是为什么不能直接拿 PnP 的相机坐标做跨帧跟踪。

    2.1 本项目中的主要坐标和坐标轴

    坐标/数据轴方向或含义是否随云台转动
    图像像素 (u,v) u 向右,v 向下;这是二维像素,不是三维米制坐标 随图像变化
    camera_optical_frame X 向右,Y 向下,Z 沿光轴向前
    camera_link ROS 机体系约定:X 向前,Y 向左,Z 向上
    gimbal_link 云台/枪口安装所依附的机体系,X 向前、Y 向左、Z 向上
    odom 本项目用于跟踪的惯性参考系,通常在启动时建立 否;云台姿态相对它变化
    装甲板物体系 模型点固定在装甲板上;当前模型点的第一维为 0,板面位于其 YZ 平面 随装甲板运动

    注意:图像的 (u,v) 和光学坐标系的 (X,Y,Z) 都有“右、下”的方向,但前者单位是像素、只有二维,后者单位是米、是三维,不能混为一谈。

    2.2 本项目的 TF 树

    odom
    └── gimbal_link 动态 TF:下位机实时回传 roll/pitch/yaw
    └── camera_link 静态 TF:相机相对云台的安装平移和倾斜
    └── camera_optical_frame
    静态 TF:把 X前Y左Z上 重排为 X右Y下Z前

    对应代码和参数:

    • 动态 odom → gimbal_link:rm_serial_driver/src/serial_driver_node.cpp
    • 静态 gimbal_link → camera_link:launch_params.yaml 中的 odom2camera 参数,名称虽然叫 odom2camera,实际被填进了 camera_joint
    • 静态 camera_link → camera_optical_frame:rm_gimbal.urdf.xacro

    当前安装参数为:

    xyz: 0.246 0.0 0.049
    rpy: 0.00 0.275 0.015

    它表示相机坐标原点相对 gimbal_link 原点的安装偏移和安装倾角。xyz 的单位是米,rpy 的单位是弧度。

    2.3 光学系为什么还要旋转一次

    相机算法/OpenCV 常用:

    X 右,Y 下,Z 前

    ROS 机体系常用:

    X 前,Y 左,Z 上

    URDF 中的固定旋转 rpy=(-π/2, 0, -π/2) 完成了这次轴重排。对应矩阵为:

    R_gimbal_or_cameraLink_from_optical =
    [ 0 0 1 ]
    [-1 0 0 ]
    [ 0 -1 0 ]

    判断一个旋转矩阵如何映射坐标轴,要看它的列:

    光学系 +X(右) → 机体系 -Y(右)
    光学系 +Y(下) → 机体系 -Z(下)
    光学系 +Z(前) → 机体系 +X(前)

    因此相机看到的“前方 4 米” (0,0,4),转换后大致就是云台系的“前方 4 米” (4,0,0)。


    3. 怎样读懂一个 TF 变换

    ROS 中:

    lookupTransform(target_frame, source_frame, time)

    返回的是把 source_frame 中的数据变到 target_frame 的变换,可记为:

    T_target_source

    对一个点:

    p_target = R_target_source · p_source + t_target_source

    其中:

    • R 改变坐标轴方向;
    • t 补偿两个坐标系原点的位置差;
    • 点需要旋转和平移,方向向量只需要旋转。

    本项目从相机光学系到 odom 的完整链为:

    T_odom_optical
    = T_odom_gimbal
    · T_gimbal_cameraLink
    · T_cameraLink_optical

    于是:

    p_odom = T_odom_optical · p_optical

    矩阵相乘顺序不能交换。可以从右往左读:先把光学系数据变到 camera_link,再到 gimbal_link,最后到 odom。

    3.1 一个直观例子

    暂时忽略安装倾角和云台转角,只保留轴重排和相机平移。假设 PnP 得到:

    p_optical = (0.5, 0.3, 4.0) m

    含义是目标在光轴前方 4 m、画面右侧约 0.5 m、画面下方约 0.3 m。轴重排后约为:

    p_gimbal ≈ (4.0, -0.5, -0.3) m

    再加相机相对云台原点的平移 (0.246, 0, 0.049):

    p_gimbal ≈ (4.246, -0.5, -0.251) m

    实车还要继续考虑安装 rpy 和当前云台动态姿态,所以最终数值由 TF2 统一计算,不建议手写分量公式。

    3.2 时间戳为什么和坐标转换同样重要

    相机曝光时云台角度为 yaw₁,等图像处理完后云台可能已经转到 yaw₂。如果用“当前最新姿态”去转换旧图像,就会把目标投到错误的世界位置。

    当前代码有意识地用图像时间戳查询 TF:

    lookupTransform(odom_frame_, "gimbal_link", img_msg->header.stamp, ...)

    串口侧还有 timestamp_offset,用于校正下位机反馈和相机时间之间的偏差。时间同步不准的典型表现是:云台静止时位置正常,一转动目标的 odom 坐标就摆动或拖影。


    4. PnP:怎样从二维图像得到三维位置

    4.1 PnP 已知什么、求什么

    已知:

  • 装甲板模型上若干个三维点,单位为米;
  • 检测到的对应二维像素点;
  • 相机内参 K 和畸变参数 D。
  • 求解:

    p_camera = R_camera_armor · p_armor + t_camera_armor

    • R_camera_armor:装甲板物体系相对相机光学系的朝向;
    • t_camera_armor:装甲板原点在相机光学系中的位置;
    • 本项目装甲板模型使用米,因此 tvec 也是米。

    最容易说反的一点是:这里求的是“装甲板在相机中的位姿”,不是最终的世界坐标。

    4.2 当前项目给 PnP 的点

    传统检测路径从左右灯条生成 6 个像素点:

    左灯条:bottom、center、top
    右灯条:top、center、bottom

    并与装甲板物体系中的 6 个模型点一一对应。当前尺寸为:

    小装甲板:0.133 m × 0.050 m
    大装甲板:0.225 m × 0.050 m

    YOLO 路径也会把四角点构造成相同接口所需的灯条/关键点数据,之后仍走 PnP。

    4.3 为什么 IPPE 会给多个解

    装甲板近似是一个平面,只从单目图像观察平面时可能存在姿态歧义。当前代码用 SOLVEPNP_IPPE 获得候选解,再结合:

    • 重投影误差;
    • 灯条在图像中的倾斜方向;
    • 装甲板姿态先验;

    选择更合理的解。可选 BA 会在 PnP 初值上进一步优化装甲板 yaw。

    重投影误差的直觉是:把求出的三维装甲板重新投回图像,如果投影点和实际检测点差得很远,说明角点、尺寸、内参、畸变或 PnP 解至少有一项不对。

    4.4 Detector 发布的位姿属于哪个坐标系

    相机驱动把图像 header.frame_id 设为 camera_optical_frame。Detector 复制这个 header,并直接把 PnP 的 R/t 填入 Armor.pose,所以:

    armor_detector/armors 中的 pose
    所属坐标系 = camera_optical_frame

    Detector 阶段没有把位置手动转换到 odom;真正的完整 TF 转换发生在 Solver 的 armorsCallback() 中。


    5. 为什么要把检测结果转换到 odom

    在 Solver 中,每块装甲板都会执行:

    armor.pose = tf2_buffer_->transform(ps, target_frame_).pose;

    当前 target_frame_ 配置为 odom,因此位置和姿态都会从 camera_optical_frame 转到 odom。

    这一步解决了两个问题:

  • 消除云台自身转动:云台转动不应被误认为敌人运动;
  • 统一多帧观测:EKF 的上一帧状态和当前帧测量必须在同一把尺子下比较。
  • 一个非常有用的正确性判据:

    固定装甲板 + 主动转动云台
    camera_optical_frame 中的位置应该明显变化
    odom 中的位置应该基本稳定

    如果两边都在明显变化,优先排查 TF 方向、外参、时间戳和 pitch 正负号。


    6. 跟踪:从“这一帧的一块板”恢复“整台机器人”

    6.1 为什么检测结果不能直接拿去瞄准

    单帧检测会遇到:

    • 角点噪声导致 PnP 抖动;
    • 某几帧漏检;
    • 同一台机器人有多块外观相同的装甲板;
    • 机器人平移的同时还会旋转;
    • 处理、通信和子弹飞行都存在延迟。

    因此 Solver 不只跟踪“当前看到的板”,而是估计“目标旋转中心 + 运动速度 + 装甲板绕中心的几何关系”。

    6.2 普通目标的 EKF 状态

    当前普通目标的 10 维状态为:

    X = [xc, vxc, yc, vyc, zc, vzc, yaw, v_yaw, r, d_zc]

    状态含义
    (xc,yc,zc) 目标旋转中心在 odom 中的位置,不是当前装甲板的位置
    (vxc,vyc,vzc) 目标中心的平移速度
    yaw 当前参考装甲板绕目标中心的角度/朝向
    v_yaw 目标旋转角速度
    r 目标中心到当前装甲板的半径
    d_zc 当前装甲板和参考中心之间的高度差

    相机真正测到的是一块装甲板:

    Z = [xa, ya, za, yaw]

    EKF 的观测模型为:

    xa = xc – r · cos(yaw)
    ya = yc – r · sin(yaw)
    za = zc + d_zc

    几何直觉如下:

    装甲板 (xa, ya)

    /
    r /
    /
    目标旋转中心 (xc,yc) ●────→ yaw 所描述的径向/板面关系

    初始化时,代码先假设 r=0.26 m,再由测到的板位置反推中心位置。之后 EKF 通过连续观测修正中心、速度、角速度和半径。

    6.3 每一帧 EKF 做什么

    上一帧状态
    → predict:按 dt 把中心位置和 yaw 向前推
    → 数据关联:在相同数字的板中找最接近预测位置的观测
    → update:用 PnP 测量修正预测
    → 得到新的目标中心、速度、角速度、半径

    代码还用状态机降低偶发检测对控制的影响:

    LOST → DETECTING → TRACKING → TEMP_LOST
    ↑ │
    └──────── 丢失超时 ───────────────┘

    • DETECTING:要连续匹配若干帧才确认;
    • TRACKING:正常输出目标;
    • TEMP_LOST:短时漏检仍可依靠预测;
    • LOST:超时后重新选目标初始化。

    6.4 为什么还要“换板”

    四装甲板机器人旋转时,可见板会从一块切换到相邻板。新板的 yaw、半径和高度可能突然变化,但机器人中心没有瞬移。

    handleArmorJump() 会在这种情况下:

    • 更新参考 yaw;
    • 交换两组半径;
    • 修正高度差;
    • 如果几何仍严重不一致,再用当前观测重置部分状态。

    所以“装甲板 yaw 跳变”不一定是误检,也可能只是视野切到了下一块板。

    6.5 前哨站为什么单独处理

    前哨站是 3 块板绕固定中心旋转,当前代码没有走普通目标 EKF,而是使用 OutpostTracker:

    • 收集观测并估计固定圆心;
    • 使用固定半径约 0.275 m;
    • 判断静止或旋转状态;
    • 按 120° 间隔预测三块板;
    • 单独学习三块板的高度。

    它和普通目标的共同点是:最终都要输出“中心、旋转角、角速度、半径”,供后面的选板和预测使用。


    7. 从目标状态到瞄准点

    7.1 先预测子弹到达时的目标

    当前代码使用:

    dt = 数据年龄 + 子弹飞行时间 + prediction_delay

    然后:

    目标中心未来位置 = 当前估计位置 + dt · 线速度
    未来 yaw = 当前 yaw + dt · v_yaw

    其中“数据年龄”是当前解算时间减检测时间戳。这样补偿的不只是子弹飞行时间,还包括图像处理和消息传输已经消耗的时间。

    如果只瞄准观测时的位置,那么目标速度越快、距离越远,落点滞后越明显。

    7.2 枪口不等于 odom 原点

    代码从 TF 读取 gimbal_link 在 odom 中的位置和姿态,并假设枪口在云台前方 0.095 m:

    p_muzzle_odom
    = p_gimbal_origin_odom
    + R_odom_gimbal · (0.095, 0, 0)

    然后计算:

    p_target_from_muzzle = p_target_odom – p_muzzle_odom

    这里有一个很容易混淆的细节:

    • 这个向量的起点已经移到了枪口;
    • 但它的三个分量仍沿 odom 的坐标轴表达;
    • 代码没有再乘 R_odom_gimbalᵀ 把它旋转成 gimbal_link 坐标。

    因此后面的 atan2(y,x) 算出的是相对 odom 的绝对目标角,不是相对当前枪口朝向的局部角度。

    7.3 根据中心恢复所有装甲板并选板

    普通四板目标由中心、yaw、两组半径和高度差恢复出四块板的位置。概念上是:

    p_armor_i = p_center + 绕中心旋转后的径向偏移 + 高度偏移

    代码再结合:

    • 目标相对枪口的方向;
    • 各板朝向;
    • 目标旋转方向和角速度;
    • 子弹飞行时间;
    • 最大侧向提前角;

    选择现在适合瞄准、到弹丸抵达时仍合适的一块板。

    高速旋转超过阈值时,Solver 可从 TRACKING_ARMOR 切到 TRACKING_CENTER,避免瞄准点在多块板之间高速跳变。

    前哨站则优先选择正向射击窗口附近、且正在接近窗口的板,并带有单独的提前角和 ±30° 开火窗口。


    8. yaw、pitch 和弹道补偿

    8.1 不考虑弹道时

    对“以枪口为起点、用 odom 轴表达”的目标向量 p=(x,y,z):

    yaw_geom = atan2(y, x)
    pitch_geom = atan2(z, sqrt(x²+y²))

    输出在内部先使用弧度,填入 GimbalCmd 时再转为度。

    8.2 为什么主要补 pitch

    子弹会因重力下坠,空气阻力还会延长飞行时间,因此实际枪口通常要比目标的几何仰角再抬高一些。

    当前配置使用 quadratic_drag 二次阻力模型,主要参数包括:

    • 弹速;
    • 重力加速度;
    • 阻力系数;
    • 空气密度;
    • 弹丸质量和直径。

    弹道补偿器在当前实现中修正 pitch;yaw 的提前量主要来自目标运动预测和旋转选板,而不是重力模型。

    弹速由串口消息中的 bullet_speed_flag 选择两档配置值。距离或弹速估计错误会同时影响:

  • pitch 下坠补偿;
  • 目标未来位置预测;
  • 旋转目标未来板面角度预测。
  • 8.3 最终下发的到底是绝对角还是角度差

    Solver 同时生成:

    pitch / yaw 相对 odom 的目标绝对角,单位:度
    pitch_diff / yaw_diff 目标绝对角 – 当前云台角,单位:度

    但当前 DefaultProtocol::send() 真正通过串口打包的是:

    fire_advice、pitch、yaw、distance

    也就是说,当前协议下发的是绝对 pitch/yaw,不是 pitch_diff/yaw_diff。差值字段主要用于调试、观察误差等上层用途。下位机必须和这里采用相同的“绝对角”协议约定,否则会出现越转越偏或指令尺度不对。

    8.4 pitch 正负号要沿整条链看

    串口接收后构造 TF 时,代码使用:

    TF pitch = – receive_data.pitch

    Solver 从 TF 取 RPY 后又执行一次:

    rpy_[1] = -rpy_[1]

    这是在 ROS 右手系和下位机 pitch 正方向之间转换。调试时不要只看其中一个负号就删除;要用“云台物理向上转时,反馈、TF、目标 pitch 和误差是否同向收敛”来验证整条符号链。


    9. 什么时候给开火建议

    普通目标要求当前云台角足够接近目标角:

    允许 yaw 误差 ≈ atan2(射击窗口宽度/2, 距离)
    允许 pitch 误差 ≈ atan2(射击窗口高度/2, 距离)

    并设置了最小约 1° 的容忍角。距离越远,同样的物理窗口对应的角度越小,因此理论上要求越严格。

    前哨站还要求:

    • 目标板位于预测到达窗口的 ±30° 内;
    • 板正在向合适的窗口方向接近;
    • 当前云台角也满足普通的角度容忍条件。

    fire_advice 只是视觉侧建议,最终是否真正发射还应由下位机结合比赛状态、摩擦轮状态和安全逻辑决定。


    10. 把源码调用链完整走一遍

    顺序节点/函数输入输出主要坐标系
    1 相机驱动 传感器图像 image_raw、camera_info 图像 / camera_optical_frame
    2 Detector::detect 或 YOLO RGB 图像 装甲板像素关键点、类别 图像像素
    3 ArmorPoseEstimator 2D 点、3D 模型、内参 R_camera_armor、t_camera_armor camera_optical_frame
    4 armor_detector/armors PnP 结果 每块板的 Armor.pose camera_optical_frame
    5 ArmorSolverNode::armorsCallback 相机系板位姿 + TF 转换后的板位姿 odom
    6 Tracker::init/update 板的测量位姿 目标中心、速度、yaw、角速度、半径 odom
    7 Solver::solve Target + 当前 TF + 弹速 未来目标和候选板 位置在 odom;向量以枪口为起点但仍用 odom 轴表达
    8 calcYawAndPitch 选中板相对枪口的向量 绝对 yaw/pitch 相对 odom 的角度
    9 弹道/手动补偿 距离、高度、弹速等 修正后的 pitch/yaw 角度
    10 isOnTarget 当前角与目标角 fire_advice 角度
    11 DefaultProtocol::send GimbalCmd 串口数据包 绝对角,度
    12 下位机反馈 实际 RPY、模式、弹速档 serial/receive + 动态 TF odom → gimbal_link

    对应的反馈闭环为:

    ┌──────── 下位机姿态反馈 ────────┐
    ↓ │
    相机 → Detector → PnP → TF → Tracker → Predictor → Solver → 云台控制
    ↑ │
    └──── odom→gimbal TF ──────┘


    11. 推荐的理解和调试顺序

    不要一上来同时调检测、EKF和弹道。按下面的层次逐个证明正确,定位会快很多。

    第 1 层:先证明坐标轴和 TF 正确

    检查:

    ros2 run tf2_ros tf2_echo odom gimbal_link
    ros2 run tf2_ros tf2_echo gimbal_link camera_optical_frame
    ros2 run tf2_ros tf2_echo odom camera_optical_frame

    验证现象:

    • 云台 yaw 左右转时,TF 中 yaw 同步变化;
    • 云台向上抬时,pitch 的物理方向正确;
    • 光学系蓝色 Z 轴指向相机前方;
    • 静态相机外参不会随时间跳变。

    第 2 层:证明 PnP 测距和方向正确

    把已知尺寸的装甲板放在已知距离:

    • 图像右侧目标应有 tvec.x > 0;
    • 图像下方目标应有 tvec.y > 0;
    • 正前方距离主要体现在 tvec.z > 0;
    • 2 m、4 m、6 m 测距误差应有一致趋势;
    • 重投影误差不能持续很大。

    如果近远距离都按固定比例偏差,先怀疑内参或装甲板模型尺寸;如果偏差随图像位置变化明显,先怀疑畸变和角点。

    第 3 层:证明相机系到 odom 的转换正确

    固定装甲板,只转动云台:

    ros2 topic echo /armor_detector/armors
    ros2 topic echo /armor_solver/measurement

    期望:Detector 中的相机系位置会变化,Solver 中转换到 odom 的测量位置基本稳定。

    如果云台越快误差越大,优先检查 TF 时间戳和 timestamp_offset,而不是先调 EKF。

    第 4 层:再看 EKF

    静止目标时:

    • 中心位置应收敛;
    • 线速度和角速度应接近 0;
    • 瞄准点不应比原始 PnP 更抖。

    移动/旋转目标时:

    • 预测位置应先于测量而不是落后;
    • 换板时中心不应瞬移;
    • 短时漏检应进入 TEMP_LOST 并继续平滑预测。

    一般调参直觉:

    Q 增大 → 更允许模型状态快速变化,响应快但可能更抖
    R 增大 → 更不相信单帧测量,更平滑但可能更滞后

    第 5 层:最后调预测和弹道

    建议分开验证:

  • 静止目标先调 pitch 落点;
  • 匀速平移目标再调 prediction_delay;
  • 旋转目标再看 v_yaw、选板和侧向提前量;
  • 最后验证 fire_advice 窗口。
  • 静止目标都打不准时,不要先用运动预测参数掩盖内参、外参、枪口偏移或弹道参数问题。


    12. 最常见的理解错误

    错误 1:把 PnP 输出当成世界坐标

    PnP 输出属于图像 header 指定的 camera_optical_frame。只有经过 TF2 转换后才属于 odom。

    错误 2:认为换坐标系只需要交换 x/y/z

    真实变换通常同时包含旋转和平移,而且多个旋转的顺序不能交换。让 TF2 处理整条链。

    错误 3:把 lookupTransform(A,B) 理解反

    它返回把 B 中的数据转换到 A 的 T_A_B。

    错误 4:用最新 TF 转换旧图像

    动态场景必须尽量使用图像采集时刻的 TF,否则云台一动就产生伪运动。

    错误 5:把 EKF 的 (x,y,z) 当成当前装甲板

    它们是目标旋转中心;装甲板位置还要由 yaw/r/d_zc 通过观测模型恢复。

    错误 6:认为减去枪口位置后就是 gimbal 坐标

    减法只改变向量起点,不会改变它沿哪组坐标轴表达。当前代码减去枪口 odom 位置后仍是 odom 轴表达。

    错误 7:把弧度和度混用

    TF、Eigen、三角函数内部主要用弧度;串口的云台角和最终 GimbalCmd 使用度。

    错误 8:认为串口下发的是 yaw_diff/pitch_diff

    当前默认协议实际发送绝对 yaw/pitch。协议两端必须完全一致。


    13. 当前源码中值得特别留意的细节

    下面不是理解主流程的前提,但调试异常时很重要。

    13.1 imu_to_camera_ 的变量名和实际查询内容不一致

    ArmorDetectorNode::imageCallback() 查询:

    lookupTransform(odom_frame_, "gimbal_link", image_time)

    取到的旋转实际是 R_odom_gimbal,却被存入名为 imu_to_camera_ 的变量,并作为 R_imu_camera 传给 BA。BA 从变量名和公式上看,期望的是“相机到惯性系”的完整旋转。

    所以如果出现以下现象,应把这条链作为重点检查项:

    • use_ba=true 时 yaw 明显异常,关闭 BA 反而正常;
    • 云台转动时 BA 结果出现系统性偏差;
    • PnP 位置正常,但姿态/旋转中心估计异常。

    完整的相机光学系到 odom 旋转理论上应包含:

    R_odom_optical
    = R_odom_gimbal
    · R_gimbal_cameraLink
    · R_cameraLink_optical

    这里应结合 BA 的坐标定义进一步核实后再修改,不能只按变量名直接替换。

    13.2 R_gimbal_camera_ 的源码注释不要照字面背

    矩阵本身与光学轴重排一致,但源码注释写的逐轴映射和按列计算出的映射不一致。应以矩阵实际乘法结果为准:

    optical +X → gimbal -Y
    optical +Y → gimbal -Z
    optical +Z → gimbal +X

    此外,这个硬编码矩阵只表示标准轴重排,没有包含 camera_joint 的实际安装 rpy=(0,0.275,0.015)。完整位姿在下游通过 URDF/TF 链转换;但 PnP 候选解筛选和 BA 是否需要安装倾角,应结合实车结果核验。

    13.3 calcYawAndPitch() 的 rpy 参数当前没有参与计算

    函数签名接收了 rpy,但函数体只对输入向量做 atan2 和弹道补偿。这与当前“输入向量仍按 odom 轴表达、计算绝对角”的实现是一致的,但函数名和参数容易让人误以为它在函数内部完成了 odom → gimbal 旋转。


    14. 用于复述整套逻辑的简短版本

    如果需要在几分钟内讲清楚,可以这样说:

    相机首先发布带内参和 camera_optical_frame 时间戳的图像。Detector 在像素平面检测灯条或关键点、识别装甲板类型和编号,再用已知装甲板尺寸、相机内参与二维角点做 PnP,得到装甲板在相机光学系中的三维位姿。Solver 用图像采集时刻的 TF,把位姿沿 camera_optical_frame → camera_link → gimbal_link → odom 转到 odom,从而剔除云台自身转动造成的伪运动。普通目标用 EKF 从单块板观测估计整车旋转中心、速度、yaw、角速度和半径,前哨站用单独的圆周模型。随后根据数据延迟和子弹飞行时间预测目标未来中心及板面角度,恢复各块装甲板的位置并选出合适目标。解算器扣除枪口偏移,计算绝对 yaw 和几何 pitch,再用弹道模型修正 pitch、用手动表修正残余误差,并根据当前云台角与目标角的误差给出开火建议。默认串口最终发送绝对 yaw、pitch、距离和开火建议;下位机回传实际云台姿态和弹速,串口节点据此更新动态 TF,形成闭环。

    只要能解释清楚下面这条坐标链,整个系统就已经理解了一半:

    二维像素
    → PnP
    camera_optical_frame 中的装甲板位姿
    → TF2(使用图像时间戳)
    odom 中的装甲板测量
    → Tracker / EKF
    odom 中的目标中心和未来装甲板
    → 扣除枪口 odom 位置
    以枪口为起点、沿 odom 轴表达的瞄准向量
    → atan2 + 弹道补偿
    相对 odom 的绝对 yaw/pitch
    → 串口
    下位机云台控制

    15. 建议继续对照的源码

    • src/rm_robot_description/urdf/rm_gimbal.urdf.xacro:TF 树和光学轴重排
    • src/rm_hardware_driver/rm_serial_driver/src/serial_driver_node.cpp:云台反馈如何生成动态 TF
    • src/rm_auto_aim/armor_detector/src/armor_pose_estimator.cpp:PnP、候选解与 BA
    • src/rm_auto_aim/armor_solver/src/armor_solver_node.cpp:相机系到 odom、跟踪调用与消息发布
    • src/rm_auto_aim/armor_solver/include/armor_solver/motion_model.hpp:EKF 状态转移和观测模型
    • src/rm_auto_aim/armor_solver/src/armor_tracker.cpp:跟踪状态机、数据关联和换板
    • src/rm_auto_aim/armor_solver/src/armor_solver.cpp:预测、选板、枪口偏移、弹道与开火
    • src/rm_hardware_driver/rm_serial_driver/src/protocol/default_protocol.cpp:最终实际发送的字段

    本文基于当前仓库中的实际源码和参数编写,目标是先建立直觉,再把代码串起来。
    重点不是背公式,而是始终回答三个问题:目标现在在哪里、子弹到达时它会在哪里、云台应该转到哪里。

    1. 先用一句话理解整套系统

    自瞄是一个带反馈的闭环:

    相机拍图
    → 检出装甲板二维角点
    → PnP 求装甲板在相机前方的三维位置
    → TF 转到不随云台转动的 odom 坐标系
    → 跟踪并预测目标未来状态
    → 选择要打的装甲板
    → 计算 yaw / pitch,并补偿弹道
    → 下发绝对云台角和开火建议
    → 下位机回传真实云台姿态和弹速
    → 更新 TF,进入下一帧

    可以把它拆成三个问题:

  • 看见:图像里的这块板在三维空间哪里?——检测、分类、PnP。
  • 看稳:云台和敌人都在动,敌人的真实运动状态是什么?——TF、数据关联、EKF。
  • 打中:子弹飞到时打哪块板,枪口应指向哪里?——预测、选板、弹道补偿、开火判断。

  • 2. 坐标系到底是什么

    坐标系可以理解成一把带方向的三维尺子。同一个物理点没有变,换了一把尺子测量,三个坐标值就可能完全不同。

    例如,目标始终停在场地中的同一处:

    • 云台向左转时,目标会移动到图像右边,因此目标的相机坐标变了;
    • 目标在场地里没有动,因此正确变换后的 odom 坐标应该基本不变。

    这正是为什么不能直接拿 PnP 的相机坐标做跨帧跟踪。

    2.1 本项目中的主要坐标和坐标轴

    坐标/数据轴方向或含义是否随云台转动
    图像像素 (u,v) u 向右,v 向下;这是二维像素,不是三维米制坐标 随图像变化
    camera_optical_frame X 向右,Y 向下,Z 沿光轴向前
    camera_link ROS 机体系约定:X 向前,Y 向左,Z 向上
    gimbal_link 云台/枪口安装所依附的机体系,X 向前、Y 向左、Z 向上
    odom 本项目用于跟踪的惯性参考系,通常在启动时建立 否;云台姿态相对它变化
    装甲板物体系 模型点固定在装甲板上;当前模型点的第一维为 0,板面位于其 YZ 平面 随装甲板运动

    注意:图像的 (u,v) 和光学坐标系的 (X,Y,Z) 都有“右、下”的方向,但前者单位是像素、只有二维,后者单位是米、是三维,不能混为一谈。

    2.2 本项目的 TF 树

    odom
    └── gimbal_link 动态 TF:下位机实时回传 roll/pitch/yaw
    └── camera_link 静态 TF:相机相对云台的安装平移和倾斜
    └── camera_optical_frame
    静态 TF:把 X前Y左Z上 重排为 X右Y下Z前

    对应代码和参数:

    • 动态 odom → gimbal_link:rm_serial_driver/src/serial_driver_node.cpp
    • 静态 gimbal_link → camera_link:launch_params.yaml 中的 odom2camera 参数,名称虽然叫 odom2camera,实际被填进了 camera_joint
    • 静态 camera_link → camera_optical_frame:rm_gimbal.urdf.xacro

    当前安装参数为:

    xyz: 0.246 0.0 0.049
    rpy: 0.00 0.275 0.015

    它表示相机坐标原点相对 gimbal_link 原点的安装偏移和安装倾角。xyz 的单位是米,rpy 的单位是弧度。

    2.3 光学系为什么还要旋转一次

    相机算法/OpenCV 常用:

    X 右,Y 下,Z 前

    ROS 机体系常用:

    X 前,Y 左,Z 上

    URDF 中的固定旋转 rpy=(-π/2, 0, -π/2) 完成了这次轴重排。对应矩阵为:

    R_gimbal_or_cameraLink_from_optical =
    [ 0 0 1 ]
    [-1 0 0 ]
    [ 0 -1 0 ]

    判断一个旋转矩阵如何映射坐标轴,要看它的列:

    光学系 +X(右) → 机体系 -Y(右)
    光学系 +Y(下) → 机体系 -Z(下)
    光学系 +Z(前) → 机体系 +X(前)

    因此相机看到的“前方 4 米” (0,0,4),转换后大致就是云台系的“前方 4 米” (4,0,0)。


    3. 怎样读懂一个 TF 变换

    ROS 中:

    lookupTransform(target_frame, source_frame, time)

    返回的是把 source_frame 中的数据变到 target_frame 的变换,可记为:

    T_target_source

    对一个点:

    p_target = R_target_source · p_source + t_target_source

    其中:

    • R 改变坐标轴方向;
    • t 补偿两个坐标系原点的位置差;
    • 点需要旋转和平移,方向向量只需要旋转。

    本项目从相机光学系到 odom 的完整链为:

    T_odom_optical
    = T_odom_gimbal
    · T_gimbal_cameraLink
    · T_cameraLink_optical

    于是:

    p_odom = T_odom_optical · p_optical

    矩阵相乘顺序不能交换。可以从右往左读:先把光学系数据变到 camera_link,再到 gimbal_link,最后到 odom。

    3.1 一个直观例子

    暂时忽略安装倾角和云台转角,只保留轴重排和相机平移。假设 PnP 得到:

    p_optical = (0.5, 0.3, 4.0) m

    含义是目标在光轴前方 4 m、画面右侧约 0.5 m、画面下方约 0.3 m。轴重排后约为:

    p_gimbal ≈ (4.0, -0.5, -0.3) m

    再加相机相对云台原点的平移 (0.246, 0, 0.049):

    p_gimbal ≈ (4.246, -0.5, -0.251) m

    实车还要继续考虑安装 rpy 和当前云台动态姿态,所以最终数值由 TF2 统一计算,不建议手写分量公式。

    3.2 时间戳为什么和坐标转换同样重要

    相机曝光时云台角度为 yaw₁,等图像处理完后云台可能已经转到 yaw₂。如果用“当前最新姿态”去转换旧图像,就会把目标投到错误的世界位置。

    当前代码有意识地用图像时间戳查询 TF:

    lookupTransform(odom_frame_, "gimbal_link", img_msg->header.stamp, ...)

    串口侧还有 timestamp_offset,用于校正下位机反馈和相机时间之间的偏差。时间同步不准的典型表现是:云台静止时位置正常,一转动目标的 odom 坐标就摆动或拖影。


    4. PnP:怎样从二维图像得到三维位置

    4.1 PnP 已知什么、求什么

    已知:

  • 装甲板模型上若干个三维点,单位为米;
  • 检测到的对应二维像素点;
  • 相机内参 K 和畸变参数 D。
  • 求解:

    p_camera = R_camera_armor · p_armor + t_camera_armor

    • R_camera_armor:装甲板物体系相对相机光学系的朝向;
    • t_camera_armor:装甲板原点在相机光学系中的位置;
    • 本项目装甲板模型使用米,因此 tvec 也是米。

    最容易说反的一点是:这里求的是“装甲板在相机中的位姿”,不是最终的世界坐标。

    4.2 当前项目给 PnP 的点

    传统检测路径从左右灯条生成 6 个像素点:

    左灯条:bottom、center、top
    右灯条:top、center、bottom

    并与装甲板物体系中的 6 个模型点一一对应。当前尺寸为:

    小装甲板:0.133 m × 0.050 m
    大装甲板:0.225 m × 0.050 m

    YOLO 路径也会把四角点构造成相同接口所需的灯条/关键点数据,之后仍走 PnP。

    4.3 为什么 IPPE 会给多个解

    装甲板近似是一个平面,只从单目图像观察平面时可能存在姿态歧义。当前代码用 SOLVEPNP_IPPE 获得候选解,再结合:

    • 重投影误差;
    • 灯条在图像中的倾斜方向;
    • 装甲板姿态先验;

    选择更合理的解。可选 BA 会在 PnP 初值上进一步优化装甲板 yaw。

    重投影误差的直觉是:把求出的三维装甲板重新投回图像,如果投影点和实际检测点差得很远,说明角点、尺寸、内参、畸变或 PnP 解至少有一项不对。

    4.4 Detector 发布的位姿属于哪个坐标系

    相机驱动把图像 header.frame_id 设为 camera_optical_frame。Detector 复制这个 header,并直接把 PnP 的 R/t 填入 Armor.pose,所以:

    armor_detector/armors 中的 pose
    所属坐标系 = camera_optical_frame

    Detector 阶段没有把位置手动转换到 odom;真正的完整 TF 转换发生在 Solver 的 armorsCallback() 中。


    5. 为什么要把检测结果转换到 odom

    在 Solver 中,每块装甲板都会执行:

    armor.pose = tf2_buffer_->transform(ps, target_frame_).pose;

    当前 target_frame_ 配置为 odom,因此位置和姿态都会从 camera_optical_frame 转到 odom。

    这一步解决了两个问题:

  • 消除云台自身转动:云台转动不应被误认为敌人运动;
  • 统一多帧观测:EKF 的上一帧状态和当前帧测量必须在同一把尺子下比较。
  • 一个非常有用的正确性判据:

    固定装甲板 + 主动转动云台
    camera_optical_frame 中的位置应该明显变化
    odom 中的位置应该基本稳定

    如果两边都在明显变化,优先排查 TF 方向、外参、时间戳和 pitch 正负号。


    6. 跟踪:从“这一帧的一块板”恢复“整台机器人”

    6.1 为什么检测结果不能直接拿去瞄准

    单帧检测会遇到:

    • 角点噪声导致 PnP 抖动;
    • 某几帧漏检;
    • 同一台机器人有多块外观相同的装甲板;
    • 机器人平移的同时还会旋转;
    • 处理、通信和子弹飞行都存在延迟。

    因此 Solver 不只跟踪“当前看到的板”,而是估计“目标旋转中心 + 运动速度 + 装甲板绕中心的几何关系”。

    6.2 普通目标的 EKF 状态

    当前普通目标的 10 维状态为:

    X = [xc, vxc, yc, vyc, zc, vzc, yaw, v_yaw, r, d_zc]

    状态含义
    (xc,yc,zc) 目标旋转中心在 odom 中的位置,不是当前装甲板的位置
    (vxc,vyc,vzc) 目标中心的平移速度
    yaw 当前参考装甲板绕目标中心的角度/朝向
    v_yaw 目标旋转角速度
    r 目标中心到当前装甲板的半径
    d_zc 当前装甲板和参考中心之间的高度差

    相机真正测到的是一块装甲板:

    Z = [xa, ya, za, yaw]

    EKF 的观测模型为:

    xa = xc – r · cos(yaw)
    ya = yc – r · sin(yaw)
    za = zc + d_zc

    几何直觉如下:

    装甲板 (xa, ya)

    /
    r /
    /
    目标旋转中心 (xc,yc) ●────→ yaw 所描述的径向/板面关系

    初始化时,代码先假设 r=0.26 m,再由测到的板位置反推中心位置。之后 EKF 通过连续观测修正中心、速度、角速度和半径。

    6.3 每一帧 EKF 做什么

    上一帧状态
    → predict:按 dt 把中心位置和 yaw 向前推
    → 数据关联:在相同数字的板中找最接近预测位置的观测
    → update:用 PnP 测量修正预测
    → 得到新的目标中心、速度、角速度、半径

    代码还用状态机降低偶发检测对控制的影响:

    LOST → DETECTING → TRACKING → TEMP_LOST
    ↑ │
    └──────── 丢失超时 ───────────────┘

    • DETECTING:要连续匹配若干帧才确认;
    • TRACKING:正常输出目标;
    • TEMP_LOST:短时漏检仍可依靠预测;
    • LOST:超时后重新选目标初始化。

    6.4 为什么还要“换板”

    四装甲板机器人旋转时,可见板会从一块切换到相邻板。新板的 yaw、半径和高度可能突然变化,但机器人中心没有瞬移。

    handleArmorJump() 会在这种情况下:

    • 更新参考 yaw;
    • 交换两组半径;
    • 修正高度差;
    • 如果几何仍严重不一致,再用当前观测重置部分状态。

    所以“装甲板 yaw 跳变”不一定是误检,也可能只是视野切到了下一块板。

    6.5 前哨站为什么单独处理

    前哨站是 3 块板绕固定中心旋转,当前代码没有走普通目标 EKF,而是使用 OutpostTracker:

    • 收集观测并估计固定圆心;
    • 使用固定半径约 0.275 m;
    • 判断静止或旋转状态;
    • 按 120° 间隔预测三块板;
    • 单独学习三块板的高度。

    它和普通目标的共同点是:最终都要输出“中心、旋转角、角速度、半径”,供后面的选板和预测使用。


    7. 从目标状态到瞄准点

    7.1 先预测子弹到达时的目标

    当前代码使用:

    dt = 数据年龄 + 子弹飞行时间 + prediction_delay

    然后:

    目标中心未来位置 = 当前估计位置 + dt · 线速度
    未来 yaw = 当前 yaw + dt · v_yaw

    其中“数据年龄”是当前解算时间减检测时间戳。这样补偿的不只是子弹飞行时间,还包括图像处理和消息传输已经消耗的时间。

    如果只瞄准观测时的位置,那么目标速度越快、距离越远,落点滞后越明显。

    7.2 枪口不等于 odom 原点

    代码从 TF 读取 gimbal_link 在 odom 中的位置和姿态,并假设枪口在云台前方 0.095 m:

    p_muzzle_odom
    = p_gimbal_origin_odom
    + R_odom_gimbal · (0.095, 0, 0)

    然后计算:

    p_target_from_muzzle = p_target_odom – p_muzzle_odom

    这里有一个很容易混淆的细节:

    • 这个向量的起点已经移到了枪口;
    • 但它的三个分量仍沿 odom 的坐标轴表达;
    • 代码没有再乘 R_odom_gimbalᵀ 把它旋转成 gimbal_link 坐标。

    因此后面的 atan2(y,x) 算出的是相对 odom 的绝对目标角,不是相对当前枪口朝向的局部角度。

    7.3 根据中心恢复所有装甲板并选板

    普通四板目标由中心、yaw、两组半径和高度差恢复出四块板的位置。概念上是:

    p_armor_i = p_center + 绕中心旋转后的径向偏移 + 高度偏移

    代码再结合:

    • 目标相对枪口的方向;
    • 各板朝向;
    • 目标旋转方向和角速度;
    • 子弹飞行时间;
    • 最大侧向提前角;

    选择现在适合瞄准、到弹丸抵达时仍合适的一块板。

    高速旋转超过阈值时,Solver 可从 TRACKING_ARMOR 切到 TRACKING_CENTER,避免瞄准点在多块板之间高速跳变。

    前哨站则优先选择正向射击窗口附近、且正在接近窗口的板,并带有单独的提前角和 ±30° 开火窗口。


    8. yaw、pitch 和弹道补偿

    8.1 不考虑弹道时

    对“以枪口为起点、用 odom 轴表达”的目标向量 p=(x,y,z):

    yaw_geom = atan2(y, x)
    pitch_geom = atan2(z, sqrt(x²+y²))

    输出在内部先使用弧度,填入 GimbalCmd 时再转为度。

    8.2 为什么主要补 pitch

    子弹会因重力下坠,空气阻力还会延长飞行时间,因此实际枪口通常要比目标的几何仰角再抬高一些。

    当前配置使用 quadratic_drag 二次阻力模型,主要参数包括:

    • 弹速;
    • 重力加速度;
    • 阻力系数;
    • 空气密度;
    • 弹丸质量和直径。

    弹道补偿器在当前实现中修正 pitch;yaw 的提前量主要来自目标运动预测和旋转选板,而不是重力模型。

    弹速由串口消息中的 bullet_speed_flag 选择两档配置值。距离或弹速估计错误会同时影响:

  • pitch 下坠补偿;
  • 目标未来位置预测;
  • 旋转目标未来板面角度预测。
  • 8.3 最终下发的到底是绝对角还是角度差

    Solver 同时生成:

    pitch / yaw 相对 odom 的目标绝对角,单位:度
    pitch_diff / yaw_diff 目标绝对角 – 当前云台角,单位:度

    但当前 DefaultProtocol::send() 真正通过串口打包的是:

    fire_advice、pitch、yaw、distance

    也就是说,当前协议下发的是绝对 pitch/yaw,不是 pitch_diff/yaw_diff。差值字段主要用于调试、观察误差等上层用途。下位机必须和这里采用相同的“绝对角”协议约定,否则会出现越转越偏或指令尺度不对。

    8.4 pitch 正负号要沿整条链看

    串口接收后构造 TF 时,代码使用:

    TF pitch = – receive_data.pitch

    Solver 从 TF 取 RPY 后又执行一次:

    rpy_[1] = -rpy_[1]

    这是在 ROS 右手系和下位机 pitch 正方向之间转换。调试时不要只看其中一个负号就删除;要用“云台物理向上转时,反馈、TF、目标 pitch 和误差是否同向收敛”来验证整条符号链。


    9. 什么时候给开火建议

    普通目标要求当前云台角足够接近目标角:

    允许 yaw 误差 ≈ atan2(射击窗口宽度/2, 距离)
    允许 pitch 误差 ≈ atan2(射击窗口高度/2, 距离)

    并设置了最小约 1° 的容忍角。距离越远,同样的物理窗口对应的角度越小,因此理论上要求越严格。

    前哨站还要求:

    • 目标板位于预测到达窗口的 ±30° 内;
    • 板正在向合适的窗口方向接近;
    • 当前云台角也满足普通的角度容忍条件。

    fire_advice 只是视觉侧建议,最终是否真正发射还应由下位机结合比赛状态、摩擦轮状态和安全逻辑决定。


    10. 把源码调用链完整走一遍

    顺序节点/函数输入输出主要坐标系
    1 相机驱动 传感器图像 image_raw、camera_info 图像 / camera_optical_frame
    2 Detector::detect 或 YOLO RGB 图像 装甲板像素关键点、类别 图像像素
    3 ArmorPoseEstimator 2D 点、3D 模型、内参 R_camera_armor、t_camera_armor camera_optical_frame
    4 armor_detector/armors PnP 结果 每块板的 Armor.pose camera_optical_frame
    5 ArmorSolverNode::armorsCallback 相机系板位姿 + TF 转换后的板位姿 odom
    6 Tracker::init/update 板的测量位姿 目标中心、速度、yaw、角速度、半径 odom
    7 Solver::solve Target + 当前 TF + 弹速 未来目标和候选板 位置在 odom;向量以枪口为起点但仍用 odom 轴表达
    8 calcYawAndPitch 选中板相对枪口的向量 绝对 yaw/pitch 相对 odom 的角度
    9 弹道/手动补偿 距离、高度、弹速等 修正后的 pitch/yaw 角度
    10 isOnTarget 当前角与目标角 fire_advice 角度
    11 DefaultProtocol::send GimbalCmd 串口数据包 绝对角,度
    12 下位机反馈 实际 RPY、模式、弹速档 serial/receive + 动态 TF odom → gimbal_link

    对应的反馈闭环为:

    ┌──────── 下位机姿态反馈 ────────┐
    ↓ │
    相机 → Detector → PnP → TF → Tracker → Predictor → Solver → 云台控制
    ↑ │
    └──── odom→gimbal TF ──────┘


    11. 推荐的理解和调试顺序

    不要一上来同时调检测、EKF和弹道。按下面的层次逐个证明正确,定位会快很多。

    第 1 层:先证明坐标轴和 TF 正确

    检查:

    ros2 run tf2_ros tf2_echo odom gimbal_link
    ros2 run tf2_ros tf2_echo gimbal_link camera_optical_frame
    ros2 run tf2_ros tf2_echo odom camera_optical_frame

    验证现象:

    • 云台 yaw 左右转时,TF 中 yaw 同步变化;
    • 云台向上抬时,pitch 的物理方向正确;
    • 光学系蓝色 Z 轴指向相机前方;
    • 静态相机外参不会随时间跳变。

    第 2 层:证明 PnP 测距和方向正确

    把已知尺寸的装甲板放在已知距离:

    • 图像右侧目标应有 tvec.x > 0;
    • 图像下方目标应有 tvec.y > 0;
    • 正前方距离主要体现在 tvec.z > 0;
    • 2 m、4 m、6 m 测距误差应有一致趋势;
    • 重投影误差不能持续很大。

    如果近远距离都按固定比例偏差,先怀疑内参或装甲板模型尺寸;如果偏差随图像位置变化明显,先怀疑畸变和角点。

    第 3 层:证明相机系到 odom 的转换正确

    固定装甲板,只转动云台:

    ros2 topic echo /armor_detector/armors
    ros2 topic echo /armor_solver/measurement

    期望:Detector 中的相机系位置会变化,Solver 中转换到 odom 的测量位置基本稳定。

    如果云台越快误差越大,优先检查 TF 时间戳和 timestamp_offset,而不是先调 EKF。

    第 4 层:再看 EKF

    静止目标时:

    • 中心位置应收敛;
    • 线速度和角速度应接近 0;
    • 瞄准点不应比原始 PnP 更抖。

    移动/旋转目标时:

    • 预测位置应先于测量而不是落后;
    • 换板时中心不应瞬移;
    • 短时漏检应进入 TEMP_LOST 并继续平滑预测。

    一般调参直觉:

    Q 增大 → 更允许模型状态快速变化,响应快但可能更抖
    R 增大 → 更不相信单帧测量,更平滑但可能更滞后

    第 5 层:最后调预测和弹道

    建议分开验证:

  • 静止目标先调 pitch 落点;
  • 匀速平移目标再调 prediction_delay;
  • 旋转目标再看 v_yaw、选板和侧向提前量;
  • 最后验证 fire_advice 窗口。
  • 静止目标都打不准时,不要先用运动预测参数掩盖内参、外参、枪口偏移或弹道参数问题。


    12. 最常见的理解错误

    错误 1:把 PnP 输出当成世界坐标

    PnP 输出属于图像 header 指定的 camera_optical_frame。只有经过 TF2 转换后才属于 odom。

    错误 2:认为换坐标系只需要交换 x/y/z

    真实变换通常同时包含旋转和平移,而且多个旋转的顺序不能交换。让 TF2 处理整条链。

    错误 3:把 lookupTransform(A,B) 理解反

    它返回把 B 中的数据转换到 A 的 T_A_B。

    错误 4:用最新 TF 转换旧图像

    动态场景必须尽量使用图像采集时刻的 TF,否则云台一动就产生伪运动。

    错误 5:把 EKF 的 (x,y,z) 当成当前装甲板

    它们是目标旋转中心;装甲板位置还要由 yaw/r/d_zc 通过观测模型恢复。

    错误 6:认为减去枪口位置后就是 gimbal 坐标

    减法只改变向量起点,不会改变它沿哪组坐标轴表达。当前代码减去枪口 odom 位置后仍是 odom 轴表达。

    错误 7:把弧度和度混用

    TF、Eigen、三角函数内部主要用弧度;串口的云台角和最终 GimbalCmd 使用度。

    错误 8:认为串口下发的是 yaw_diff/pitch_diff

    当前默认协议实际发送绝对 yaw/pitch。协议两端必须完全一致。


    13. 当前源码中值得特别留意的细节

    下面不是理解主流程的前提,但调试异常时很重要。

    13.1 imu_to_camera_ 的变量名和实际查询内容不一致

    ArmorDetectorNode::imageCallback() 查询:

    lookupTransform(odom_frame_, "gimbal_link", image_time)

    取到的旋转实际是 R_odom_gimbal,却被存入名为 imu_to_camera_ 的变量,并作为 R_imu_camera 传给 BA。BA 从变量名和公式上看,期望的是“相机到惯性系”的完整旋转。

    所以如果出现以下现象,应把这条链作为重点检查项:

    • use_ba=true 时 yaw 明显异常,关闭 BA 反而正常;
    • 云台转动时 BA 结果出现系统性偏差;
    • PnP 位置正常,但姿态/旋转中心估计异常。

    完整的相机光学系到 odom 旋转理论上应包含:

    R_odom_optical
    = R_odom_gimbal
    · R_gimbal_cameraLink
    · R_cameraLink_optical

    这里应结合 BA 的坐标定义进一步核实后再修改,不能只按变量名直接替换。

    13.2 R_gimbal_camera_ 的源码注释不要照字面背

    矩阵本身与光学轴重排一致,但源码注释写的逐轴映射和按列计算出的映射不一致。应以矩阵实际乘法结果为准:

    optical +X → gimbal -Y
    optical +Y → gimbal -Z
    optical +Z → gimbal +X

    此外,这个硬编码矩阵只表示标准轴重排,没有包含 camera_joint 的实际安装 rpy=(0,0.275,0.015)。完整位姿在下游通过 URDF/TF 链转换;但 PnP 候选解筛选和 BA 是否需要安装倾角,应结合实车结果核验。

    13.3 calcYawAndPitch() 的 rpy 参数当前没有参与计算

    函数签名接收了 rpy,但函数体只对输入向量做 atan2 和弹道补偿。这与当前“输入向量仍按 odom 轴表达、计算绝对角”的实现是一致的,但函数名和参数容易让人误以为它在函数内部完成了 odom → gimbal 旋转。


    14. 用于复述整套逻辑的简短版本

    如果需要在几分钟内讲清楚,可以这样说:

    相机首先发布带内参和 camera_optical_frame 时间戳的图像。Detector 在像素平面检测灯条或关键点、识别装甲板类型和编号,再用已知装甲板尺寸、相机内参与二维角点做 PnP,得到装甲板在相机光学系中的三维位姿。Solver 用图像采集时刻的 TF,把位姿沿 camera_optical_frame → camera_link → gimbal_link → odom 转到 odom,从而剔除云台自身转动造成的伪运动。普通目标用 EKF 从单块板观测估计整车旋转中心、速度、yaw、角速度和半径,前哨站用单独的圆周模型。随后根据数据延迟和子弹飞行时间预测目标未来中心及板面角度,恢复各块装甲板的位置并选出合适目标。解算器扣除枪口偏移,计算绝对 yaw 和几何 pitch,再用弹道模型修正 pitch、用手动表修正残余误差,并根据当前云台角与目标角的误差给出开火建议。默认串口最终发送绝对 yaw、pitch、距离和开火建议;下位机回传实际云台姿态和弹速,串口节点据此更新动态 TF,形成闭环。

    只要能解释清楚下面这条坐标链,整个系统就已经理解了一半:

    二维像素
    → PnP
    camera_optical_frame 中的装甲板位姿
    → TF2(使用图像时间戳)
    odom 中的装甲板测量
    → Tracker / EKF
    odom 中的目标中心和未来装甲板
    → 扣除枪口 odom 位置
    以枪口为起点、沿 odom 轴表达的瞄准向量
    → atan2 + 弹道补偿
    相对 odom 的绝对 yaw/pitch
    → 串口
    下位机云台控制

    15. 建议继续对照的源码

    • src/rm_robot_description/urdf/rm_gimbal.urdf.xacro:TF 树和光学轴重排
    • src/rm_hardware_driver/rm_serial_driver/src/serial_driver_node.cpp:云台反馈如何生成动态 TF
    • src/rm_auto_aim/armor_detector/src/armor_pose_estimator.cpp:PnP、候选解与 BA
    • src/rm_auto_aim/armor_solver/src/armor_solver_node.cpp:相机系到 odom、跟踪调用与消息发布
    • src/rm_auto_aim/armor_solver/include/armor_solver/motion_model.hpp:EKF 状态转移和观测模型
    • src/rm_auto_aim/armor_solver/src/armor_tracker.cpp:跟踪状态机、数据关联和换板
    • src/rm_auto_aim/armor_solver/src/armor_solver.cpp:预测、选板、枪口偏移、弹道与开火
    • src/rm_hardware_driver/rm_serial_driver/src/protocol/default_protocol.cpp:最终实际发送的字段

    在这里插入图片描述
    请添加图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 坐标转换与自瞄整体思路
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址