使用四元数解决欧拉角万向锁问题(二)

一、背景

使用四元数解决欧拉角万向锁问题(一)一文中已经实现了基于固定空间坐标系下的滚转角改正(如下图1),规避了“ZXY”顺序内旋方式下,俯仰角为±Pi/2时的万向锁问题,这对于固定地点的视频投放已经够用了。但我们的系统获取了无人机的实时姿态,并基于此实现了无人机的视频画面的动态投放。因此希望能够根据无人机视频的实时投放姿态进行滚转角的改正(在机身坐标系下),这样会方便用户的使用。
在这里插入图片描述
图1 基于固定坐标系的滚转角改正效果
大体的思路是利用四元数与轴角表示法的关系,使用初始姿态+偏航俯仰改正(中间姿态)后的z轴向量作为转轴;将滚转角改正数作为转角,计算出对应的四元数,并作用于中间姿态以得到最终结果。(如下图2)
在这里插入图片描述
图2 基于机身坐标系的欧拉角改正方案

二、具体应用公式

1.单位四元数对应旋转作用于向量

其中,q为单位四元数,v为向量:
v ′ = q v q ˉ v' = q v \bar{q} v=qvqˉ

2.轴角表示转四元数

对于单位向量[nx,ny,nz]作为旋转轴,旋转θ角对应的单位四元数:
w = cos ⁡ ( θ 2 ) x = n x sin ⁡ ( θ 2 ) y = n y sin ⁡ ( θ 2 ) z = n z sin ⁡ ( θ 2 ) \begin{aligned} w &= \cos\left(\frac{\theta}{2}\right) \\ x &= n_x \sin\left(\frac{\theta}{2}\right) \\ y &= n_y \sin\left(\frac{\theta}{2}\right) \\ z &= n_z \sin\left(\frac{\theta}{2}\right) \end{aligned} wxyz=cos(2θ)=nxsin(2θ)=nysin(2θ)=nzsin(2θ)

三、代码及实验

1. python

# -*- coding: utf-8 -*-
# @Author: 27987
# @Date: 2024-7-2 14:17
# @Last Modified by: 27987
# @Last Modified time: 2024-7-2 14:17
import numpy as np
from scipy.spatial.transform import Rotation as R
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei']  # used for chinese support
plt.rcParams['axes.unicode_minus'] = False  # minus support

# 绘制旋转后的坐标轴
def plot_rotated_axes(ax, r, name=None, offset=(0, 0, 0), scale=1):
    colors = ("#FF6666", "#005533", "#1199EE")  # Colorblind-safe RGB
    loc = np.array([offset, offset])
    for i, (axis, c) in enumerate(zip((ax.xaxis, ax.yaxis, ax.zaxis),
                                      colors)):
        axlabel = axis.axis_name
        axis.set_label_text(axlabel)
        axis.label.set_color(c)
        axis.line.set_color(c)
        axis.set_tick_params(colors=c)
        line = np.zeros((2, 3))
        line[1, i] = scale
        line_rot = r.apply(line)
        line_plot = line_rot + loc
        ax.plot(line_plot[:, 0], line_plot[:, 1], line_plot[:, 2], c)
        text_loc = line[1]*1.2
        text_loc_rot = r.apply(text_loc)
        text_plot = text_loc_rot + loc[0]
        ax.text(*text_plot, axlabel.upper(), color=c,
                va="center", ha="center")
    ax.text(*offset, name, color="k", va="center", ha="center",
            bbox={"fc": "w", "alpha": 0.8, "boxstyle": "circle"})

def cal_quat_from_axis_rotate(axis, angle):
    w = np.cos(angle/2)
    x = np.sin(angle/2) * axis[0]
    y = np.sin(angle/2) * axis[1]
    z = np.sin(angle/2) * axis[2]
    return [x, y, z, w]
