Files
2025-09-05 15:49:00 +08:00

559 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

def quaternion_rotate(quaternion, axis, angle):
"""
Rotate a quaternion around a specified axis by a given angle.
Parameters:
quaternion (numpy array): The input quaternion [w, x, y, z].
axis (str): The axis to rotate around ('x', 'y', or 'z').
angle (float): The rotation angle in degrees.
Returns:
numpy array: The rotated quaternion.
"""
# Convert angle from degrees to radians
angle_rad = np.radians(angle)
# Calculate the rotation quaternion based on the specified axis
cos_half_angle = np.cos(angle_rad / 2)
sin_half_angle = np.sin(angle_rad / 2)
if axis == 'x':
q_axis = np.array([cos_half_angle, sin_half_angle, 0, 0])
elif axis == 'y':
q_axis = np.array([cos_half_angle, 0, sin_half_angle, 0])
elif axis == 'z':
q_axis = np.array([cos_half_angle, 0, 0, sin_half_angle])
else:
raise ValueError("Unsupported axis. Use 'x', 'y', or 'z'.")
# Extract components of the input quaternion
w1, x1, y1, z1 = quaternion
w2, x2, y2, z2 = q_axis
# Quaternion multiplication
w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2
z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2
return np.array([w, x, y, z])
def axis_to_quaternion(axis, target_axis='y'):
"""
Calculate the quaternion that rotates a given axis to the target axis.
Parameters:
axis (str): The axis in the object's local coordinate system ('x', 'y', or 'z').
target_axis (str): The target axis in the world coordinate system ('x', 'y', or 'z').
Returns:
numpy array: The quaternion representing the rotation.
"""
# Define unit vectors for each axis
unit_vectors = {
'x': np.array([1, 0, 0]),
'y': np.array([0, 1, 0]),
'z': np.array([0, 0, 1])
}
if axis not in unit_vectors or target_axis not in unit_vectors:
raise ValueError("Unsupported axis. Use 'x', 'y', or 'z'.")
v1 = unit_vectors[axis]
v2 = unit_vectors[target_axis]
# Calculate the cross product and dot product
cross_prod = np.cross(v1, v2)
dot_prod = np.dot(v1, v2)
# Calculate the quaternion
w = np.sqrt((np.linalg.norm(v1) ** 2) * (np.linalg.norm(v2) ** 2)) + dot_prod
x, y, z = cross_prod
# Normalize the quaternion
q = np.array([w, x, y, z])
q = q / np.linalg.norm(q)
return q
def is_y_axis_up(pose_matrix):
"""
判断物体在世界坐标系中的 y 轴是否朝上。
参数:
pose_matrix (numpy.ndarray): 4x4 齐次变换矩阵。
返回:
bool: True 如果 y 轴朝上False 如果 y 轴朝下。
"""
# 提取旋转矩阵的第二列
y_axis_vector = pose_matrix[:3, 1]
# 世界坐标系的 y 轴
world_y_axis = np.array([0, 1, 0])
# 计算点积
dot_product = np.dot(y_axis_vector, world_y_axis)
# 返回 True 如果朝上,否则返回 False
return dot_product > 0
def is_local_axis_facing_world_axis(pose_matrix, local_axis='y', world_axis='z'):
# 定义局部坐标系的轴索引
local_axis_index = {'x': 0, 'y': 1, 'z': 2}
# 定义世界坐标系的轴向量
world_axes = {
'x': np.array([1, 0, 0]),
'y': np.array([0, 1, 0]),
'z': np.array([0, 0, 1])
}
# 提取局部坐标系的指定轴
local_axis_vector = pose_matrix[:3, local_axis_index[local_axis]]
# 获取世界坐标系的指定轴向量
world_axis_vector = world_axes[world_axis]
# 计算点积
dot_product = np.dot(local_axis_vector, world_axis_vector)
# 返回 True 如果朝向指定世界轴,否则返回 False
return dot_product > 0
def rotate_180_along_axis(target_affine, rot_axis='z'):
'''
gripper是对称结构绕Z轴旋转180度前后是等效的。选择离当前pose更近的target pose以避免不必要的旋转
'''
if rot_axis == 'z':
R_180 = np.array([[-1, 0, 0],
[0, -1, 0],
[0, 0, 1]])
elif rot_axis == 'y':
R_180 = np.array([[-1, 0, 0],
[0, 1, 0],
[0, 0, -1]])
elif rot_axis == 'x':
R_180 = np.array([[1, 0, 0],
[0, -1, 0],
[0, 0, -1]])
else:
assert False, "Invalid rotation axis. Please choose from 'x', 'y', 'z'."
# 提取旋转部分3x3矩阵
target_rotation = target_affine[:3, :3]
# 将target_rotation绕其自身的Z轴转180度得到target_rotation_2
target_rotation_2 = np.dot(target_rotation, R_180)
# 重新组合旋转矩阵target_rotation_2和原始的平移部分
target_affine_2 = np.eye(4)
target_affine_2[:3, :3] = target_rotation_2
target_affine_2[:3, 3] = target_affine[:3, 3] # 保留原始的平移部分
return target_affine_2
def rotate_along_axis(target_affine, angle_degrees, rot_axis='z', use_local=True):
'''
根据指定的角度和旋转轴来旋转target_affine。
参数:
- target_affine: 4x4 仿射变换矩阵
- angle_degrees: 旋转角度(以度为单位)
- rot_axis: 旋转轴,'x''y''z'
'''
# 将角度转换为弧度
angle_radians = np.deg2rad(angle_degrees)
# 创建旋转对象
if rot_axis == 'z':
rotation_vector = np.array([0, 0, angle_radians])
elif rot_axis == 'y':
rotation_vector = np.array([0, angle_radians, 0])
elif rot_axis == 'x':
rotation_vector = np.array([angle_radians, 0, 0])
else:
raise ValueError("Invalid rotation axis. Please choose from 'x', 'y', 'z'.")
# 生成旋转矩阵
R_angle = R.from_rotvec(rotation_vector).as_matrix()
# 提取旋转部分3x3矩阵
target_rotation = target_affine[:3, :3]
# 将 target_rotation 绕指定轴旋转指定角度,得到 target_rotation_2
if use_local:
target_rotation_2 = np.dot(target_rotation, R_angle)
else:
target_rotation_2 = np.dot(R_angle, target_rotation)
# 重新组合旋转矩阵 target_rotation_2 和原始的平移部分
target_affine_2 = np.eye(4)
target_affine_2[:3, :3] = target_rotation_2
target_affine_2[:3, 3] = target_affine[:3, 3] # 保留原始的平移部分
return target_affine_2
def rotate_along_axis(target_affine, angle_degrees, rot_axis='z', use_local=True):
'''
根据指定的角度和旋转轴来旋转target_affine。
参数:
- target_affine: 4x4 仿射变换矩阵
- angle_degrees: 旋转角度(以度为单位)
- rot_axis: 旋转轴,'x''y''z'
'''
# 将角度转换为弧度
angle_radians = np.deg2rad(angle_degrees)
# 创建旋转对象
if rot_axis == 'z':
rotation_vector = np.array([0, 0, angle_radians])
elif rot_axis == 'y':
rotation_vector = np.array([0, angle_radians, 0])
elif rot_axis == 'x':
rotation_vector = np.array([angle_radians, 0, 0])
else:
raise ValueError("Invalid rotation axis. Please choose from 'x', 'y', 'z'.")
# 生成旋转矩阵
R_angle = R.from_rotvec(rotation_vector).as_matrix()
# 提取旋转部分3x3矩阵
target_rotation = target_affine[:3, :3]
# 将 target_rotation 绕指定轴旋转指定角度,得到 target_rotation_2
if use_local:
target_rotation_2 = np.dot(target_rotation, R_angle)
else:
target_rotation_2 = np.dot(R_angle, target_rotation)
# 重新组合旋转矩阵 target_rotation_2 和原始的平移部分
target_affine_2 = np.eye(4)
target_affine_2[:3, :3] = target_rotation_2
target_affine_2[:3, 3] = target_affine[:3, 3] # 保留原始的平移部分
return target_affine_2
import numpy as np
from scipy.spatial.transform import Rotation as R
def get_quaternion_wxyz_from_rotation_matrix(rotation_matrix):
"""
Convert a 3x3 rotation matrix to a quaternion in the wxyz format.
Parameters:
R (numpy array): A 3x3 rotation matrix.
Returns:
numpy array: A 4x1 quaternion in the wxyz format.
"""
# Convert the rotation matrix to a quaternion
rot = R.from_matrix(rotation_matrix)
quat = rot.as_quat()
# Reorder the quaternion to the wxyz format
if quat.shape[0] == 4:
quaternions_wxyz = quat[[3, 0, 1, 2]]
else:
quaternions_wxyz = quat[:, [3, 0, 1, 2]]
return quaternions_wxyz
def get_quaternion_from_rotation_matrix(R):
assert R.shape == (3, 3)
# 计算四元数分量
trace = np.trace(R)
if trace > 0:
S = np.sqrt(trace + 1.0) * 2 # S=4*qw
qw = 0.25 * S
qx = (R[2, 1] - R[1, 2]) / S
qy = (R[0, 2] - R[2, 0]) / S
qz = (R[1, 0] - R[0, 1]) / S
elif (R[0, 0] > R[1, 1]) and (R[0, 0] > R[2, 2]):
S = np.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2]) * 2 # S=4*qx
qw = (R[2, 1] - R[1, 2]) / S
qx = 0.25 * S
qy = (R[0, 1] + R[1, 0]) / S
qz = (R[0, 2] + R[2, 0]) / S
elif R[1, 1] > R[2, 2]:
S = np.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2]) * 2 # S=4*qy
qw = (R[0, 2] - R[2, 0]) / S
qx = (R[0, 1] + R[1, 0]) / S
qy = 0.25 * S
qz = (R[1, 2] + R[2, 1]) / S
else:
S = np.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1]) * 2 # S=4*qz
qw = (R[1, 0] - R[0, 1]) / S
qx = (R[0, 2] + R[2, 0]) / S
qy = (R[1, 2] + R[2, 1]) / S
qz = 0.25 * S
return np.array([qw, qx, qy, qz])
# @nb.jit(nopython=True)
def get_rotation_matrix_from_quaternion(quat: np.ndarray) -> np.ndarray:
"""Convert a quaternion to a rotation matrix.
Args:
quat (np.ndarray): A 4x1 vector in order (w, x, y, z)
Returns:
np.ndarray: The resulting 3x3 rotation matrix.
"""
w, x, y, z = quat
rot = np.array(
[
[2 * (w ** 2 + x ** 2) - 1, 2 * (x * y - w * z), 2 * (x * z + w * y)],
[2 * (x * y + w * z), 2 * (w ** 2 + y ** 2) - 1, 2 * (y * z - w * x)],
[2 * (x * z - w * y), 2 * (y * z + w * x), 2 * (w ** 2 + z ** 2) - 1],
]
)
return rot
# @nb.jit(nopython=True)
def get_xyz_euler_from_quaternion(quat: np.ndarray) -> np.ndarray:
"""Convert a quaternion to XYZ euler angles.
Args:
quat (np.ndarray): A 4x1 vector in order (w, x, y, z).
Returns:
np.ndarray: A 3x1 vector containing (roll, pitch, yaw).
"""
w, x, y, z = quat
y_sqr = y * y
t0 = +2.0 * (w * x + y * z)
t1 = +1.0 - 2.0 * (x * x + y_sqr)
eulerx = np.arctan2(t0, t1)
t2 = +2.0 * (w * y - z * x)
t2 = +1.0 if t2 > +1.0 else t2
t2 = -1.0 if t2 < -1.0 else t2
eulery = np.arcsin(t2)
t3 = +2.0 * (w * z + x * y)
t4 = +1.0 - 2.0 * (y_sqr + z * z)
eulerz = np.arctan2(t3, t4)
result = np.zeros(3)
result[0] = eulerx
result[1] = eulery
result[2] = eulerz
return result
# @nb.jit(nopython=True)
def get_quaternion_from_euler(euler: np.ndarray, order: str = "XYZ") -> np.ndarray:
"""Convert an euler angle to a quaternion based on specified euler angle order.
Supported Euler angle orders: {'XYZ', 'YXZ', 'ZXY', 'ZYX', 'YZX', 'XZY'}.
Args:
euler (np.ndarray): A 3x1 vector with angles in radians.
order (str, optional): The specified order of input euler angles. Defaults to "XYZ".
Raises:
ValueError: If input order is not valid.
Reference:
[1] https://github.com/mrdoob/three.js/blob/master/src/math/Quaternion.js
"""
# extract input angles
r, p, y = euler
# compute constants
y = y / 2.0
p = p / 2.0
r = r / 2.0
c3 = np.cos(y)
s3 = np.sin(y)
c2 = np.cos(p)
s2 = np.sin(p)
c1 = np.cos(r)
s1 = np.sin(r)
# convert to quaternion based on order
if order == "XYZ":
result = np.array(
[
c1 * c2 * c3 - s1 * s2 * s3,
c1 * s2 * s3 + c2 * c3 * s1,
c1 * c3 * s2 - s1 * c2 * s3,
c1 * c2 * s3 + s1 * c3 * s2,
]
)
if result[0] < 0:
result = -result
return result
elif order == "YXZ":
result = np.array(
[
c1 * c2 * c3 + s1 * s2 * s3,
s1 * c2 * c3 + c1 * s2 * s3,
c1 * s2 * c3 - s1 * c2 * s3,
c1 * c2 * s3 - s1 * s2 * c3,
]
)
return result
elif order == "ZXY":
result = np.array(
[
c1 * c2 * c3 - s1 * s2 * s3,
s1 * c2 * c3 - c1 * s2 * s3,
c1 * s2 * c3 + s1 * c2 * s3,
c1 * c2 * s3 + s1 * s2 * c3,
]
)
return result
elif order == "ZYX":
result = np.array(
[
c1 * c2 * c3 + s1 * s2 * s3,
s1 * c2 * c3 - c1 * s2 * s3,
c1 * s2 * c3 + s1 * c2 * s3,
c1 * c2 * s3 - s1 * s2 * c3,
]
)
return result
elif order == "YZX":
result = np.array(
[
c1 * c2 * c3 - s1 * s2 * s3,
s1 * c2 * c3 + c1 * s2 * s3,
c1 * s2 * c3 + s1 * c2 * s3,
c1 * c2 * s3 - s1 * s2 * c3,
]
)
return result
elif order == "XZY":
result = np.array(
[
c1 * c2 * c3 + s1 * s2 * s3,
s1 * c2 * c3 - c1 * s2 * s3,
c1 * s2 * c3 - s1 * c2 * s3,
c1 * c2 * s3 + s1 * s2 * c3,
]
)
return result
else:
raise ValueError("Input euler angle order is meaningless.")
# @nb.jit(nopython=True)
def get_rotation_matrix_from_euler(euler: np.ndarray, order: str = "XYZ") -> np.ndarray:
quat = get_quaternion_from_euler(euler, order)
return get_rotation_matrix_from_quaternion(quat)
# @nb.jit(nopython=True)
def quat_multiplication(q: np.ndarray, p: np.ndarray) -> np.ndarray:
"""Compute the product of two quaternions.
Args:
q (np.ndarray): First quaternion in order (w, x, y, z).
p (np.ndarray): Second quaternion in order (w, x, y, z).
Returns:
np.ndarray: A 4x1 vector representing a quaternion in order (w, x, y, z).
"""
quat = np.array(
[
p[0] * q[0] - p[1] * q[1] - p[2] * q[2] - p[3] * q[3],
p[0] * q[1] + p[1] * q[0] - p[2] * q[3] + p[3] * q[2],
p[0] * q[2] + p[1] * q[3] + p[2] * q[0] - p[3] * q[1],
p[0] * q[3] - p[1] * q[2] + p[2] * q[1] + p[3] * q[0],
]
)
return quat
# @nb.jit(nopython=True)
def skew(vector: np.ndarray) -> np.ndarray:
"""Convert vector to skew symmetric matrix.
This function returns a skew-symmetric matrix to perform cross-product
as a matrix multiplication operation, i.e.:
np.cross(a, b) = np.dot(skew(a), b)
Args:
vector (np.ndarray): A 3x1 vector.
Returns:
np.ndarray: The resluting skew-symmetric matrix.
"""
mat = np.array([[0, -vector[2], vector[1]], [vector[2], 0, -vector[0]], [-vector[1], vector[0], 0]])
return mat
import math
_POLE_LIMIT = 1.0 - 1e-6
def matrix_to_euler_angles(mat: np.ndarray, degrees: bool = False, extrinsic: bool = True) -> np.ndarray:
"""Convert rotation matrix to Euler XYZ extrinsic or intrinsic angles.
Args:
mat (np.ndarray): A 3x3 rotation matrix.
degrees (bool, optional): Whether returned angles should be in degrees.
extrinsic (bool, optional): True if the rotation matrix follows the extrinsic matrix
convention (equivalent to ZYX ordering but returned in the reverse) and False if it follows
the intrinsic matrix conventions (equivalent to XYZ ordering).
Defaults to True.
Returns:
np.ndarray: Euler XYZ angles (intrinsic form) if extrinsic is False and Euler XYZ angles (extrinsic form) if extrinsic is True.
"""
if extrinsic:
if mat[2, 0] > _POLE_LIMIT:
roll = np.arctan2(mat[0, 1], mat[0, 2])
pitch = -np.pi / 2
yaw = 0.0
return np.array([roll, pitch, yaw])
if mat[2, 0] < -_POLE_LIMIT:
roll = np.arctan2(mat[0, 1], mat[0, 2])
pitch = np.pi / 2
yaw = 0.0
return np.array([roll, pitch, yaw])
roll = np.arctan2(mat[2, 1], mat[2, 2])
pitch = -np.arcsin(mat[2, 0])
yaw = np.arctan2(mat[1, 0], mat[0, 0])
if degrees:
roll = math.degrees(roll)
pitch = math.degrees(pitch)
yaw = math.degrees(yaw)
return np.array([roll, pitch, yaw])
else:
if mat[0, 2] > _POLE_LIMIT:
roll = np.arctan2(mat[1, 0], mat[1, 1])
pitch = np.pi / 2
yaw = 0.0
return np.array([roll, pitch, yaw])
if mat[0, 2] < -_POLE_LIMIT:
roll = np.arctan2(mat[1, 0], mat[1, 1])
pitch = -np.pi / 2
yaw = 0.0
return np.array([roll, pitch, yaw])
roll = -math.atan2(mat[1, 2], mat[2, 2])
pitch = math.asin(mat[0, 2])
yaw = -math.atan2(mat[0, 1], mat[0, 0])
if degrees:
roll = math.degrees(roll)
pitch = math.degrees(pitch)
yaw = math.degrees(yaw)
return np.array([roll, pitch, yaw])