PyRoboPath
是一个先进的、开源的 Python 库,致力于为学术研究人员、行业工程师以及机器人爱好者提供一套完整、高效、易用且可扩展的机器人路径规划解决方案。它不仅仅是一个算法的集合,更是一个集成了机器人建模、环境表示、碰撞检测、多样化路径规划算法、路径平滑与优化、以及可视化工具的综合性框架。
PyRoboPath
的设计哲学基于以下核心理念:
PyRoboPath
提供了简洁直观的 API 接口,使得用户即使没有深厚的路径规划背景也能快速上手。同时,核心算法和数据结构经过精心优化,以确保在处理复杂问题时仍能保持较高的运算效率。Python 的易读性和丰富的生态系统进一步增强了其易用性。PyRoboPath
内置了多种主流和前沿的路径规划算法,包括但不限于基于采样的规划器(如 PRM, RRT, RRT*)、基于搜索的规划器(如 A*, D* Lite)、以及优化类规划器(如 CHOMP, STOMP 的概念实现)。它支持不同类型的机器人(如机械臂、移动机器人、无人机)在二维或三维空间中的路径规划。PyRoboPath
强调与主流仿真环境(如 PyBullet, Gazebo)的集成能力,并内置了灵活的可视化工具,能够清晰展示机器人、环境、构型空间以及规划出的路径。PyRoboPath
鼓励全球开发者和研究者的贡献与合作,共同推动机器人路径规划技术的发展和应用。PyRoboPath
旨在服务于广泛的用户群体:
一个理想化的 PyRoboPath
库会包含以下核心功能模块:
pyrobopath.core
: 核心数据结构与定义
State
: 表示机器人状态(例如,关节角度、位置姿态)。Path
: 表示一系列状态组成的路径。Space
: 定义状态空间,包括边界、度量衡等。Transformations
: 提供姿态表示(旋转矩阵、四元数、欧拉角)和坐标变换工具。pyrobopath.robots
: 机器人建模与运动学
RobotModel
: 基类,用于描述机器人的几何和运动学特性。SerialLinkArm
: 用于串联机械臂的模型,支持 DH 参数或 URDF 解析。MobileRobot
: 用于移动机器人的模型(如差分驱动、全向轮)。Kinematics
: 正向运动学和逆向运动学求解器接口。pyrobopath.environments
: 环境建模与障碍物表示
Environment
: 描述机器人工作环境的容器。Obstacle
: 基类,表示障碍物。Box
, Sphere
, Cylinder
, MeshObstacle
:具体的障碍物几何形状。PointCloudObstacle
: 基于点云数据的障碍物表示。OctreeEnvironment
: 基于八叉树的环境表示,用于高效查询。pyrobopath.collision_detection
: 碰撞检测模块
CollisionChecker
: 碰撞检测器基类。DiscreteCollisionChecker
: 基于离散点对检测。ContinuousCollisionChecker
: 连续碰撞检测,用于检测路径段上的碰撞。DistanceQueries
: 提供计算机器人与障碍物之间最小距离的功能。pyrobopath.planners
: 路径规划算法
samplers
: 构型空间采样器
UniformSampler
, GaussianSampler
, BridgeSampler
, ObstacleBasedSampler
graph_based
: 基于图搜索的规划器
Dijkstra
, AStar
(A*)VisibilityGraph
sampling_based
: 基于采样的规划器
PRM
(Probabilistic Roadmap)LazyPRM
RRT
(Rapidly-exploring Random Tree)RRTConnect
RRTStar
(RRT*)InformedRRTStar
BiRRT
, LBTRRT
optimization_based
: 基于优化的规划器 (概念接口)
CHOMPInterface
STOMPInterface
TrajectoryOptimizer
PlannerBase
: 所有规划器的基类,定义通用接口如 plan(start_state, goal_state)
。pyrobopath.path_processing
: 路径后处理
PathSmoother
: 路径平滑算法(如快捷键平滑、B样条平滑)。PathShortener
: 路径缩短。PathInterpolator
: 路径插值,生成更密集的路径点。pyrobopath.visualization
: 可视化工具
Visualizer2D
: 二维场景和路径可视化。Visualizer3D
: 三维场景和路径可视化,可能基于 Matplotlib, PyVista, Open3D 或与 PyBullet/MeshCat 集成。pyrobopath.utils
: 实用工具
Logger
: 日志记录工具。Timer
: 性能分析计时器。ConfigLoader
: 配置文件加载。IOUtils
: 文件读写,如导入/导出路径、场景等。pyrobopath.io
: 输入/输出与格式支持
URDFParser
: 解析URDF文件以构建机器人模型。SDFParser
: 解析SDF文件。SceneFileHandler
: 加载/保存场景描述文件(如自定义XML/JSON格式,或更标准的如COLLADA)。PyRoboPath
与其他相关库的关系(概念性)PyRoboPath
可以作为 ROS 中路径规划节点的核心逻辑库。它可以与 ROS 的消息传递、参数服务器、TF变换等系统集成,接收来自感知模块的目标和环境信息,并将规划结果发布给运动执行模块。PyRoboPath
可以被视为提供了一种替代的或补充的规划器集合。高级用户甚至可以将 PyRoboPath
中的特定算法封装成 MoveIt 插件。PyRoboPath
的设计目标是提供一个纯 Python 或 Python 友好的接口,同时可能在底层高效部分(如碰撞检测)选择性地绑定成熟的 C++ 库(类似 OMPL 的某些组件或 FCL)。PyRoboPath
更强调 Python 生态的整合和易用性。PyRoboPath
可以与它们紧密集成,利用它们的物理引擎进行精确的碰撞检测、运动学/动力学仿真,并在这些环境中测试和验证规划算法。PyRoboPath
大量依赖这些基础科学计算库进行矩阵运算、数值优化、几何计算等。由于 PyRoboPath
是我们为了深度探讨而构建的框架,我们将假设一个理想的安装和使用流程。
安装 (假设通过 pip):
pip install pyrobopath
基本使用示例 (概念性):
# 导入 PyRoboPath 的核心模块
# from pyrobopath.robots import SerialLinkArm # 导入串联机械臂类
# from pyrobopath.environments import Environment, Box # 导入环境类和立方体障碍物类
# from pyrobopath.planners import RRTConnect # 导入 RRT-Connect 规划器
# from pyrobopath.core import State, Path # 导入状态和路径类
# from pyrobopath.visualization import Visualizer3D # 导入3D可视化工具 (假设)
# from pyrobopath.utils import Logger # 导入日志工具
# # 1. 初始化日志记录器 (运维精髓:良好的日志是调试和监控的基础)
# logger = Logger(level=Logger.INFO) # 设置日志级别为 INFO
# logger.info("PyRoboPath Demo: 开始执行路径规划任务") # 记录一条信息日志
# # 2. 定义机器人模型 (假设从 URDF 文件加载一个7自由度机械臂)
# try:
# # robot_urdf_path = "path/to/your/robot.urdf" # 定义URDF文件路径
# # robot = SerialLinkArm.from_urdf(robot_urdf_path) # 从URDF文件加载机器人模型
# # logger.info(f"机器人模型 '{robot.name}' 加载成功,拥有 {robot.num_dof} 个自由度。") # 记录机器人加载信息
# except FileNotFoundError:
# # logger.error(f"机器人URDF文件未找到: {robot_urdf_path}") # 记录错误日志
# # exit() # 退出程序
# pass # 暂时跳过,实际应用中应处理
# # 3. 创建并配置环境
# env = Environment(dimensions=3) # 创建一个三维环境
# # 添加一个立方体障碍物:尺寸为 (0.5, 0.5, 0.5) 米,中心位于 (1.0, 0.0, 0.5) 米
# obstacle1 = Box(size=[0.5, 0.5, 0.5], position=[1.0, 0.0, 0.5]) # 创建障碍物实例
# env.add_obstacle(obstacle1, name="CubeObstacle1") # 将障碍物添加到环境中,并命名
# logger.info(f"环境创建成功,并添加了障碍物: {obstacle1.name}") # 记录环境创建信息
# # 4. 定义起始和目标构型 (对于7自由度机械臂,是7个关节角度)
# # start_q = State([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) # 定义起始关节角度
# # goal_q = State([1.5, 0.5, -1.0, 0.8, 0.0, -0.5, 0.2]) # 定义目标关节角度
# # logger.info(f"起始构型: {start_q.values}") # 记录起始构型
# # logger.info(f"目标构型: {goal_q.values}") # 记录目标构型
# # 5. 初始化路径规划器
# # planner = RRTConnect(robot=robot, environment=env) # 初始化RRT-Connect规划器
# # planner.set_parameters(step_size=0.1, max_iterations=5000, goal_bias=0.1) # 设置规划器参数
# # logger.info(f"路径规划器 {type(planner).__name__} 初始化完成。") # 记录规划器初始化信息
# # 6. 执行路径规划
# logger.info("开始路径规划...") # 记录规划开始信息
# # path_solution: Path = planner.plan(start_q, goal_q) # 调用plan方法进行规划
# # 7. 处理规划结果
# # if path_solution and path_solution.is_valid(): # 检查路径是否找到且有效
# # logger.info(f"路径规划成功!路径长度: {path_solution.length():.2f}, 包含 {len(path_solution.states)} 个状态点。") # 记录成功信息
# # # 你可以在这里对路径进行平滑、插值等后处理
# # # smoothed_path = PyRoboPath.path_processing.shortcut_smoother(path_solution, robot, env, iterations=50)
# # # logger.info(f"路径平滑后长度: {smoothed_path.length():.2f}")
# # # 8. 可视化 (假设)
# # # viz = Visualizer3D() # 创建3D可视化器实例
# # # viz.add_robot(robot, start_q) # 添加机器人模型到可视化器
# # # viz.add_environment(env) # 添加环境和障碍物
# # # viz.plot_path(path_solution, color='green', linewidth=3) # 绘制规划出的路径
# # # # viz.plot_path(smoothed_path, color='blue', linewidth=3) # 绘制平滑后的路径
# # # viz.show() # 显示可视化窗口
# # else:
# # logger.warning("路径规划失败或未找到有效路径。") # 记录警告信息
# # # 可以尝试增加迭代次数、调整参数或使用其他规划器
# # # planner_prm = PyRoboPath.planners.PRM(robot=robot, environment=env)
# # # ...
# logger.info("PyRoboPath Demo: 执行结束。") # 记录结束信息
上述代码是一个高度概念化的演示,展示了使用 PyRoboPath
进行一次完整路径规划任务可能涉及的步骤。每个步骤都伴随着日志记录,这体现了“运维精髓”中对系统状态和行为监控的重要性。实际的 PyRoboPath
API 和类名可能会有所不同,但这提供了一个清晰的结构。
在接下来的章节中,我们将逐一深入探讨机器人路径规划的各个方面,并结合 PyRoboPath
的(概念性)实现细节和更丰富的代码示例。
在深入 PyRoboPath
的内部机制之前,理解机器人路径规划领域的一些核心概念至关重要。这些概念是所有路径规划算法和系统的基石。
在路径规划的语境下,机器人通常被抽象为其运动学模型。关键属性包括:
PyRoboPath
中的 RobotModel
类会封装这些信息。q = [θ₁, θ₂, ..., θₙ]
。对于移动机器人,可能是 [x, y, θ]
。q
,计算其末端执行器(或其他关键点)在笛卡尔空间中的位置和姿态。PyRoboPath
的 RobotModel
会提供 fk(q)
方法。q
。这是一个更复杂的问题,可能没有解、有唯一解或有无穷多解。PyRoboPath
会提供 ik(target_pose)
方法接口,其实现可能涉及数值优化或解析解。PyRoboPath
可能会有专门的模块或接口来处理动力学约束。工作空间是指机器人末端执行器能够到达的所有点的集合。
PyRoboPath
中,虽然不直接计算整个工作空间(计算成本高),但在进行可达性分析或IK求解时会隐式地利用这些概念。构型空间是路径规划中最为核心的概念之一。它是由机器人所有可能构型组成的N维空间,其中N是机器人的自由度。
为什么引入C-space?
将路径规划问题从复杂几何形状的机器人在充满障碍物的工作空间中运动,转化为一个“点”机器人在C-space中的障碍物区域内运动。这极大地简化了问题的表述和求解。
自由构型空间 (Free C-space, C_free): C-space 中所有机器人不与任何障碍物发生碰撞的构型集合。路径规划的目标就是在 C_free 中找到一条从起始构型到目标构型的路径。
C_free = {q ∈ C | Robot(q) ∩ Obstacles = ∅}
这里的 Robot(q)
表示机器人在构型 q
时所占据的工作空间区域,Obstacles
表示工作空间中所有障碍物占据的区域。
障碍物构型空间 (Obstacle C-space, C_obs): C-space 中所有机器人与至少一个障碍物发生碰撞的构型集合。
C_obs = C \ C_free
在 PyRoboPath
中,Space
对象(例如 ConfigurationSpace
类)会定义构型的维度、边界、以及可能的度量函数。碰撞检测模块 (CollisionChecker
) 的核心功能就是判断一个给定的构型 q
是否属于 C_free
(即 is_collision_free(q)
)。
障碍物是工作空间中限制机器人运动的物体。
PyRoboPath
的 environments
模块会提供多种 Obstacle
子类来表示不同类型的障碍物,并能将它们组织在 Environment
对象中。碰撞检测是判断机器人在特定构型下或沿某段路径运动时是否与环境中的障碍物发生接触的过程。这是路径规划中计算开销最大的部分之一。
q
时是否发生碰撞。PyRoboPath
的 CollisionChecker.is_collision_free_state(q)
方法会执行此操作。q₁
运动到 q₂
的一段路径 p(s)
(其中 s ∈ [0,1]
) 上是否始终无碰撞。这比离散检测更复杂,因为机器人是连续运动的。通常通过对路径进行离散化采样并对每个采样点进行离散碰撞检测,或者使用更高级的基于扫掠体积的方法。PyRoboPath
的 CollisionChecker.is_collision_free_path(q1, q2)
或 CollisionChecker.is_collision_free_path(path_segment)
方法会执行此操作。高效的碰撞检测算法和数据结构(如包围盒层次结构 BVH、KD树、八叉树)对于规划性能至关重要。PyRoboPath
可能会封装如 FCL (Flexible Collision Library) 这样的专用C++库来加速碰撞检测。
路径 (Path): 路径是一个纯几何概念,表示从起始构型到目标构型的一系列连续构型。它只关心“去哪里”,不关心“何时去”或“多快去”。在 PyRoboPath
中,Path
对象通常存储为一个状态点列表或一个参数化曲线函数。
P: [0, 1] → C_free
,使得 P(0) = q_start
且 P(1) = q_goal
。
轨迹 (Trajectory): 轨迹是路径加上时间信息。它指定了机器人在每个时间点应该处于的构型(以及可能的速度和加速度)。轨迹描述了“如何以及何时”执行路径。
T: [t_0, t_f] → C_free
,其中 q(t)
表示时间 t
时的构型。
生成轨迹通常是在找到几何路径之后的一个独立步骤,称为“路径参数化”或“轨迹生成”,需要考虑机器人的动力学约束、速度和加速度限制。PyRoboPath
的 path_processing
模块或一个专门的 trajectory_generation
模块会处理这个问题。
给定:
q_start ∈ C_free
。q_goal ∈ C_free
(或一个目标区域 G ⊂ C_free
)。求解:
找到一条连续的路径 P
,使得:
P(0) = q_start
(路径从起始构型开始)。P(1) = q_goal
(路径在目标构型结束)。s ∈ [0, 1]
,P(s) ∈ C_free
(路径全程无碰撞)。一个路径规划算法的性能通常通过以下几个方面来评价:
完备性 (Completeness):
PyRoboPath
中的许多规划器(如RRT系列)都是概率完备的。最优性 (Optimality):
PyRoboPath
的 RRT* 就是一个例子。计算效率 (Computational Efficiency):
路径质量 (Path Quality):
PyRoboPath
的 path_processing
模块用于改善初始路径的质量。鲁棒性 (Robustness):
路径规划算法种类繁多,可以从不同角度进行分类。PyRoboPath
致力于提供各类代表性算法的实现。
基于图搜索的方法 (Graph Search Methods):
PyRoboPath
的 planners.graph_based
模块会包含这些算法。基于随机采样的方法 (Sampling-Based Methods):
q_start
到q_goal
的路径。适用于多查询场景。q_start
开始逐步生长一棵树,每次迭代时在C-space中随机采样一点,并从树中选择最近的节点向该采样点扩展一小步。如果扩展无碰撞,则将新节点和边加入树中。目标是使树尽快探索到q_goal
。适用于单查询场景。PyRoboPath
的 planners.sampling_based
模块是其核心之一。势场法 (Potential Field Methods):
PyRoboPath
可能会提供基本的势场法实现,或作为局部规划器使用。优化方法 (Optimization-Based Methods):
PyRoboPath
的 planners.optimization_based
模块将提供这类算法的接口。精确细胞分解法 (Exact Cell Decomposition):
PyRoboPath
的设计目标是灵活组合和扩展这些不同类型的算法,以适应各种复杂的应用需求。在后续章节中,我们将详细剖析其中一些关键算法的原理、在PyRoboPath
中的概念性实现、以及它们的使用场景。
机器人路径规划本质上是一个几何问题,其描述、分析和求解都离不开坚实的数学和几何基础。PyRoboPath
在其内部实现中会广泛使用这些工具。本章将概述其中最关键的一些概念。
描述刚体在三维空间中的位置和姿态(合称位姿,Pose)是机器人学的基础。位置通常用一个三维向量 p = [x, y, z]ᵀ
表示。姿态(方向)的表示则更为复杂。
旋转矩阵 (Rotation Matrix, R):
R Rᵀ = Rᵀ R = I
) 且行列式为1 (det(R) = 1
)。R ∈ SO(3)
(特殊正交群)。p' = R p
)。PyRoboPath
内部会大量使用旋转矩阵进行坐标变换。例如,pyrobopath.core.Transformations.rotation_matrix_to_euler(R)
。import numpy as np # 导入NumPy库,用于进行科学计算,特别是多维数组和矩阵运算
# # 假设的 PyRoboPath 变换工具模块
# class PyRoboPathTransformations:
# @staticmethod
# def euler_to_rotation_matrix(roll, pitch, yaw, order='xyz'):
# """
# 将欧拉角转换为旋转矩阵 (RPY - x-y-z顺序)
# roll: 绕x轴旋转角度 (弧度)
# pitch: 绕y轴旋转角度 (弧度)
# yaw: 绕z轴旋转角度 (弧度)
# order: 欧拉角旋转顺序,默认为 'xyz'
# 返回: 3x3 的旋转矩阵
# """
# Rx = np.array([[1, 0, 0], # 定义绕x轴旋转的基础矩阵
# [0, np.cos(roll), -np.sin(roll)],
# [0, np.sin(roll), np.cos(roll)]])
# Ry = np.array([[np.cos(pitch), 0, np.sin(pitch)], # 定义绕y轴旋转的基础矩阵
# [0, 1, 0],
# [-np.sin(pitch), 0, np.cos(pitch)]])
# Rz = np.array([[np.cos(yaw), -np.sin(yaw), 0], # 定义绕z轴旋转的基础矩阵
# [np.sin(yaw), np.cos(yaw), 0],
# [0, 0, 1]])
# if order.lower() == 'xyz': # R = Rx * Ry * Rz (后乘,等效于先绕固定Z,再绕固定Y,最后绕固定X)
# # 或者等效于先绕动坐标系的x, 再绕动坐标系的y', 最后绕动坐标系的z''
# # 不同的库和教材对于欧拉角旋转顺序的定义可能不同,这里采用一种常见的RPY约定
# R = Rz @ Ry @ Rx # 按照 ZYX 的顺序应用旋转(因为是内旋)或者 XYZ 外旋
# # 注意:标准的RPY (roll, pitch, yaw) 通常指绕固定轴X, Y, Z的顺序旋转,
# # 或者等效地绕活动轴Z, Y', X''的顺序。 R = Rz(yaw)Ry(pitch)Rx(roll)
# # 为了符合常见 X-Y-Z 航空欧拉角 (Tait-Bryan angles)的 R = Rz Ry Rx 约定:
# R = Rz @ Ry @ Rx
# elif order.lower() == 'zyx': # R = Rz * Ry * Rx
# R = Rx @ Ry @ Rz # 这里的实现可能需要根据具体约定调整,常见的是RzRyRx
# # ... 其他顺序可以类似添加 ...
# else:
# raise ValueError("不支持的欧拉角顺序: {}".format(order)) # 如果顺序无效则抛出错误
# return R # 返回计算得到的旋转矩阵
# @staticmethod
# def is_rotation_matrix(R, tol=1e-6):
# """
# 检查一个矩阵是否为有效的旋转矩阵
# R: 输入的3x3矩阵
# tol: 容差,用于浮点数比较
# 返回: True 如果是旋转矩阵,否则 False
# """
# if not isinstance(R, np.ndarray) or R.shape != (3,3): # 检查是否为NumPy数组且形状为3x3
# return False # 如果不是,则返回False
# # 检查 R R^T = I
# should_be_identity = R @ R.T # 计算 R 乘以 R的转置
# identity = np.identity(3) # 创建一个3x3的单位矩阵
# if not np.allclose(should_be_identity, identity, atol=tol): # 检查结果是否接近单位矩阵
# return False # 如果不接近,则返回False
# # 检查 det(R) = 1
# if not np.isclose(np.linalg.det(R), 1.0, atol=tol): # 检查行列式是否接近1
# return False # 如果不接近,则返回False
# return True # 所有检查通过,返回True
# # 示例使用
# roll_angle = np.deg2rad(30) # 将30度转换为弧度,作为roll角
# pitch_angle = np.deg2rad(45) # 将45度转换为弧度,作为pitch角
# yaw_angle = np.deg2rad(60) # 将60度转换为弧度,作为yaw角
# R_xyz = PyRoboPathTransformations.euler_to_rotation_matrix(roll_angle, pitch_angle, yaw_angle, order='xyz') # 计算旋转矩阵
# print("旋转矩阵 (RPY ZYX):") # 打印提示信息
# print(R_xyz) # 打印旋转矩阵
# print(f"是否为有效的旋转矩阵? {PyRoboPathTransformations.is_rotation_matrix(R_xyz)}") # 验证矩阵有效性
欧拉角 (Euler Angles):
PyRoboPath
会提供欧拉角与旋转矩阵/四元数之间的转换函数,并要求用户明确旋转顺序。q_euler = PyRoboPathTransformations.rotation_matrix_to_euler(R, order='xyz')
R = PyRoboPathTransformations.euler_to_rotation_matrix(roll, pitch, yaw, order='xyz')
轴角表示 (Axis-Angle Representation):
k = [kx, ky, kz]ᵀ
(通常为单位向量) 和一个旋转角度 θ
来表示姿态。物理意义清晰:绕轴 k
旋转 θ
角度。θk
,称为旋转向量,其模为 θ
)。θ=0
时旋转轴无定义 (奇异性)。直接组合两次旋转不如旋转矩阵或四元数方便。R = PyRoboPathTransformations.axis_angle_to_rotation_matrix(axis, angle)
axis, angle = PyRoboPathTransformations.rotation_matrix_to_axis_angle(R)
四元数 (Quaternion, q):
q = [w, x, y, z]
或 q = w + xi + yj + zk
,其中 w
是实部,[x, y, z]
是虚部向量。对于单位四元数(用于表示旋转),满足 w² + x² + y² + z² = 1
。w = cos(θ/2)
, [x, y, z] = sin(θ/2) * k
。q
和 -q
两个单位四元数表示(双倍覆盖 SO(3) 到 S³ 的映射)。PyRoboPath
推荐在高频姿态运算和插值中使用四元数,并提供与其他表示的转换。quat = PyRoboPathTransformations.rotation_matrix_to_quaternion(R)
R = PyRoboPathTransformations.quaternion_to_rotation_matrix(quat)
# import numpy as np # NumPy已在前面导入
# # 假设的 PyRoboPath 变换工具模块 (扩展)
# class PyRoboPathTransformations: # 此处为了演示,重新定义,实际应为同一个类
# # ... (前面定义的 euler_to_rotation_matrix, is_rotation_matrix 方法) ...
# @staticmethod
# def quaternion_to_rotation_matrix(q):
# """
# 将单位四元数转换为旋转矩阵。
# q: 四元数 [w, x, y, z] (NumPy数组或列表)
# 返回: 3x3 旋转矩阵
# """
# q = np.asarray(q, dtype=float) # 确保为NumPy浮点数组
# if not np.isclose(np.linalg.norm(q), 1.0): # 检查是否为单位四元数
# # logger.warning("输入的四元数不是单位四元数,结果可能不正确。建议归一化。") # 实际库中应有日志
# q = q / np.linalg.norm(q) # 如果不是,则进行归一化处理
# w, x, y, z = q[0], q[1], q[2], q[3] # 提取四元数的各个分量
# # 计算旋转矩阵的元素
# R = np.array([
# [1 - 2*y**2 - 2*z**2, 2*x*y - 2*w*z, 2*x*z + 2*w*y], # R11, R12, R13
# [2*x*y + 2*w*z, 1 - 2*x**2 - 2*z**2, 2*y*z - 2*w*x], # R21, R22, R23
# [2*x*z - 2*w*y, 2*y*z + 2*w*x, 1 - 2*x**2 - 2*y**2] # R31, R32, R33
# ])
# return R # 返回计算得到的旋转矩阵
# @staticmethod
# def rotation_matrix_to_quaternion(R):
# """
# 将旋转矩阵转换为单位四元数 [w, x, y, z]。
# R: 3x3 旋转矩阵
# 返回: NumPy数组表示的四元数 [w, x, y, z]
# """
# # if not PyRoboPathTransformations.is_rotation_matrix(R): # 检查输入是否为有效旋转矩阵
# # raise ValueError("输入的不是一个有效的旋转矩阵") # 如果无效,则抛出错误
# tr = np.trace(R) # 计算矩阵的迹
# if tr > 0: # 当迹大于0时,w分量最大
# S = np.sqrt(tr + 1.0) * 2 # S = 4w
# w = 0.25 * S # 计算w分量
# x = (R[2, 1] - R[1, 2]) / S # 计算x分量
# y = (R[0, 2] - R[2, 0]) / S # 计算y分量
# z = (R[1, 0] - R[0, 1]) / S # 计算z分量
# elif (R[0, 0] > R[1, 1]) and (R[0, 0] > R[2, 2]): # 当R00是主对角线上最大元素时,x分量最大
# S = np.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2]) * 2 # S = 4x
# w = (R[2, 1] - R[1, 2]) / S # 计算w分量
# x = 0.25 * S # 计算x分量
# y = (R[0, 1] + R[1, 0]) / S # 计算y分量
# z = (R[0, 2] + R[2, 0]) / S # 计算z分量
# elif R[1, 1] > R[2, 2]: # 当R11是主对角线上最大元素时 (且R00不最大),y分量最大
# S = np.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2]) * 2 # S = 4y
# w = (R[0, 2] - R[2, 0]) / S # 计算w分量
# x = (R[0, 1] + R[1, 0]) / S # 计算x分量
# y = 0.25 * S # 计算y分量
# z = (R[1, 2] + R[2, 1]) / S # 计算z分量
# else: # 当R22是主对角线上最大元素时,z分量最大
# S = np.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1]) * 2 # S = 4z
# w = (R[1, 0] - R[0, 1]) / S # 计算w分量
# x = (R[0, 2] + R[2, 0]) / S # 计算x分量
# y = (R[1, 2] + R[2, 1]) / S # 计算y分量
# z = 0.25 * S # 计算z分量
# return np.array([w, x, y, z]) / np.linalg.norm([w,x,y,z]) # 返回归一化后的四元数
# @staticmethod
# def slerp(q1, q2, t):
# """
# 球面线性插值 (Slerp) 两个单位四元数。
# q1, q2: 起始和结束四元数 [w, x, y, z] (单位四元数)
# t: 插值参数, 0 <= t <= 1
# 返回: 插值得到的四元数
# """
# q1 = np.asarray(q1, dtype=float) # 转换为NumPy数组
# q2 = np.asarray(q2, dtype=float) # 转换为NumPy数组
# # 确保 q1 和 q2 是单位四元数 (实际应用中应在输入时就保证)
# # q1 /= np.linalg.norm(q1)
# # q2 /= np.linalg.norm(q2)
# dot_product = np.dot(q1, q2) # 计算两个四元数的点积
# # 如果点积为负,反转其中一个四元数以选择最短路径
# if dot_product < 0.0:
# q1 = -q1 # 反转q1
# dot_product = -dot_product # 点积也反号
# # 防止因浮点误差导致 dot_product > 1
# dot_product = np.clip(dot_product, -1.0, 1.0) # 将点积限制在[-1, 1]区间
# theta_0 = np.arccos(dot_product) # 计算两个四元数之间的夹角
# sin_theta_0 = np.sin(theta_0) # 计算夹角的正弦值
# if np.abs(sin_theta_0) < 1e-6: # 如果夹角非常小 (接近0或pi),则使用线性插值
# # 此时 q1 和 q2 非常接近或相反,Slerp退化为Lerp
# return q1 # 当t=0时,返回q1;当t=1时,若q1与q2同向则应为q2,若反向则q2已被反转,也为q2
# # 更精确的线性插值: (1-t)*q1 + t*q2 然后归一化。但通常q1,q2非常接近时直接返回q1或q2之一即可
# # Slerp公式
# scale_q1 = np.sin((1.0 - t) * theta_0) / sin_theta_0 # 计算q1的缩放因子
# scale_q2 = np.sin(t * theta_0) / sin_theta_0 # 计算q2的缩放因子
# q_interp = scale_q1 * q1 + scale_q2 * q2 # 计算插值后的四元数
# return q_interp / np.linalg.norm(q_interp) # 返回归一化后的插值四元数
# # 示例使用
# # R_identity = np.identity(3) # 创建一个单位旋转矩阵 (无旋转)
# # q_identity = PyRoboPathTransformations.rotation_matrix_to_quaternion(R_identity) # 转换为四元数
# # print(f"单位旋转矩阵对应的四元数: {q_identity}") # 打印结果,应为 [1, 0, 0, 0] 或 [-1, 0, 0, 0]
# # R_test = PyRoboPathTransformations.euler_to_rotation_matrix(np.pi/2, 0, 0) # 绕x轴旋转90度
# # q_test_from_R = PyRoboPathTransformations.rotation_matrix_to_quaternion(R_test) # 转换
# # print(f"绕X轴90度旋转矩阵对应的四元数: {q_test_from_R}") # 打印结果
# # # 理论值: cos(pi/4) + i*sin(pi/4) = [0.707, 0.707, 0, 0]
# # R_from_q_test = PyRoboPathTransformations.quaternion_to_rotation_matrix(q_test_from_R) # 从四元数转回旋转矩阵
# # print("从四元数转回的旋转矩阵:") # 打印提示
# # print(R_from_q_test) # 打印矩阵
# # print(f"与原旋转矩阵是否接近: {np.allclose(R_test, R_from_q_test)}") # 比较是否一致
# # Slerp 示例
# # q_start_slerp = np.array([1.0, 0.0, 0.0, 0.0]) # 定义Slerp起始四元数 (无旋转)
# # q_end_slerp_axis_angle = (np.array([0,0,1]), np.pi/2) # 目标:绕z轴旋转90度
# # w_end = np.cos(np.pi/4) # cos(theta/2)
# # xyz_end = np.sin(np.pi/4) * np.array([0,0,1]) # sin(theta/2) * axis
# # q_end_slerp = np.array([w_end, xyz_end[0], xyz_end[1], xyz_end[2]]) # 定义Slerp结束四元数
# # print(f"Slerp 起始四元数: {q_start_slerp}") # 打印起始四元数
# # print(f"Slerp 结束四元数 (绕Z轴90度): {q_end_slerp}") # 打印结束四元数
# # for t_interp in [0.0, 0.25, 0.5, 0.75, 1.0]: # 遍历不同的插值参数t
# # q_interpolated = PyRoboPathTransformations.slerp(q_start_slerp, q_end_slerp, t_interp) # 执行球面线性插值
# # print(f"t = {t_interp:.2f}, 插值四元数: {np.round(q_interpolated, 4)}") # 打印插值结果,保留4位小数
# # # 当 t=0.5 时,应为绕Z轴旋转45度
# # # w_mid = np.cos(np.pi/8) approx 0.9239
# # # z_mid = np.sin(np.pi/8) approx 0.3827
# # # [0.9239, 0, 0, 0.3827]
齐次变换矩阵将刚体的旋转和平移合并到一个4x4矩阵中,非常方便地表示位姿和进行坐标系之间的变换。
T ∈ SE(3)
(特殊欧几里得群)。
T = [[R, p], [0, 1]]
其中 R
是3x3旋转矩阵,p
是3x1平移向量,[0, 1]
是 [0, 0, 0, 1]
的简写。
ᵀA_B
。p_A = ᵀA_B * p_B
(其中 p_A
和 p_B
都是齐次坐标 [x, y, z, 1]ᵀ
)。ᵀA_C = ᵀA_B * ᵀB_C
。(ᵀA_B)⁻¹ = ᵀB_A = [[Rᵀ, -Rᵀp], [0, 1]]
。PyRoboPath
会广泛使用齐次变换矩阵来计算机器人各连杆的位姿(正向运动学)、传感器数据到机器人基座的转换、物体在世界坐标系中的位姿等。
# import numpy as np # NumPy已在前面导入
# # 假设的 PyRoboPath 变换工具模块 (扩展)
# class PyRoboPathTransformations:
# # ... (前面定义的姿态相关方法) ...
# @staticmethod
# def create_homogeneous_transform(R, p):
# """
# 根据旋转矩阵 R 和平移向量 p 创建一个4x4齐次变换矩阵 T.
# R: 3x3 旋转矩阵
# p: 3x1 或 1x3 或 3-element list/array 平移向量 [x, y, z]
# 返回: 4x4 齐次变换矩阵
# """
# T = np.identity(4) # 初始化一个4x4单位矩阵
# T[:3, :3] = R # 将旋转矩阵R赋给T的左上角3x3部分
# T[:3, 3] = np.asarray(p).flatten() # 将平移向量p赋给T的前三行最后一列
# return T # 返回创建的齐次变换矩阵
# @staticmethod
# def get_rotation_from_transform(T):
# """
# 从齐次变换矩阵 T 中提取旋转矩阵 R.
# T: 4x4 齐次变换矩阵
# 返回: 3x3 旋转矩阵
# """
# return T[:3, :3] # 返回T的左上角3x3部分
# @staticmethod
# def get_translation_from_transform(T):
# """
# 从齐次变换矩阵 T 中提取平移向量 p.
# T: 4x4 齐次变换矩阵
# 返回: 3x1 平移向量 (NumPy array)
# """
# return T[:3, 3].reshape(3, 1) # 返回T的前三行最后一列,并确保形状为3x1
# @staticmethod
# def invert_transform(T):
# """
# 计算齐次变换矩阵 T 的逆.
# T: 4x4 齐次变换矩阵
# 返回: T的逆矩阵 T_inv
# """
# R = T[:3, :3] # 提取旋转部分
# p = T[:3, 3] # 提取平移部分
# R_inv = R.T # 旋转矩阵的逆是其转置
# p_inv = -R_inv @ p # 计算逆变换后的平移向量
# T_inv = np.identity(4) # 初始化一个4x4单位矩阵
# T_inv[:3, :3] = R_inv # 设置旋转部分
# T_inv[:3, 3] = p_inv # 设置平移部分
# return T_inv # 返回逆变换矩阵
# @staticmethod
# def transform_point(T, point_coords):
# """
# 使用齐次变换矩阵 T 变换一个点.
# T: 4x4 齐次变换矩阵
# point_coords: 点的坐标 [x, y, z] (list, tuple, or NumPy array)
# 返回: 变换后的点坐标 [x', y', z'] (NumPy array)
# """
# p_homogeneous = np.ones(4) # 创建一个齐次坐标向量 [x, y, z, 1]
# p_homogeneous[:3] = np.asarray(point_coords).flatten() # 设置点坐标
# p_transformed_homogeneous = T @ p_homogeneous # 执行变换:T * p_homogeneous
# return p_transformed_homogeneous[:3] / p_transformed_homogeneous[3] # 返回笛卡尔坐标 (除以w分量,通常为1)
# # 示例使用
# # 假设坐标系B相对于坐标系A:绕Z轴旋转90度,然后沿X轴平移0.5米,沿Y轴平移1.0米
# R_A_B = PyRoboPathTransformations.euler_to_rotation_matrix(0, 0, np.deg2rad(90), order='xyz') # 绕Z轴旋转90度 (yaw)
# p_A_B = np.array([0.5, 1.0, 0.0]) # 定义平移向量
# T_A_B = PyRoboPathTransformations.create_homogeneous_transform(R_A_B, p_A_B) # 创建齐次变换矩阵
# print("齐次变换矩阵 T_A_B:") # 打印提示信息
# print(T_A_B) # 打印矩阵
# # 有一个点 P 在坐标系 B 中的坐标为 P_B = [0.2, 0.3, 0.1]
# P_B = np.array([0.2, 0.3, 0.1]) # 定义点在B坐标系中的坐标
# # 计算点 P 在坐标系 A 中的坐标 P_A
# P_A = PyRoboPathTransformations.transform_point(T_A_B, P_B) # 进行坐标变换
# print(f"点 P_B = {P_B} 在坐标系 A 中的坐标 P_A = {np.round(P_A, 4)}") # 打印变换后的坐标
# # 计算 T_A_B 的逆矩阵 T_B_A
# T_B_A = PyRoboPathTransformations.invert_transform(T_A_B) # 计算逆变换
# print("T_A_B 的逆矩阵 T_B_A:") # 打印提示信息
# print(T_B_A) # 打印逆矩阵
# # 使用 T_B_A 将 P_A 变换回 P_B,验证逆变换的正确性
# P_B_retransformed = PyRoboPathTransformations.transform_point(T_B_A, P_A) # 将P_A变换回B坐标系
# print(f"将 P_A 通过 T_B_A 变换回 P_B_retransformed = {np.round(P_B_retransformed, 4)}") # 打印结果
# print(f"P_B_retransformed 是否接近原始 P_B? {np.allclose(P_B, P_B_retransformed)}") # 验证是否一致
在C-space中以及工作空间中,衡量两点(构型或位置)之间的“距离”是路径规划算法的基础。
欧几里得距离 (Euclidean Distance, L₂ norm):
p₁=(x₁,y₁,z₁)
和 p₂=(x₂,y₂,z₂)
:d(p₁, p₂) = √((x₁-x₂)² + (y₁-y₂)² + (z₁-z₂)²)
q₁=[a₁,b₁,...]
和 q₂=[a₂,b₂,...]
:d(q₁, q₂) = √((a₁-a₂)² + (b₁-b₂)² + ...)
加权欧几里得距离:
wᵢ
,以反映其重要性或运动代价。d(q₁, q₂) = √ (w₁²(a₁-a₂)² + w₂²(b₁-b₂)² + ...)
PyRoboPath
的 Space
类可以允许用户定义或配置距离函数,包括加权版本。曼哈顿距离 (Manhattan Distance, L₁ norm):
d(q₁, q₂) = |a₁-a₂| + |b₁-b₂| + ...
切比雪夫距离 (Chebyshev Distance, L∞ norm):
d(q₁, q₂) = max(|a₁-a₂|, |b₁-b₂|, ...)
姿态距离:
R₁
和 R₂
,可以使用轴角表示的角距离:angle(R₁ᵀ R₂)
,即计算相对旋转 R₁ᵀ R₂
的旋转角度。quat₁
和 quat₂
,角距离为 2 * acos(|quat₁ ⋅ quat₂|)
(点积的绝对值)。绝对值是为了取两个表示同一旋转的四元数中夹角较小的那个。PyRoboPath
的 State
或 Space
类需要妥善处理包含姿态的构型距离计算,例如分别计算位置距离和姿态距离,然后加权组合。# import numpy as np # NumPy已在前面导入
# # 假设的 PyRoboPath 核心模块中的距离函数
# class PyRoboPathMetrics:
# @staticmethod
# def euclidean_distance(p1, p2):
# """计算两个点之间的欧几里得距离 (L2范数)"""
# p1 = np.asarray(p1) # 转换为NumPy数组
# p2 = np.asarray(p2) # 转换为NumPy数组
# return np.linalg.norm(p1 - p2) # 计算差值的L2范数
# @staticmethod
# def weighted_euclidean_distance(q1, q2, weights):
# """计算两个构型之间的加权欧几里得距离"""
# q1 = np.asarray(q1) # 转换为NumPy数组
# q2 = np.asarray(q2) # 转换为NumPy数组
# weights = np.asarray(weights) # 转换为NumPy数组
# if len(q1) != len(weights) or len(q2) != len(weights): # 检查维度是否匹配
# raise ValueError("构型和权重的维度必须匹配") # 如果不匹配则抛出错误
# return np.sqrt(np.sum(weights**2 * (q1 - q2)**2)) # 计算加权平方和的平方根
# @staticmethod
# def quaternion_angular_distance(quat1, quat2):
# """
# 计算两个单位四元数之间的角距离 (弧度).
# quat1, quat2: 四元数 [w, x, y, z]
# 返回: 0到pi之间的角距离
# """
# q1 = np.asarray(quat1) / np.linalg.norm(quat1) # 归一化quat1
# q2 = np.asarray(quat2) / np.linalg.norm(quat2) # 归一化quat2
# dot_product = np.abs(np.dot(q1, q2)) # 计算点积的绝对值
# dot_product = np.clip(dot_product, -1.0, 1.0) # 裁剪到[-1, 1]以防数值误差
# return 2 * np.arccos(dot_product) # 计算角距离: 2 * acos(|q1 . q2|)
# # 示例使用
# # config1 = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6]) # 定义构型1
# # config2 = np.array([0.15, 0.22, 0.38, 0.41, 0.55, 0.69]) # 定义构型2
# # joint_weights = np.array([1.0, 1.0, 1.5, 1.5, 0.8, 0.8]) # 定义关节权重
# # dist_euc = PyRoboPathMetrics.euclidean_distance(config1, config2) # 计算欧几里得距离
# # dist_weighted_euc = PyRoboPathMetrics.weighted_euclidean_distance(config1, config2, joint_weights) # 计算加权欧几里得距离
# # print(f"构型1: {config1}") # 打印构型1
# # print(f"构型2: {config2}") # 打印构型2
# # print(f"欧几里得距离: {dist_euc:.4f}") # 打印欧几里得距离
# # print(f"关节权重: {joint_weights}") # 打印关节权重
# # print(f"加权欧几里得距离: {dist_weighted_euc:.4f}") # 打印加权欧几里得距离
# # 四元数距离示例
# # q_rot_x_90 = PyRoboPathTransformations.rotation_matrix_to_quaternion(
# # PyRoboPathTransformations.euler_to_rotation_matrix(np.pi/2, 0, 0)
# # ) # 绕X轴旋转90度的四元数
# # q_rot_x_80 = PyRoboPathTransformations.rotation_matrix_to_quaternion(
# # PyRoboPathTransformations.euler_to_rotation_matrix(np.deg2rad(80), 0, 0)
# # ) # 绕X轴旋转80度的四元数
# # q_identity = np.array([1.0, 0, 0, 0]) # 单位四元数 (无旋转)
# # ang_dist_90_identity = PyRoboPathMetrics.quaternion_angular_distance(q_rot_x_90, q_identity) # 计算90度旋转与无旋转的角距离
# # ang_dist_90_80 = PyRoboPathMetrics.quaternion_angular_distance(q_rot_x_90, q_rot_x_80) # 计算90度旋转与80度旋转的角距离
# # print(f"绕X轴90度旋转四元数与单位四元数的角距离: {np.rad2deg(ang_dist_90_identity):.2f} 度") # 打印结果,应为90度
# # print(f"绕X轴90度与80度旋转四元数的角距离: {np.rad2deg(ang_dist_90_80):.2f} 度") # 打印结果,应为10度
路径规划中常需要表示和操作基本的几何形状,用于构建机器人模型、障碍物以及进行碰撞检测。
[x, y]
或 [x, y, z]
。n
和到原点距离 d
定义 (n⋅x + d = 0
),或由平面上一点和法向量定义。关键运算:点到平面距离,线/射线与平面求交。PyRoboPath
的 environments
模块会提供这些几何图元的类表示,并内建或调用几何库(如 Shapely (2D), Trimesh (3D))进行相关运算,如:
point_in_polygon(point, polygon_vertices)
segment_intersects_polygon(segment, polygon_vertices)
aabb_intersects_aabb(aabb1, aabb2)
obb_intersects_obb(obb1, obb2, using_sat=True)
distance_point_triangle(point, triangle_vertices)
mesh_collision(mesh1, transform1, mesh2, transform2)
(可能调用FCL)许多路径规划算法(如PRM, A*, Dijkstra)都依赖于图的表示和搜索。
V
: 顶点(Nodes/Vertices)集合。在路径规划中,顶点可以代表C-space中的构型、工作空间中的位置、或区域。E
: 边(Edges)集合。边连接两个顶点,表示它们之间存在直接的可达路径。边可以有权重(cost),表示通过该边的代价(如距离、时间、能量)。PyRoboPath
的 planners
模块会使用图数据结构(可能基于 networkx
库或自定义实现)来存储路线图(PRM)或搜索过程中的节点关系,并实现上述搜索算法。
# import networkx as nx # 导入 NetworkX 库,用于创建、操作和研究复杂网络的结构、动态和功能
# import matplotlib.pyplot as plt # 导入 Matplotlib 的 pyplot 模块,用于绘图
# # 示例:使用 NetworkX 构建和搜索图 (概念性,实际PyRoboPath会有自己的封装)
# # 1. 创建一个图 (代表C-space中的一些采样点和它们的连接)
# G = nx.Graph() # 创建一个无向图实例
# # 添加节点 (节点可以是任何可哈希对象,这里用整数ID代表构型ID)
# # 假设这些节点是 C_free 中的采样点
# nodes_data = {
# 0: {"pos": (0,0), "label": "q_start"}, # 节点0,坐标(0,0),标签"q_start"
# 1: {"pos": (1,2)}, # 节点1,坐标(1,2)
# 2: {"pos": (3,1)}, # 节点2,坐标(3,1)
# 3: {"pos": (2,3)}, # 节点3,坐标(2,3)
# 4: {"pos": (4,3), "label": "q_goal"} # 节点4,坐标(4,3),标签"q_goal"
# }
# for node_id, attrs in nodes_data.items(): # 遍历节点数据
# G.add_node(node_id, **attrs) # 将节点及其属性添加到图中
# # 添加边 (代表构型间的无碰撞路径段),并赋予权重 (如欧几里得距离)
# edges_data = [
# (0, 1, {"weight": PyRoboPathMetrics.euclidean_distance(nodes_data[0]["pos"], nodes_data[1]["pos"])}), # 边0-1及其权重
# (0, 2, {"weight": PyRoboPathMetrics.euclidean_distance(nodes_data[0]["pos"], nodes_data[2]["pos"])}), # 边0-2及其权重
# (1, 3, {"weight": PyRoboPathMetrics.euclidean_distance(nodes_data[1]["pos"], nodes_data[3]["pos"])}), # 边1-3及其权重
# (2, 3, {"weight": PyRoboPathMetrics.euclidean_distance(nodes_data[2]["pos"], nodes_data[3]["pos"])}), # 边2-3及其权重
# (2, 4, {"weight": PyRoboPathMetrics.euclidean_distance(nodes_data[2]["pos"], nodes_data[4]["pos"])}), # 边2-4及其权重
# (3, 4, {"weight": PyRoboPathMetrics.euclidean_distance(nodes_data[3]["pos"], nodes_data[4]["pos"])}) # 边3-4及其权重
# ]
# G.add_edges_from(edges_data) # 将边及其属性添加到图中
# # 2. 可视化图 (可选,用于理解)
# # pos_map = nx.get_node_attributes(G, 'pos') # 获取所有节点的位置属性
# # labels_map = nx.get_node_attributes(G, 'label') # 获取所有节点的标签属性 (部分节点有)
# # edge_labels = nx.get_edge_attributes(G, 'weight') # 获取所有边的权重属性
# # for k, v in edge_labels.items(): edge_labels[k] = f"{v:.2f}" # 格式化权重显示
# # plt.figure(figsize=(8, 6)) # 创建一个新的图形窗口
# # nx.draw(G, pos_map, with_labels=True, labels={n: str(n) + (f"\n{labels_map[n]}" if n in labels_map else "") for n in G.nodes()}, node_size=700, node_color="lightblue", font_size=10) # 绘制图形
# # nx.draw_networkx_edge_labels(G, pos_map, edge_labels=edge_labels) # 绘制边的权重标签
# # plt.title("示例C-space Roadmap图") # 设置图形标题
# # plt.show() # 显示图形
# # 3. 使用图搜索算法寻找路径
# start_node = 0 # 定义起始节点
# goal_node = 4 # 定义目标节点
# try:
# # 使用 Dijkstra 算法找到最短路径
# # path_dijkstra = nx.dijkstra_path(G, source=start_node, target=goal_node, weight='weight') # 计算最短路径
# # length_dijkstra = nx.dijkstra_path_length(G, source=start_node, target=goal_node, weight='weight') # 计算最短路径长度
# # print(f"Dijkstra 路径: {path_dijkstra}") # 打印Dijkstra路径
# # print(f"Dijkstra 路径长度: {length_dijkstra:.2f}") # 打印Dijkstra路径长度
# # 定义 A* 的启发函数 (heuristic): 从当前节点到目标节点的欧几里得距离
# def astar_heuristic(u, v): # 定义A*启发函数
# # u: 当前节点, v: 目标节点 (在networkx中, v通常是固定的goal)
# # pos_u = nodes_data[u]["pos"] # 获取u节点的位置
# # pos_v = nodes_data[v]["pos"] # 获取v节点的位置 (目标节点)
# # return PyRoboPathMetrics.euclidean_distance(pos_u, pos_v) # 返回欧几里得距离作为启发值
# return 0 # 实际使用中,应正确获取目标节点的位置
# # # 使用 A* 算法找到最短路径
# # path_astar = nx.astar_path(G, source=start_node, target=goal_node, heuristic=lambda u,v: astar_heuristic(u,goal_node), weight='weight') # 使用A*算法计算路径
# # length_astar = nx.astar_path_length(G, source=start_node, target=goal_node, heuristic=lambda u,v: astar_heuristic(u,goal_node), weight='weight') # 使用A*算法计算路径长度
# # print(f"A* 路径: {path_astar}") # 打印A*路径
# # print(f"A* 路径长度: {length_astar:.2f}") # 打印A*路径长度
# except nx.NetworkXNoPath: # 捕获NetworkX中无路径的异常
# print(f"从节点 {start_node} 到节点 {goal_node} 不存在路径。") # 打印无路径信息
# except KeyError as e: # 捕获字典键错误
# print(f"图中节点未找到: {e}") # 打印节点未找到信息
# # 在 PyRoboPath 中,这些图操作会被封装在规划器内部。
# # 例如,PRM规划器会构建这样的图,然后使用类似Dijkstra或A*的搜索。
这些数学和几何基础是构建 PyRoboPath
这样一个复杂机器人路径规划库的支柱。在后续章节中,当我们讨论具体的机器人模型、环境表示、碰撞检测和规划算法时,这些概念会反复出现并被应用。对这些基础的深入理解,将有助于更好地掌握 PyRoboPath
的内部机制和高级应用。
虽然 PyRoboPath
是我们为教学和深度探讨而构建的一个概念性框架,但理解其理想的安装和环境设置过程,对于把握一个成熟Python库的生命周期管理至关重要。这本身也体现了“运维精髓”中关于标准化部署和环境一致性的要求。
一个典型的Python科学计算或机器人库,如我们构想的 PyRoboPath
,通常会有以下系统要求和核心依赖:
操作系统:
PyRoboPath
会优先保证在Linux上的稳定性和性能。Python 版本:
核心依赖库 (通过 pip 安装):
NumPy: (Numerical Python) 几乎是所有科学计算的基础,提供高效的多维数组对象 (ndarray
) 及相关运算。PyRoboPath
将广泛使用NumPy进行向量、矩阵运算,表示构型、位姿、路径点等。
import numpy as np # 导入NumPy库,并使用别名np
# PyRoboPath 内部大量使用 np.array, np.dot, np.linalg.inv, np.linalg.norm 等
SciPy: (Scientific Python) 基于NumPy,提供更多高级科学和工程计算功能,如优化、插值、信号处理、线性代数、统计等。PyRoboPath
可能使用 scipy.optimize
进行IK数值求解,scipy.interpolate
进行路径插值和平滑,scipy.spatial
进行空间数据结构(如KDTree)和距离计算。
from scipy.spatial import KDTree # 从SciPy库的spatial模块导入KDTree类
from scipy.optimize import minimize # 从SciPy库的optimize模块导入minimize函数
# KDTree可用于高效查找最近邻点 (例如在PRM或RRT中)
# minimize可用于基于优化的IK求解或轨迹优化
Matplotlib: 流行的2D绘图库,也可进行基本的3D绘图。PyRoboPath
会用它进行路径、机器人构型、环境、C-space障碍物(2D/3D切片)等的可视化。
import matplotlib.pyplot as plt # 导入Matplotlib的pyplot模块,并使用别名plt
from mpl_toolkits.mplot3d import Axes3D # 导入用于3D绘图的Axes3D
# fig = plt.figure() # 创建一个新的图形窗口
# ax = fig.add_subplot(111, projection='3d') # 在图形窗口中添加一个3D子图
# ax.plot(xs, ys, zs) # 绘制3D线条
# plt.show() # 显示图形
NetworkX: 用于创建、操作和研究复杂网络的结构、动态和功能的Python包。PyRoboPath
中的基于图的规划器(如PRM, A*)会用它来表示和操作C-space路线图或搜索图。
import networkx as nx # 导入NetworkX库,并使用别名nx
# G = nx.Graph() # 创建一个图对象
# G.add_node(0, config=np.array([0.1, 0.2])) # 添加一个带属性的节点
# G.add_edge(0, 1, weight=0.5) # 添加一条带权重的边
# path = nx.shortest_path(G, source=0, target=1, weight='weight') # 计算最短路径
可选依赖库 (根据功能需求):
PyBullet: 一个基于Bullet物理引擎的Python模块,用于机器人仿真和强化学习。PyRoboPath
可以集成PyBullet进行精确的碰撞检测、机器人运动学/动力学仿真和3D可视化。
# import pybullet as p # 导入PyBullet库,并使用别名p
# p.connect(p.GUI) # 连接到PyBullet仿真环境,并打开GUI
# robot_id = p.loadURDF("robot.urdf") # 加载URDF机器人模型
# p.setGravity(0, 0, -9.81) # 设置重力
# for _ in range(1000): # 运行仿真循环
# p.stepSimulation() # 执行一步仿真
# p.disconnect() #断开与仿真环境的连接
python-fcl (或类似的FCL绑定): FCL (Flexible Collision Library) 是一个用C++编写的高效碰撞检测库,支持多种几何图元和网格间的碰撞查询及距离计算。PyRoboPath
会优先使用这类库进行核心碰撞检测,以提高性能。
# import fcl # 假设的python-fcl库导入方式
# obj1_geom = fcl.Box(1,1,1) # 创建一个1x1x1的立方体几何对象
# obj1_transform = fcl.Transform() # 创建一个单位变换
# obj1 = fcl.CollisionObject(obj1_geom, obj1_transform) # 创建碰撞对象1
# # ...类似地创建obj2...
# req = fcl.CollisionRequest() # 创建碰撞检测请求
# res = fcl.CollisionResult() # 创建碰撞检测结果对象
# fcl.collide(obj1, obj2, req, res) # 执行碰撞检测
# is_collision = res.is_collision() # 获取碰撞结果
Trimesh: 用于加载和使用三角网格的纯Python库。可以处理STL, OBJ, PLY等多种网格文件格式,进行网格修复、布尔运算、凸包计算等。PyRoboPath
会用它来处理机器人连杆和环境障碍物的复杂几何模型。
import trimesh # 导入Trimesh库
# mesh = trimesh.load_mesh('obstacle.stl') # 从STL文件加载网格模型
# if mesh.is_watertight: # 检查网格是否水密
# volume = mesh.volume # 计算网格的体积
# convex_hull = mesh.convex_hull # 计算网格的凸包
PyYAML: 用于读写YAML (YAML Ain’t Markup Language) 格式的配置文件。YAML因其人类可读性好,常用于复杂的配置场景。
import yaml # 导入PyYAML库
# with open('config.yaml', 'r') as f: # 以只读模式打开YAML配置文件
# config_data = yaml.safe_load(f) # 安全加载YAML数据
# robot_name = config_data['robot']['name'] # 从加载的数据中获取机器人名称
lxml: 用于处理XML和HTML的强大Python库。PyRoboPath
会用它(或Python内置的 xml.etree.ElementTree
)来解析URDF/SDF等基于XML的机器人描述文件。
from lxml import etree # 从lxml库导入etree模块
# tree = etree.parse("robot_model.urdf") # 解析URDF文件
# root = tree.getroot() # 获取XML树的根元素
# robot_name_attr = root.get("name") # 获取根元素的'name'属性
构建工具 (如果从源码安装):
setuptools
: Python标准的打包和分发工具。wheel
: 用于构建Python的wheel包格式。PyRoboPath
包含C/C++扩展模块(例如为了性能优化或绑定外部库如FCL),则需要编译器。对于最终用户,最便捷的安装方式是通过Python的包管理器 pip
从 PyPI (Python Package Index) 安装预编译的wheel包或源码包。
# 建议在虚拟环境中使用
python -m venv pyrobopath_env # 创建名为 pyrobopath_env 的虚拟环境
source pyrobopath_env/bin/activate # (Linux/macOS) 激活虚拟环境
# pyrobopath_env\Scripts\activate # (Windows) 激活虚拟环境
pip install numpy scipy matplotlib networkx # 首先安装一些常见的基础依赖,有时有助于解决复杂依赖问题
# 理想的 PyRoboPath 安装命令
pip install pyrobopath # 安装最新稳定版的 PyRoboPath
# 安装特定版本
# pip install pyrobopath==0.1.0
# 安装并包含可选依赖 (例如,包含 PyBullet 支持)
# pip install pyrobopath[pybullet] # 使用 extras_require 功能
# 升级 PyRoboPath
# pip install --upgrade pyrobopath
python -m venv pyrobopath_env
: 这条命令使用 Python 内置的 venv
模块创建一个名为 pyrobopath_env
的独立 Python 虚拟环境。虚拟环境有助于隔离项目依赖,避免不同项目间的库版本冲突。source pyrobopath_env/bin/activate
: 在 Linux 或 macOS 系统下,这条命令用于激活先前创建的 pyrobopath_env
pyrobopath.robots
)机器人模型是路径规划系统的核心输入之一。PyRoboPath
的 robots
模块旨在提供灵活而强大的工具来定义、操作和查询各种类型的机器人。本章将详细探讨该模块的概念性设计和实现。
RobotModel
基类详解所有特定类型的机器人模型(如串联臂、移动机器人)都应继承自一个通用的 RobotModel
基类。这个基类定义了机器人模型的通用接口和核心属性。
import numpy as np # 导入NumPy库,用于数值运算
from abc import ABC, abstractmethod # 从abc模块导入ABC(抽象基类)和abstractmethod(抽象方法装饰器)
# 假设的 PyRoboPath 变换和核心类型模块
# from pyrobopath.core import State, Pose # State用于表示构型,Pose用于表示位姿
# from pyrobopath.environments import CollisionGeometry # CollisionGeometry用于表示碰撞几何体
class RobotModel(ABC): # 定义机器人模型的抽象基类 RobotModel,继承自ABC
"""
机器人模型的抽象基类。
定义了所有机器人类型应遵循的通用接口和属性。
"""
def __init__(self, name: str, num_dof: int): # 初始化方法
"""
构造函数。
:param name: 机器人名称 (str)
:param num_dof: 机器人的自由度 (int)
"""
self._name: str = name # 将机器人名称存储为私有属性 _name
self._num_dof: int = num_dof # 将自由度数量存储为私有属性 _num_dof
self._current_config: State = None # 初始化当前构型为None,类型为State
self._joint_limits_lower: np.ndarray = np.full(num_dof, -np.inf) # 初始化关节下限为负无穷,大小为num_dof
self._joint_limits_upper: np.ndarray = np.full(num_dof, np.inf) # 初始化关节上限为正无穷,大小为num_dof
self._joint_velocity_limits: np.ndarray = np.full(num_dof, np.inf) # 初始化关节速度上限为正无穷
self._joint_acceleration_limits: np.ndarray = np.full(num_dof, np.inf) # 初始化关节加速度上限为正无穷
self._links = [] # 初始化连杆列表为空
self._joints = [] # 初始化关节列表为空
self._base_transform: Pose = Pose() # 初始化机器人基座相对于世界坐标系的位姿,默认为单位变换 (Pose类需要定义)
self._tool_transform: Pose = Pose() # 初始化工具末端相对于末端连杆坐标系的位姿,默认为单位变换
# logger.info(f"机器人模型 '{self.name}' 已创建,具有 {self.num_dof} 个自由度。") # 实际库中会有日志
@property
def name(self) -> str: # 定义只读属性 name
"""返回机器人名称"""
return self._name # 返回私有属性 _name
@property
def num_dof(self) -> int: # 定义只读属性 num_dof
"""返回机器人的自由度"""
return self._num_dof # 返回私有属性 _num_dof
def set_joint_limits(self, lower_limits: list | np.ndarray, upper_limits: list | np.ndarray): # 定义设置关节限制的方法
"""
设置机器人的关节角度限制。
:param lower_limits: 各关节的下限列表或NumPy数组
:param upper_limits: 各关节的上限列表或NumPy数组
"""
if len(lower_limits) != self.num_dof or len(upper_limits) != self.num_dof: # 检查输入限制的维度是否与自由度匹配
raise ValueError(f"关节限制的维度 ({
len(lower_limits)}, {
len(upper_limits)}) 必须等于自由度 ({
self.num_dof})") # 如果不匹配则抛出值错误
self._joint_limits_lower = np.asarray(lower_limits, dtype=float) # 将下限转换为NumPy浮点数组并存储
self._joint_limits_upper = np.asarray(upper_limits, dtype=float) # 将上限转换为NumPy浮点数组并存储
# logger.debug(f"机器人 '{self.name}' 的关节限制已设置: L={self._joint_limits_lower}, U={self._joint_limits_upper}") # 记录调试信息
@property
def joint_limits(self) -> tuple[np.ndarray, np.ndarray]: # 定义只读属性 joint_limits
"""返回关节的下限和上限 (tuple[np.ndarray, np.ndarray])"""
return self._joint_limits_lower, self._joint_limits_upper # 返回存储的关节下限和上限
def set_velocity_limits(self, velocity_limits: list | np.ndarray): # 定义设置关节速度限制的方法
"""设置关节速度限制 (通常是绝对值)"""
if len(velocity_limits) != self.num_dof: # 检查输入限制的维度是否与自由度匹配
raise ValueError(f"关节速度限制的维度 ({
len(velocity_limits)}) 必须等于自由度 ({
self.num_dof})") # 如果不匹配则抛出值错误
self._joint_velocity_limits = np.abs(np.asarray(velocity_limits, dtype=float)) # 转换为NumPy数组,取绝对值并存储
# logger.debug(f"机器人 '{self.name}' 的关节速度限制已设置: V_max={self._joint_velocity_limits}") # 记录调试信息
@property
def velocity_limits(self) -> np.ndarray: # 定义只读属性 velocity_limits
"""返回关节速度限制"""
return self._joint_velocity_limits # 返回存储的关节速度限制
# 类似的可以添加加速度限制 set_acceleration_limits 和 acceleration_limits
def set_current_config(self, config: State | list | np.ndarray): # 定义设置当前构型的方法
"""
设置机器人的当前构型。
:param config: 机器人的新构型 (State对象, list, 或 NumPy array)
"""
if isinstance(config, State): # 如果输入是State对象
if len(config.values) != self.num_dof: # 检查State值的长度是否等于自由度
raise ValueError(f"构型状态的维度 ({
len(config.values)}) 必须等于自由度 ({
self.num_dof})") # 维度不匹配则抛出错误
self._current_config = config # 直接赋值
else:
config_arr = np.asarray(config, dtype=float) # 将输入转换为NumPy浮点数组
if len(config_arr) != self.num_dof: # 检查数组长度是否等于自由度
raise ValueError(f"构型数组的维度 ({
len(config_arr)}) 必须等于自由度 ({
self.num_dof})") # 维度不匹配则抛出错误
self._current_config = State(config_arr) # 创建新的State对象并赋值
# logger.trace(f"机器人 '{self.name}' 当前构型更新为: {self._current_config.values}") # 记录追踪信息(非常详细的日志)
def get_current_config(self) -> State | None: # 定义获取当前构型的方法
"""返回机器人的当前构型 (State)"""
return self._current_config # 返回存储的当前构型
def set_base_transform(self, pose: Pose): # 定义设置机器人基座变换的方法
"""设置机器人基座相对于世界坐标系的位姿"""
if not isinstance(pose, Pose): # 检查输入是否为Pose对象
raise TypeError("基座变换必须是 Pose 对象") # 类型不匹配则抛出错误
self._base_transform = pose # 赋值
# logger.debug(f"机器人 '{self.name}' 基座变换已更新。") # 记录调试信息
@property
def base_transform(self) -> Pose: # 定义只读属性 base_transform
"""获取机器人基座相对于世界坐标系的位姿"""
return self._base_transform # 返回存储的基座变换
def set_tool_transform(self, pose: Pose): # 定义设置工具变换的方法
"""设置工具中心点 (TCP) 相对于末端连杆坐标系的位姿"""
if not isinstance(pose, Pose): # 检查输入是否为Pose对象
raise TypeError("工具变换必须是 Pose 对象") # 类型不匹配则抛出错误
self._tool_transform = pose # 赋值
# logger.debug(f"机器人 '{self.name}' 工具变换已更新。") # 记录调试信息
@property
def tool_transform(self) -> Pose: # 定义只读属性 tool_transform
"""获取工具中心点 (TCP) 相对于末端连杆坐标系的位姿"""
return self._tool_transform # 返回存储的工具变换
@abstractmethod
def forward_kinematics(self, config: State | list | np.ndarray | None = None, link_name: str | int | None = None) -> Pose | dict[str | int, Pose]: # 定义正向运动学抽象方法
"""
计算正向运动学。
:param config: 给定的机器人构型。如果为 None,则使用当前构型。
:param link_name: (可选) 特定连杆的名称或索引。如果为 None,则计算末端执行器(考虑工具)的位姿。
如果提供了 link_name,则返回该连杆的位姿。
也可以是一个特殊值如 "all" 来返回所有连杆的位姿字典。
:return: Pose 对象表示末端执行器或指定连杆的位姿;或者一个字典,键是连杆名/索引,值是对应的Pose。
所有位姿都是相对于机器人基座坐标系的。
"""
pass # 抽象方法,需要在子类中实现
@abstractmethod
def inverse_kinematics(self, target_pose: Pose, start_config_approx: State | None = None, **kwargs) -> list[State] | None: # 定义逆向运动学抽象方法
"""
计算逆向运动学。
给定末端执行器(考虑工具)的目标位姿(相对于机器人基座),找到一个或多个关节构型。
:param target_pose: 末端执行器的目标位姿 (Pose对象)。
:param start_config_approx: (可选) 初始猜测构型,用于数值解法。
:param kwargs: 其他特定于IK求解器的参数 (如: 容差, 最大迭代次数, 求解器类型等)。
:return: 一个包含一个或多个解构型 (State对象) 的列表;如果无解,则返回 None 或空列表。
"""
pass # 抽象方法,需要在子类中实现
@abstractmethod
def jacobian(self, config: State | list | np.ndarray | None = None, link_name: str | int | None = None) -> np.ndarray: # 定义雅可比矩阵抽象方法
"""
计算机器人末端执行器或指定连杆的雅可比矩阵。
雅可比矩阵 J 将关节速度 dq/dt 映射到末端执行器的笛卡尔速度 (线速度v, 角速度ω): [v; ω] = J * dq/dt.
通常是一个 6xN 的矩阵,N 是自由度。
:param config: 给定的机器人构型。如果为 None,则使用当前构型。
:param link_name: (可选) 特定连杆的名称或索引。如果为 None,则计算末端执行器(考虑工具)的雅可比。
:return: NumPy 数组表示的雅可比矩阵。
"""
pass # 抽象方法,需要在子类中实现
@abstractmethod
def get_link_collision_geometry(self, link_name: str | int) -> list[CollisionGeometry]: # 定义获取连杆碰撞几何体抽象方法
"""
获取指定连杆的碰撞几何体列表。
每个几何体通常包含形状和相对于其所属连杆坐标系的位姿。
:param link_name: 连杆的名称或索引。
:return: CollisionGeometry 对象列表。
"""
pass # 抽象方法,需要在子类中实现
@abstractmethod
def get_all_link_collision_geometries_at_config(self, config: State | list | np.ndarray | None = None) -> dict[str | int, list[tuple[CollisionGeometry, Pose]]]: # 定义获取所有连杆在特定构型下碰撞几何体的抽象方法
"""
获取所有连杆的碰撞几何体及其在给定构型下的全局位姿(相对于机器人基座)。
这对于碰撞检测非常重要。
:param config: 给定的机器人构型。如果为 None,则使用当前构型。
:return: 一个字典,键是连杆名/索引,值是 (CollisionGeometry, global_pose_of_geometry) 元组的列表。
"""
pass # 抽象方法,需要在子类中实现
def check_joint_limits(self, config: State | list | np.ndarray) -> bool: # 定义检查关节限制的方法
"""
检查给定构型是否在关节限制范围内。
:param config: 要检查的构型。
:return: True 如果在限制内,否则 False。
"""
if isinstance(config, State): # 如果输入是State对象
q_vals = config.values # 获取State的值
else:
q_vals = np.asarray(config, dtype=float) # 否则转换为NumPy数组
if len(q_vals) != self.num_dof: # 检查维度是否匹配
# logger.warning(f"检查关节限制时构型维度 ({len(q_vals)}) 与机器人DOF ({self.num_dof}) 不匹配。") # 记录警告
return False # 维度不匹配,返回False
lower_ok = np.all(q_vals >= self._joint_limits_lower - 1e-6) # 检查是否所有关节值都大于等于下限 (考虑容差)
upper_ok = np.all(q_vals <= self._joint_limits_upper + 1e-6) # 检查是否所有关节值都小于等于上限 (考虑容差)
return lower_ok and upper_ok # 当且仅当所有关节都在限位内时返回True
def get_random_config(self, ensure_within_limits: bool = True) -> State: # 定义获取随机构型的方法
"""
生成一个随机的机器人构型。
:param ensure_within_limits: 是否确保生成的构型在关节限制内。
如果为True且关节限制非无限,则在限制内采样。
如果为False或限制为无限,则可能在更广的范围内采样(需小心)。
:return: 一个随机的 State 对象。
"""
rand_vals = np.zeros(self.num_dof) # 初始化一个大小为num_dof的零数组
for i in range(self.num_dof): # 遍历每个自由度
low = self._joint_limits_lower[i] # 获取第i个关节的下限
high = self._joint_limits_upper[i] # 获取第i个关节的上限
if ensure_within_limits and np.isfinite(low) and np.isfinite(high): # 如果需要确保在限制内且限制是有限的
if low > high: # 如果下限大于上限 (异常情况)
# logger.warning(f"关节 {i} 的下限 ({low}) 大于上限 ({high}),随机采样将使用[high, low]") # 记录警告
rand_vals[i] = np.random.uniform(high, low) # 在[high, low]间均匀采样 (可能是个错误配置)
else:
rand_vals[i] = np.random.uniform(low, high) # 在[low, high]间均匀采样
else: # 如果不要求在限制内,或限制是无限的
# 对于无限的关节(如连续旋转关节),定义一个合理的采样范围,例如 -pi 到 pi
if np.isinf(low) and np.isinf(high): # 如果上下限都是无限
rand_vals[i] = np.random.uniform(-np.pi, np.pi) # 在[-pi, pi]间均匀采样
elif np.isinf(low): # 如果只有下限是无限
rand_vals[i] = np.random.uniform(high - 2*np.pi, high) # 在[high-2pi, high]间采样 (假设)
elif np.isinf(high): # 如果只有上限是无限
rand_vals[i] = np.random.uniform(low, low + 2*np.pi) # 在[low, low+2pi]间采样 (假设)
else: # 理论上此分支不可达,因为上面 ensure_within_limits 已处理有限情况
rand_vals[i] = np.random.uniform(-np.pi, np.pi) # 默认采样范围
return State(rand_vals) # 返回创建的随机State对象
def __repr__(self) -> str: # 定义对象的字符串表示方法
return f"{
self.num_dof}>" # 返回描述机器人名称和自由度的字符串
# 假设的State和Pose类 (简化版)
class State: # 定义State类
def __init__(self, values: list | np.ndarray): # 初始化方法
self.values: np.ndarray = np.asarray(values, dtype=float) # 将输入值转换为NumPy浮点数组并存储
def __len__(self): # 定义长度方法
return len(self.values) # 返回值的长度
def __repr__(self): # 定义对象的字符串表示方法
return f"State({
self.values.tolist()})" # 返回表示State值的字符串
class Pose: # 定义Pose类
def __init__(self, position: list | np.ndarray = None, orientation_quat: list | np.ndarray = None): # 初始化方法
if position is None: # 如果位置为空
self.position: np.ndarray = np.array([0., 0., 0.]) # 默认为原点 [0,0,0]
else:
self.position: np.ndarray = np.asarray(position, dtype=float) # 否则转换为NumPy浮点数组
if orientation_quat is None: # 如果姿态四元数为空
self.orientation_quat: np.ndarray = np.array([1., 0., 0., 0.]) # 默认为单位四元数 [w,x,y,z] (无旋转)
else:
self.orientation_quat: np.ndarray = np.asarray(orientation_quat, dtype=float) # 否则转换为NumPy浮点数组
if not np.isclose(np.linalg.norm(self.orientation_quat), 1.0): # 检查四元数是否为单位四元数
# logger.warning("提供的姿态四元数不是单位四元数,将进行归一化。") # 记录警告
self.orientation_quat /= np.linalg.norm(self.orientation_quat) # 进行归一化
@property
def rotation_matrix(self) -> np.ndarray: # 定义只读属性 rotation_matrix
"""从四元数计算并返回3x3旋转矩阵"""
w, x, y, z = self.orientation_quat[0], self.orientation_quat[1], self.orientation_quat[2], self.orientation_quat[3] # 提取四元数分量
R = np.array([ # 根据四元数计算旋转矩阵
[1 - 2*y**2 - 2*z**2, 2*x*y - 2*w*z, 2*x*z + 2*w*y],
[2*x*y + 2*w*z, 1 - 2*x**2 - 2*z**2, 2*y*z - 2*w*x],
[2*x*z - 2*w*y, 2*y*z + 2*w*x, 1 - 2*x**2 - 2*y**2]
])
return R # 返回旋转矩阵
@property
def homogeneous_matrix(self) -> np.ndarray: # 定义只读属性 homogeneous_matrix
"""返回4x4齐次变换矩阵"""
T = np.identity(4) # 创建一个4x4单位矩阵
T[:3, :3] = self.rotation_matrix # 将旋转矩阵赋给左上角3x3部分
T[