# test effects of different operations on initial rotation (r1)
def euler_to_quaternion_intrinsic_zxy(euler_angles):
    """
    Convert intrinsic ZXY Euler angles to quaternion.

    Parameters:
    euler_angles (tuple or list): Euler angles in ZXY order (yaw, pitch, roll) in radians.

    Returns:
    np.ndarray: Quaternion [x, y, z, w].
    """
    print("input:(45,90,90) ZXY order: yaw, pitch, roll")
    r0 = R.from_quat(np.asarray([0,0,0,1]))
    
    # Create a rotation object from Euler angles with intrinsic ZXY order
    r1 = R.from_euler('ZXY', euler_angles, degrees=False)
    # Convert the rotation object to a quaternion
    quaternion = r1.as_quat()
    print("r1: Together:")
    print(quaternion)
    
    r2 = R.from_euler('ZXY', (euler_angles[0], 0, 0), degrees=False)
    quaternion_rotate_z = r2.as_quat()
    print("r2: Z:")
    print(quaternion_rotate_z)
    
    r3 = R.from_euler('ZXY', (0, euler_angles[1], 0), degrees=False)
    quaternion_rotate_x = r3.as_quat()
    print("r3: X:")
    print(quaternion_rotate_x)
    
    r4 = R.from_euler('ZXY', (0, 0, euler_angles[2]), degrees=False)
    quaternion_rotate_y = r4.as_quat()
    print("r4: Y:")
    print(quaternion_rotate_y)
    
    quaternion_rotate_all = quaternion_multiply(quaternion_rotate_z,quaternion_multiply(quaternion_rotate_x,quaternion_rotate_y))
    r5 = R.from_quat(quaternion_rotate_all)
    print("r5: split input euler angles, turn them into quaterion and multiply:Z*X*Y:")
    print(quaternion_rotate_all)

    r6 = R.from_euler('ZXY', (0, 0, np.pi/4), degrees=False)
    quaternion_rotate_mod = r6.as_quat()
    print("r6: roll pi/4 to quaternion:")
    print(quaternion_rotate_mod)
    
    r7 = R.from_euler('ZXY', (np.pi/2, np.pi/2, np.pi/2 + np.pi/4), degrees=False)
    quaternion_rotate_plus = r7.as_quat()
    print("r7: rotation based on (pi/2, pi/2, 3pi/4):")
    print(quaternion_rotate_plus)
    
    r8 = R.from_quat(quaternion_rotate_plus)
    euler_plus = r8.as_euler('ZXY',degrees=True)
    print("r8、r7 to euler angles:")
    print(euler_plus)
    
    r9 = R.from_quat(quaternion)
    euler = r9.as_euler('ZXY', degrees=True)
    
    after_rotate = quaternion_multiply(quaternion_rotate_mod,quaternion)
    after_rotate_ = quaternion_multiply(quaternion,quaternion_rotate_mod)
    
    r10 = R.from_quat(after_rotate)
    euler_rotate = r10.as_euler('ZXY', degrees=True)
    
    print("r9: r1 to quaternion and rotate:", euler)
    print("r10: r6 to quaternion and multiply r9 from left:", euler_rotate)
    print("r10: pi/4 multiply r1 from left :")
    print(after_rotate)
    
    r11 = R.from_quat(after_rotate_)
    print("r11: r6 multiply r9 from right :")
    print(after_rotate_)
    print("r11: r11 to euler angles:")
    print(r11.as_euler('ZXY', degrees=True))

    relative_z = r1.apply([0,0,1])
    relative_z_quat = cal_quat_from_axis_rotate(relative_z, 45*np.pi/180)
    r12 = R.from_quat(relative_z_quat)

    r13 = r12 * r1
    r14 = r1 * r12


    ax = plt.figure().add_subplot(projection="3d", proj_type="ortho")
    
    # some test tring to feel the up and direction vector in Cesium and failed. QAQ
    # up = np.zeros((2,3))
    # direction = np.zeros((2,3))
    # direction[1,:] = [0.4178069101631521, -0.6706103372286868, -0.6129593472832818]
    # up[1,:] = [0.5775928646133864, -0.324720111520177, 0.7489615022963638]
    # ax.plot(direction[:, 0], direction[:, 1], direction[:, 2])
    # ax.plot(up[:, 0], up[:, 1], up[:, 2])
    
    plot_rotated_axes(ax, r0, name="r0", offset=(0, 0, 0))
    plot_rotated_axes(ax, r1, name="r1", offset=(3, 0, 0))
    # plot_rotated_axes(ax, r2, name="r2", offset=(3, 0, 0))
    # plot_rotated_axes(ax, r3, name="r3", offset=(6, 0, 0))
    # plot_rotated_axes(ax, r4, name="r4", offset=(6, 0, 0))
    # plot_rotated_axes(ax, r5, name="r5", offset=(9, 0, 0))
    plot_rotated_axes(ax, r6, name="r6", offset=(6, 0, 0))
    plot_rotated_axes(ax, r7, name="r7", offset=(9, 0, 0))
    plot_rotated_axes(ax, r8, name="r8", offset=(12, 0, 0))
    plot_rotated_axes(ax, r9, name="r9", offset=(15, 0, 0))
    plot_rotated_axes(ax, r10, name="r10", offset=(18, 0, 0))
    plot_rotated_axes(ax, r11, name="r11", offset=(21, 0, 0))
    plot_rotated_axes(ax, r12, name="r12", offset=(24, 0, 0))
    plot_rotated_axes(ax, r13, name="r13", offset=(27, 0, 0))
    plot_rotated_axes(ax, r14, name="r14", offset=(30, 0, 0))
    ax.set(xlim=(-1.25, 33), ylim=(-1.25, 1.25), zlim=(-1.25, 1.25))
    ax.set(xticks=range(-1, 33), yticks=[-1, 0, 1], zticks=[-1, 0, 1])
    ax.set_aspect("equal", adjustable="box")
    plt.tight_layout()
    # add comments
    _ = ax.annotate(
    "r0: no rotation\n"
    "r1: rotation based on input\n"
    # "r4: rotation based on imput roll angle\n"
    # "r5: split input euler angles, turn them into quaterion and multiply:Z*X*Y:\n"
    "r6: roll pi/4\n"
    "r7: directly add roll pi/4 by euler angles\n"
    "r8: r7 to quaterion and rotate\n"
    "r9: r1 to quaternion and rotate\n"
    "r10: r6*r9 quaternion\n"
    "r11: r9*r6 quaternion\n"
    "r12: rotate with z' for pi/4\n"
    "r13: r12*r1\n"
    "r14: r1*r12\n"
    ,
    xy=(0.6, 0.7), xycoords="axes fraction", ha="left"
)
    plt.show()
    return None

