(6-4-01)IMU融合与机体状态估计:综合实战:腿式机器人的IMU关节融合与状态估计(1)

6.4 综合实战:腿式机器人的IMU关节融合与状态估计

本实例是一个用于基于本体感知状态估计的开源项目,通过融合机器人身上的IMU(惯性测量单元)数据和关节编码器(Joint Encoders)数据来估计腿式机器人主体的运动状态(如姿态和位置轨迹)。在本实例中提供了一个Python实现,可以对从宇树科技Unitree Go2机器人采集的ROS2 rosbag数据进行解码和状态估计,不依赖完整的ROS2环境,适合作为研究和实验用途的基础代码框架。

实例6-4 :腿式机器人的IMU 关节融合与状态估计(源码路径:codes\6\leg-odometry

6.4.1 配置文件

文件config/go2.yaml是本项目的核心配置文件,用于定义ROS话题、IMU采样率、数据处理时间、初始状态标准差、IMU噪声模型、机器人几何参数和输出目录。这些参数用于EKF状态估计和IMU前向传播模块,指导滤波器初始化、传感器数据融合及运动状态计算,从而实现腿式机器人精确的姿态与位置估计。

python 复制代码
topic: "/lowstate"       # ROS 话题名称

imudatarate: 200         # IMU 数据采样率 (Hz)

starttime: 337679         # rosbag2 数据开始时间
endtime: 337857           # rosbag2 数据结束时间

initAlignmentTime: 10     # 初始对齐所需的时间 (秒)

# 初始状态标准差
initposstd: [0.05, 0.05, 0.05]   # 初始位置标准差 (m)
initvelstd: [0.05, 0.05, 0.05]   # 初始速度标准差 (m/s)
initattstd: [1.0, 1.0, 0.5]      # 初始姿态标准差 (度)

# IMU 噪声参数
imunoise:
  arw:   0.1        # 陀螺角随机游走(角速度噪声)deg/√h
  vrw:   6          # 加速度随机游走 (m/s^1.5)
  gbstd: 0.1        # 陀螺零偏标准差
  abstd: 2000       # 加速度偏置标准差
  gsstd: 500        # 陀螺测量噪声标准差
  asstd: 500        # 加速度测量噪声标准差
  corrtime: 0.5     # 噪声相关时间常数 (秒)

# 坐标变换
rotmat: [-1,0,0,
         0,-1,0,
         0,0,1]        # 旋转矩阵:IMU 坐标系到机器人基座坐标系

base_in_bodyimu: [0.0, 0.0, 0.1]  # 机器人基座在 IMU 坐标系下的位置偏移 (m)

# 机器人几何参数
robotpara:
  ox: 0.1934        # 前腿横向偏移
  oy: 0.0465        # 前腿纵向偏移
  ot: 0.0955        # 前腿高度偏移
  lc: 0.213         # 腿长参数 lc
  lt: 0.213         # 腿长参数 lt

# 输出文件夹
output:
  dir: "./output"   # 输出结果目录

6.4.2 工具文件

"utils"目录提供了Unitree Go2机器人低层状态数据处理与辅助工具,包括数据结构定义、IMU与机器人传感器状态的表示、姿态与旋转矩阵转换、配置参数加载、文件读写、ROS消息到NumPy数组的转换、生成数据流,以及机器人轨迹可视化功能,为 EKF 状态估计和导航分析模块提供完整的基础工具支持。

(1)文件src/types.py定义了实例中使用的核心数据结构和常量,包括IMU数据、机器人姿态(Attitude)、位置-速度-姿态状态(PVA)、机器人传感器数据(关节角度、速度、足端力)、IMU误差与噪声模型、导航状态(NavState)以及机器人几何参数(RobotPara)。这些类用于统一管理传感器输入、滤波器状态、噪声参数和机器人几何参数,为EKF状态估计和运动融合模块提供数据接口。

python 复制代码
import numpy as np
from scipy.spatial.transform import Rotation as R

# 数据类型与常量定义
DT = np.float64                     # 使用的浮点类型
D2R = np.pi / 180.0                 # 角度转弧度
R2D = 180.0 / np.pi                 # 弧度转角度
IMU_RATE = 200                       # IMU 数据采样率 (Hz)
window_length = 11                   # 滤波窗口长度
halfwindow_length = window_length // 2
NormG = 9.782940329221166            # 重力加速度标准值 (m/s^2)

# IMU 数据结构
class IMU:
    def __init__(self):
        self.timestamp = 0.0
        self.dt = 0.0
        self.angular_velocity = np.zeros(3, dtype=DT)  # 三维角速度 (Eigen::Vector3d)
        self.acceleration = np.zeros(3, dtype=DT)      # 三维加速度 (Eigen::Vector3d)

# 姿态数据结构
class Attitude:
    def __init__(self):
        self.qbn = R.from_quat([0.0, 0.0, 0.0, 1.0])  # 四元数 (单位四元数)
        self.cbn = np.eye(3, dtype=DT)                # 方向余弦矩阵 (单位矩阵)
        self.euler = np.zeros(3, dtype=DT)            # 欧拉角 (roll, pitch, yaw)

# 位置-速度-姿态结构
class PVA:
    def __init__(self):
        self.pos = np.zeros(3, dtype=DT)             # 位置 (m)
        self.vel = np.zeros(3, dtype=DT)             # 速度 (m/s)
        self.att = Attitude()                         # 姿态

# 机器人传感器数据
class RobotSensor:
    def __init__(self):
        self.timestamp = 0.0
        self.joint_angular_position = np.zeros(12, dtype=DT)  # 关节角度 (12 关节)
        self.joint_angular_velocity = np.zeros(12, dtype=DT)  # 关节角速度 (12 关节)
        self.footforce = np.zeros(4, dtype=DT)                # 足端力 (4 只足)

# IMU 误差结构
class ImuError:
    def __init__(self):
        self.gyrbias = np.zeros(3, dtype=DT)    # 陀螺零偏 (rad/s)
        self.accbias = np.zeros(3, dtype=DT)    # 加速度零偏 (m/s^2)
        self.gyrscale = np.zeros(3, dtype=DT)   # 陀螺比例误差
        self.accscale = np.zeros(3, dtype=DT)   # 加速度比例误差

# 导航状态
class NavState:
    def __init__(self):
        self.pos = np.zeros(3, dtype=DT)        # 位置
        self.vel = np.zeros(3, dtype=DT)        # 速度
        self.euler = np.zeros(3, dtype=DT)      # 欧拉角
        self.imuerror = ImuError()              # IMU 误差

# IMU 噪声参数
class ImuNoise:
    def __init__(self):
        self.gyr_arw = np.zeros(3, dtype=DT)      # 陀螺角随机游走 (deg/√h)
        self.acc_vrw = np.zeros(3, dtype=DT)      # 加速度随机游走
        self.gyrbias_std = np.zeros(3, dtype=DT)  # 陀螺零偏标准差
        self.accbias_std = np.zeros(3, dtype=DT)  # 加速度零偏标准差
        self.gyrscale_std = np.zeros(3, dtype=DT) # 陀螺比例误差标准差
        self.accscale_std = np.zeros(3, dtype=DT) # 加速度比例误差标准差
        self.corr_time = 0.0                       # 噪声相关时间常数

# 机器人几何参数
class RobotPara:
    def __init__(self):
        self.ox = 0.0    # 前腿横向偏移
        self.oy = 0.0    # 前腿纵向偏移
        self.ot = 0.0    # 前腿高度偏移
        self.lc = 0.0    # 腿长参数 lc
        self.lt = 0.0    # 腿长参数 lt

# 全局参数集合
class Paras:
    def __init__(self):
        # 初始状态及标准差
        self.initstate = NavState()
        self.initstate_std = NavState()

        # IMU 噪声参数
        self.imunoise = ImuNoise()

        self.starttime = 0.0                  # 数据开始时间
        self.initAlignmentTime = 0            # 初始对齐时间

        self.robotbody_rotmat = np.eye(3, dtype=DT)   # 机器人机身旋转矩阵
        self.base_in_bodyimu = np.zeros(3, dtype=DT)  # 机器人基座在 IMU 坐标下的位置偏移

        self.robotpara = RobotPara()          # 机器人几何参数

(2)文件src/rot.py提供了欧拉角与方向余弦矩阵(CBN,Coordinate Body to Navigation)之间的转换函数,包括:

  1. euler2cbn:将欧拉角(roll, pitch, yaw)转换为方向余弦矩阵,用于将机体坐标系向量旋转到导航坐标系。
  2. matrix2euler:将方向余弦矩阵转换回欧拉角,用于从旋转矩阵恢复roll、pitch、yaw值。
python 复制代码
import numpy as np

def euler2cbn(euler_rpy: np.ndarray) -> np.ndarray:
    """
    将欧拉角 (roll, pitch, yaw) 转换为方向余弦矩阵 C_bn
    C_bn: body -> navigation
    """
    roll, pitch, yaw = float(euler_rpy[0]), float(euler_rpy[1]), float(euler_rpy[2])
    cr, sr = np.cos(roll),  np.sin(roll)
    cp, sp = np.cos(pitch), np.sin(pitch)
    cy, sy = np.cos(yaw),   np.sin(yaw)

    # 绕 X 轴旋转矩阵
    Rx = np.array([[1, 0, 0],
                   [0, cr, -sr],
                   [0, sr,  cr]], dtype=np.float64)
    # 绕 Y 轴旋转矩阵
    Ry = np.array([[ cp, 0, sp],
                   [ 0, 1, 0],
                   [-sp, 0, cp]], dtype=np.float64)
    # 绕 Z 轴旋转矩阵
    Rz = np.array([[cy, -sy, 0],
                   [sy,  cy, 0],
                   [ 0,  0, 1]], dtype=np.float64)
    
    # 按 Z-Y-X 顺序旋转,返回 body->navigation 的方向余弦矩阵
    return Rz @ Ry @ Rx

def matrix2euler(cbn: np.ndarray) -> np.ndarray:
    """
    将方向余弦矩阵 C_bn 转换为欧拉角 (roll, pitch, yaw)
    输出角度单位为弧度,范围 roll,pitch,yaw ∈ (-pi, pi]
    """
    roll  = np.arctan2(cbn[2,1], cbn[2,2])
    pitch = -np.arcsin(cbn[2,0])
    yaw   = np.arctan2(cbn[1,0], cbn[0,0])  # (-pi, pi]
    return np.array([roll, pitch, yaw], dtype=np.float64)

(3)文件src/dataloader.py定义了DataLoader 类,用于从ROS2 rosbag文件中读取Unitree Go2机器人低层状态消息,并将其转换为NumPy数组。主要功能包括:

  1. 注册自定义ROS消息类型(从unitree_go/msg文件夹读取.msg文件)。
  2. ROS时间戳转换为GPS时间(用于统一时间基准)。
  3. 将lowstate 消息转换为NumPy数组,包括IMU数据、关节角度和速度、足端力。
  4. 按顺序读取rosbag数据,生成完整的低层状态数组,供EKF和状态估计模块使用。
python 复制代码
class DataLoader:
    def __init__(self, config: Config, data_path: Path):
        self.config = config
        self.data_path = data_path
        self.add_types = {}

        # 注册自定义 ROS 消息类型
        go2_msg_folder = "./unitree_go/msg"
        for root, dirs, files in os.walk(go2_msg_folder):
            for file in files:
                if file.endswith(".msg"):
                    msgpath = Path(root) / file
                    msgdef  = msgpath.read_text(encoding="utf-8")

                    # 从 msg 文件解析类型并保存
                    self.add_types.update(get_types_from_msg(msgdef, self._guess_msgtype(msgpath)))

        register_types(self.add_types)

    def _guess_msgtype(self, path: Path) -> str:
        """根据路径猜测 ROS 消息类型名称"""
        name = path.relative_to(path.parents[2]).with_suffix("")
        if "msg" not in name.parts:
            name = name.parent / "msg" / name.name
        return str(PurePosixPath(name))

    def _rostime2gpst(self, cur_ts) -> float:
        """将 ROS 时间戳 (ns) 转换为 GPS 周内秒 (s)"""
        sec = cur_ts // 10**9
        nanosec = cur_ts % 10**9

        gps_epoch = datetime(1980, 1, 6)   # GPS 起始时间
        leap_seconds = 18                  # UTC 与 GPS 的闰秒差

        ros_datetime = datetime.fromtimestamp(sec + nanosec*1e-9)

        # 计算从 GPS epoch 到当前时间的总秒数
        delta = ros_datetime - gps_epoch
        total_seconds = delta.total_seconds()

        # 考虑本地时区偏移
        offset_seconds = (datetime.fromtimestamp(sec) - 
                          datetime.utcfromtimestamp(sec)).total_seconds()

        # 计算 GPS 周和周内秒
        gps_week = int(total_seconds // (7 * 24 * 3600))
        sow = total_seconds % (7 * 24 * 3600) - offset_seconds + leap_seconds

        return sow

    def _rosMsg2np(self, lowstate_msg, lowstate_dim, cur_ts):
        """将 ROS lowstate 消息转换为 NumPy 数组"""
        robot_sensor_np = np.zeros((1, lowstate_dim), dtype=np.float64)

        # ROS 时间戳转 GPS 时间
        ts = self._rostime2gpst(cur_ts)
        robot_sensor_np[0, 0] = ts

        # IMU 数据
        robot_sensor_np[0, 1:4] = np.array(lowstate_msg.imu_state.gyroscope)
        robot_sensor_np[0, 4:7] = -np.array(lowstate_msg.imu_state.accelerometer)  # 加速度取负

        # 电机位置 q
        robot_sensor_np[0, 7]  = lowstate_msg.motor_state[3].q
        robot_sensor_np[0, 8]  = lowstate_msg.motor_state[4].q
        robot_sensor_np[0, 9]  = lowstate_msg.motor_state[5].q
        robot_sensor_np[0, 10] = lowstate_msg.motor_state[0].q
        robot_sensor_np[0, 11] = lowstate_msg.motor_state[1].q
        robot_sensor_np[0, 12] = lowstate_msg.motor_state[2].q
        robot_sensor_np[0, 13] = lowstate_msg.motor_state[9].q
        robot_sensor_np[0, 14] = lowstate_msg.motor_state[10].q
        robot_sensor_np[0, 15] = lowstate_msg.motor_state[11].q
        robot_sensor_np[0, 16] = lowstate_msg.motor_state[6].q
        robot_sensor_np[0, 17] = lowstate_msg.motor_state[7].q
        robot_sensor_np[0, 18] = lowstate_msg.motor_state[8].q

        # 电机速度 dq
        robot_sensor_np[0, 19:31] = [
            lowstate_msg.motor_state[3].dq, lowstate_msg.motor_state[4].dq, lowstate_msg.motor_state[5].dq,
            lowstate_msg.motor_state[0].dq, lowstate_msg.motor_state[1].dq, lowstate_msg.motor_state[2].dq,
            lowstate_msg.motor_state[9].dq, lowstate_msg.motor_state[10].dq, lowstate_msg.motor_state[11].dq,
            lowstate_msg.motor_state[6].dq, lowstate_msg.motor_state[7].dq, lowstate_msg.motor_state[8].dq
        ]

        # 足端力
        robot_sensor_np[0, 31] = lowstate_msg.foot_force[1]
        robot_sensor_np[0, 32] = lowstate_msg.foot_force[0]
        robot_sensor_np[0, 33] = lowstate_msg.foot_force[3]
        robot_sensor_np[0, 34] = lowstate_msg.foot_force[2]

        return robot_sensor_np

    def load_data(self):
        """加载 rosbag 数据,并转换为 NumPy 数组"""
        bag = AnyReader([self.data_path])
        bag.open()

        datatopic = self.config.topic
        msg_counts = bag.topics[datatopic].msgcount
        data_connections = [x for x in bag.connections if x.topic == datatopic]
        lowstate_msgs = bag.messages(connections=data_connections)

        lowstate_dim = 35  # 1 timestamp + 6 IMU + 24 motor state + 4 foot force
        lowstate_np = np.zeros((msg_counts, lowstate_dim))

        for i in tqdm(range(msg_counts), total=msg_counts, desc="Loading bag", unit="msg"):
            try:
                lowstate_connection, cur_ts, lowstate_data = next(lowstate_msgs)  # 时间戳 ns

                # 反序列化 ROS 消息
                try:
                    lowstate_msg = deserialize_cdr(lowstate_data, lowstate_connection.msgtype)
                except Exception as e:
                    print(f"Error deserialize_cdr {i}: {e}")

                # 转为 NumPy 数组
                lowstate_np[i] = self._rosMsg2np(lowstate_msg, lowstate_dim, cur_ts)

            except Exception as e:
                print(f"Error processing message {i}: {e}")
                continue

        return lowstate_np

(4)文件src/loadanddump.py提供了文件读写与参数加载工具函数,包括:

  1. FileSaver类:将浮点数据按列写入文本文件,用于记录导航状态、IMU误差和标准差。
  2. loadConfig函数:从配置字典加载机器人初始状态、IMU噪声、几何参数和旋转矩阵,并填充到 Paras 对象中。
  3. writeNavResult函数:将当前导航状态(位置、速度、姿态)和IMU误差写入文件。
  4. writeSTD函数:将状态协方差的标准差写入文件,按不同量纲(位置、角度、偏置等)进行单位转换。

这些工具在EKF状态估计与数据保存模块中被频繁调用。

python 复制代码
class FileSaver:
    """用于将浮点数据按列写入文本文件"""
    def __init__(self, path: str, columns: int):
        self.f = open(path, "w", encoding="utf-8")
        self.columns = int(columns)

    def dump(self, data: List[float]):
        """写入一行数据,列数必须匹配"""
        if len(data) != self.columns:
            raise ValueError(f"FileSaver columns mismatch: expect {self.columns}, got {len(data)}")
        line = "".join(f"{float(x):<15.9f} " for x in data)
        self.f.write(line.rstrip() + "\n")

    def close(self):
        """关闭文件"""
        try:
            self.f.close()
        except Exception:
            pass

def loadConfig(config: dict, paras: Paras) -> bool:
    """从配置字典加载参数到 Paras 对象"""
    try:
        initposstd_vec = list(config["initposstd"])
        initvelstd_vec = list(config["initvelstd"])
        initattstd_vec = list(config["initattstd"])
    except Exception:
        print("加载初始状态标准差失败,请检查 position/velocity/attitude 的配置!", file=sys.stderr)
        return False

    # 设置初始状态标准差
    for i in range(3):
        paras.initstate_std.pos[i]   = float(initposstd_vec[i])
        paras.initstate_std.vel[i]   = float(initvelstd_vec[i])
        paras.initstate_std.euler[i] = float(initattstd_vec[i]) * D2R

    # IMU 噪声参数
    try:
        arw = float(config["imunoise"]["arw"])
        vrw = float(config["imunoise"]["vrw"])
        gbstd = float(config["imunoise"]["gbstd"])
        abstd = float(config["imunoise"]["abstd"])
        gsstd = float(config["imunoise"]["gsstd"])
        asstd = float(config["imunoise"]["asstd"])
        paras.imunoise.corr_time = float(config["imunoise"]["corrtime"])
    except Exception:
        print("加载 IMU 噪声失败,请检查配置!", file=sys.stderr)
        return False

    for i in range(3):
        paras.imunoise.gyr_arw[i] = arw   * (D2R / 60.0)
        paras.imunoise.acc_vrw[i] = vrw   / 60.0
        paras.imunoise.gyrbias_std[i]  = gbstd * (D2R / 3600.0)
        paras.imunoise.accbias_std[i]  = abstd * 1e-5
        paras.imunoise.gyrscale_std[i] = gsstd * 1e-6
        paras.imunoise.accscale_std[i] = asstd * 1e-6
        paras.initstate_std.imuerror.gyrbias[i]  = gbstd * (D2R / 3600.0)
        paras.initstate_std.imuerror.accbias[i]  = abstd * 1e-5
        paras.initstate_std.imuerror.gyrscale[i] = gsstd * 1e-6
        paras.initstate_std.imuerror.accscale[i] = asstd * 1e-6

    paras.imunoise.corr_time *= 3600.0  # 转换为秒
    paras.starttime = float(config["starttime"])
    paras.initAlignmentTime = int(config["initAlignmentTime"])

    # IMU 安装偏移
    base_in_bodyimu_vec = list(config["base_in_bodyimu"])
    paras.base_in_bodyimu = np.array(base_in_bodyimu_vec, dtype=np.float64)

    # 机器人几何参数
    paras.robotpara.ox = float(config["robotpara"]["ox"])
    paras.robotpara.oy = float(config["robotpara"]["oy"])
    paras.robotpara.ot = float(config["robotpara"]["ot"])
    paras.robotpara.lc = float(config["robotpara"]["lc"])
    paras.robotpara.lt = float(config["robotpara"]["lt"])

    # 机器人机身旋转矩阵
    robot_b_rotmat_vec = np.array(config["rotmat"], dtype=np.float64)
    if robot_b_rotmat_vec.size != 9:
        raise ValueError("rotmat 必须包含 9 个元素")
    paras.robotbody_rotmat = robot_b_rotmat_vec.reshape(3, 3, order="C")
    
    return True

def writeNavResult(time: float, navstate: NavState, navfile: FileSaver, imuerrfile: FileSaver):
    """写入导航状态和 IMU 误差"""
    # 导航状态
    result = []
    result.append(time)
    result.append(navstate.pos[0])
    result.append(navstate.pos[1])
    result.append(navstate.pos[2])
    result.append(navstate.vel[0])
    result.append(navstate.vel[1])
    result.append(navstate.vel[2])
    result.append(navstate.euler[0] * R2D)
    result.append(navstate.euler[1] * R2D)
    result.append(navstate.euler[2] * R2D)
    navfile.dump(result)

    # IMU 误差
    imuerr = navstate.imuerror
    result = []
    result.append(time)
    result.append(imuerr.gyrbias[0] * R2D * 3600.0)
    result.append(imuerr.gyrbias[1] * R2D * 3600.0)
    result.append(imuerr.gyrbias[2] * R2D * 3600.0)
    result.append(imuerr.accbias[0] * 1e5)
    result.append(imuerr.accbias[1] * 1e5)
    result.append(imuerr.accbias[2] * 1e5)
    result.append(imuerr.gyrscale[0] * 1e6)
    result.append(imuerr.gyrscale[1] * 1e6)
    result.append(imuerr.gyrscale[2] * 1e6)
    result.append(imuerr.accscale[0] * 1e6)
    result.append(imuerr.accscale[1] * 1e6)
    result.append(imuerr.accscale[2] * 1e6)
    imuerrfile.dump(result)

def writeSTD(time: float, cov: np.ndarray, stdfile: FileSaver):
    """将状态协方差的标准差写入文件,并进行单位转换"""
    result = []
    result.append(time)

    # 位置和速度标准差
    for i in range(0, 6):
        result.append(np.sqrt(max(cov[i, i], 0.0)))

    # 姿态欧拉角标准差 (弧度转度)
    for i in range(6, 9):
        result.append(np.sqrt(max(cov[i, i], 0.0)) * R2D)

    # 陀螺零偏标准差 (弧度转度每小时)
    for i in range(9, 12):
        result.append(np.sqrt(max(cov[i, i], 0.0)) * R2D * 3600.0)

    # 加速度偏置标准差
    for i in range(12, 15):
        result.append(np.sqrt(max(cov[i, i], 0.0)) * 1e5)

    # 比例误差标准差
    for i in range(15, 21):
        result.append(np.sqrt(max(cov[i, i], 0.0)) * 1e6)

    stdfile.dump(result)

(5)文件src/streams.py定义了从NumPy低层状态数组生成数据流的函数make_streams_from_lowstate,用于实现EKF或其他状态估计模块。

  1. imu_gen:生成IMU数据流(角速度、加速度、时间戳)。
  2. sensor_gen:生成机器人传感器数据流(关节角度、关节速度、足端力、时间戳)。

通过生成器方式返回两个可迭代对象,方便在状态估计循环中按时间顺序处理数据。

python 复制代码
import numpy as np
from .types import IMU, RobotSensor

def make_streams_from_lowstate(lowstate_np: np.ndarray, imu_rate_hz: float):
    """
    从 lowstate NumPy 数组生成 IMU 和 RobotSensor 数据流
    
    参数:
        lowstate_np: NumPy 数组,低层状态数据 (shape: [msg_count, 35])
        imu_rate_hz: IMU 采样率 (Hz)
    
    返回:
        imu_gen(): IMU 数据生成器
        sensor_gen(): RobotSensor 数据生成器
    """
    assert lowstate_np.shape[1] >= 35
    dt_default = 1.0 / float(imu_rate_hz)  # 默认时间间隔

    def imu_gen():
        """生成 IMU 数据流"""
        for row in lowstate_np:
            imu = IMU()
            ts = float(row[0])
            imu.timestamp = ts
            imu.angular_velocity = row[1:4].astype(np.float64).copy()  # 角速度
            imu.acceleration = row[4:7].astype(np.float64).copy()      # 加速度
            imu.dt = dt_default
            yield imu

    def sensor_gen():
        """生成 RobotSensor 数据流"""
        for row in lowstate_np:
            rs = RobotSensor()
            rs.timestamp = float(row[0])
            rs.joint_angular_position = row[7:19].astype(np.float64).copy()  # 电机位置 q
            rs.joint_angular_velocity = row[19:31].astype(np.float64).copy() # 电机速度 dq
            rs.footforce = row[31:35].astype(np.float64).copy()              # 足端力
            yield rs

    return imu_gen(), sensor_gen()

(6)文件src/vis.py提供了机器人轨迹可视化工具函数plot_traj,用于从保存的导航结果文件(文本文件)中读取位置数据,并在XY平面上绘制机器人的二维轨迹。它可以帮助快速直观地查看机器人运动路径,用于调试或结果分析。

python 复制代码
def plot_traj(traj_path: Path):
    """
    绘制机器人 XY 平面轨迹 (俯视图)
    
    参数:
        traj_path: 导航轨迹文件路径 (文本文件,列为 t px py pz roll pitch yaw)
    """
    if not traj_path.exists():
        raise FileNotFoundError(traj_path)

    # 加载数据,跳过第一行表头
    data = np.loadtxt(traj_path, skiprows=1)
    x, y = data[:, 1], data[:, 2]  # 提取 X 和 Y 坐标

    fig, ax = plt.subplots(figsize=(6, 6))
    ax.plot(x, y, lw=1.0)
    ax.set_xlabel('X [m]')
    ax.set_ylabel('Y [m]')
    ax.set_title('XY Trajectory_python (Top View)')
    ax.grid(True)

    ax.set_aspect('equal', adjustable='box')  # 保持 XY 比例一致

    # 自动计算坐标轴范围并加上边距
    dx, dy = (x.max()-x.min()), (y.max()-y.min())
    pad = 0.05 * max(dx, dy) if max(dx, dy) > 0 else 1.0
    ax.set_xlim(x.min()-pad, x.max()+pad)
    ax.set_ylim(y.min()-pad, y.max()+pad)

    plt.tight_layout()
    plt.show()
相关推荐
广州赛远2 小时前
LP130F防尘防油防护服质量解析-如何避开机器人防护的常见深坑
机器人
犀思云3 小时前
解构网络复杂性:基于 FusionWAN NaaS 的确定性架构工程实践与流量编排深度指南
网络·人工智能·机器人·智能仓储·专线
HyperAI超神经3 小时前
物理信息机器学习新突破!新型GNN架构可对复杂多体动力系统进行准确预测,赋能机器人/航空航天/材料科学
人工智能·深度学习·机器学习·架构·机器人·cpu·物理
视***间3 小时前
视程空间:以技术创新为翼,打造边缘计算全场景解决方案
大数据·人工智能·机器人·边缘计算
童话名剑5 小时前
YOLO v1(学习笔记)
人工智能·深度学习·yolo·目标检测
双星系统5 小时前
双臂机器人怎么选夹爪?
机器人
wang_chao1185 小时前
目标检测基础概念
人工智能·目标检测·目标跟踪
程序员小明儿5 小时前
具身智能:为什么人形机器人是AI的终极形态?
人工智能·机器人
鲁邦通物联网5 小时前
工业级 IoT 架构实战:基于边缘计算与分布式锁解决重载 agv机器人梯控 系统中的通讯死锁与并发冲突
机器人·机器人梯控·agv梯控·机器人乘梯·机器人自主乘梯·agv机器人梯控