# 四元数乘法
def quaternion_multiply(q1, q2):
    """
    Multiply two quaternions.

    Parameters:
    q1, q2 (tuple or list): Quaternions in the form [x, y, z, w].

    Returns:
    np.ndarray: Resulting quaternion [x, y, z, w].
    """
    x1, y1, z1, w1 = q1
    x2, y2, z2, w2 = q2

    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([x, y, z, w])

if __name__ == "__main__":
    # Example usage
    euler_angles = (-17.9/180*np.pi, -np.pi/2, 0)  # ZXY order: yaw, pitch, roll
    # euler_angles = (np.pi/2, np.pi/2, np.pi/2)  # ZXY order: yaw, pitch, roll
    euler_to_quaternion_intrinsic_zxy(euler_angles)

在这里插入图片描述
图3 python实验结果

2. 实验结果以及分析

实验结果:

  1. r12 围绕 z’(r1 的 z轴)进行了45度旋转。
  2. r13 旋转符合预期。
  3. r14 旋转不知道如何解释。QAQ

四、验证

增加如下代码:

cal_quat_from_axis_rotate(axis, angle){
        let w = Math.cos(angle/2),
        temp = Math.sin(angle/2),
        x = temp * axis[0],
        y = temp * axis[1],
        z = temp * axis[2]
        return [x, y, z, w]
    }
    addModifiedRotation(yaw,pitch,roll,yaw_mod,pitch_mod,roll_mod){
        yaw = yaw / 180 * Math.PI
        pitch = pitch / 180 * Math.PI
        roll = roll / 180 * Math.PI
        yaw_mod = yaw_mod / 180 * Math.PI
        pitch_mod = pitch_mod / 180 * Math.PI
        roll_mod = roll_mod / 180 * Math.PI
        
        console.log("before modified: ", yaw / Math.PI * 180, pitch / Math.PI * 180, roll / Math.PI * 180)
        // yaw = 0;
        // pitch = Math.PI/2;
        // roll = 0;
        var qua_origin = this.eulerToQuaternionIntrinsicZXY([yaw + yaw_mod,pitch + pitch_mod,roll])
        console.log("try to modify: ", yaw_mod, pitch_mod, roll_mod)
        //计算动轴
        var relative_z = this.quaternionMultiply(this.quaternionMultiply(qua_origin, [0,0,1,0]),[-qua_origin[0], -qua_origin[1], -qua_origin[2], qua_origin[3]])
        
        console.log("getting desired axis:", relative_z)
        var relative_z_quat = this.cal_quat_from_axis_rotate(relative_z, roll_mod)
        console.log("getting desired axis quat:", relative_z_quat)
        // var qua_mod = this.eulerToQuaternionIntrinsicZXY([yaw_mod,pitch_mod,0])
        // var qua_res = this.quaternionMultiply(qua_origin, qua_mod)
        console.log("对应偏航和俯仰修正的四元数 printing qua_res:", qua_origin)
        var qua_final = this.quaternionMultiply(relative_z_quat, qua_origin)
        console.log("增加滚转修正", qua_final)
        var euler_res = this.quaternionToEulerIntrinsicZXY(qua_final)
        console.log("getting modified res:", euler_res)
        // let zaxis = new Cesium.Cartesian3;
        // Cesium.Cartesian3.fromElements(relative_z[0], relative_z[1], relative_z[2], zaxis)
        // console.log("zaxis: ", zaxis)
        return euler_res
    }
    //仅接受弧度制
    eulerToQuaternionIntrinsicZXY(eulerAngles) {
        const [yaw, pitch, roll] = eulerAngles;
    
        // Calculate trigonometric functions
        const cy = Math.cos(yaw * 0.5);
        const sy = Math.sin(yaw * 0.5);
        const cp = Math.cos(pitch * 0.5);
        const sp = Math.sin(pitch * 0.5);
        const cr = Math.cos(roll * 0.5);
        const sr = Math.sin(roll * 0.5);
    
        // Calculate quaternion
        const w = cr * cp * cy - sr * sp * sy;
        const x = cr * sp * cy - sr * cp * sy;
        const y = cr * sp * sy + sr * cp * cy;
        const z = cr * cp * sy + sr * sp * cy;
        // const w = cr * cp * cy - sr * sp * cy;
        // const x = cr * sp * cy + sr * cp * sy;
        // const y = cr * cp * sy - cr * sp * cy;
        // const z = sr * cp * cy - cr * sp * sy;
        console.log("converted: ",[x, y, z, w])
        return [x, y, z, w];
    }
    quaternionToEulerIntrinsicZXY(quaternion) {
        const [x, y, z, w] = quaternion;
        let precision = 30
        // Calculate intermediate values
        let x_x = parseFloat(x * x).toFixed(precision),
        y_y = parseFloat(y * y).toFixed(precision),
        z_z = parseFloat(z * z).toFixed(precision),
        x_y = parseFloat(x * y).toFixed(precision),
        x_z = parseFloat(x * z).toFixed(precision),
        w_x = parseFloat(w * x).toFixed(precision),
        w_z = parseFloat(w * z).toFixed(precision),
        w_y = parseFloat(w * y).toFixed(precision),
        z_y = parseFloat(z * y).toFixed(precision);
        // console.log("getting xx,yy,zz,xy,xz,wx,wz,wy,zy: ",x_x,y_y,z_z,x_y,x_z,w_x,w_z,w_y,z_y)

        const t12 = 2 * ((parseFloat(x_y) - parseFloat(w_z)).toFixed(precision));
        const t22 = (1 - parseFloat((2 * (parseFloat(x_x) + parseFloat(z_z)).toFixed(precision)).toFixed(precision))).toFixed(precision);
        const t31 = 2 * (parseFloat(x_z) - parseFloat(w_y)).toFixed(precision);
        const t32 = 2 * (parseFloat(z_y) + parseFloat(w_x)).toFixed(precision);
        const t33 = (1 - parseFloat(2 * (parseFloat(y_y) + parseFloat(x_x)).toFixed(precision))).toFixed(precision);
        // console.log("getting -t12,t22,-t31,t32,t33",-t12,t22,-t31,t32,t33)

        var pitch = 0
        var yaw = 0, roll = 0
        if(t32 != 1&& t32 != -1){
            pitch = Math.asin(t32);
            yaw = Math.atan2(-t12, t22);
            roll = Math.atan2(-t31, t33);
            console.log("yaw:", yaw, "pitch:", pitch, "roll", roll)
        }
        else{
            roll = 0
            const t11 = (1-parseFloat(2*(parseFloat(y_y) + parseFloat(z_z)).toFixed(precision))).toFixed(precision);
            const t21 = 2*(parseFloat(x_y) + parseFloat(w_z)).toFixed(precision);
            console.log("getting t11,t21", t11, t21)
            if (t32 = 1){
                pitch = Math.PI/2
                yaw = Math.atan2(t21, t11)
            }
            else if (t32 = -1){
                pitch = -Math.PI/2
                yaw = Math.atan2(t21, t11)
            }
            console.log("yaw:", yaw, "pitch:", pitch, "roll", roll)
        }
        // pitch = pitch/Math.PI*180;
        // yaw = yaw/Math.PI*180;
        // roll = roll/Math.PI*180;
        return [yaw, pitch, roll];
    }
    quaternionMultiply(q1, q2) {
        const [x1, y1, z1, w1] = q1;
        const [x2, y2, z2, w2] = q2;
    
        const w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2;
        const x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2;
        const y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2;
        const z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2;
    
        return [x, y, z, w];
    }

在这里插入图片描述
图4 JavaScript验证结果
由图4可见无论画面如何旋转,增加滚转角后画面形变始终为等腰梯形,符合预期。

五、存在问题

根据原始计算结果,上图4中的视频画面实际上旋转了180度,最终应用的代码投放前需要在滚转角上取个负号来加以改正,为什么会有画面的反转,以及这个负号为什么好用,我在StackOverflow上提了这个问题,还希望能有大佬不吝赐教。
这个滚转角要改正成正的91
图5 测试数据

六、参考资料

  1. 视觉SLAM十四讲:从理论到实践(高翔)
  2. 使用四元数解决欧拉角万向锁问题(一)
  3. 单位四元数对应旋转作用于向量
  4. 轴角表示转四元数
  5. wiki-Euler_angles#Tait–Bryan_angles
  6. Computing Euler angles from a rotation matrix
  7. scipy文档 scipy.spatial.transform.Rotation
  8. Cesium文档 Camera
  9. atan2与atan的区别
  • 14
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值