我正在尝试在我的OpenGL程序中将骨架动画的矩阵转换为四元数,但是我遇到了一个问题:
给定一些单位四元数,我需要获得一个四元数,用它来变换一个向量,使得变换后的向量是每个四元数分别变换后向量的平均值。(对于矩阵,我只需要将矩阵相加并除以矩阵数量即可)
我正在尝试在我的OpenGL程序中将骨架动画的矩阵转换为四元数,但是我遇到了一个问题:
给定一些单位四元数,我需要获得一个四元数,用它来变换一个向量,使得变换后的向量是每个四元数分别变换后向量的平均值。(对于矩阵,我只需要将矩阵相加并除以矩阵数量即可)
使用Python>=3.6中的Numpy最简单的Markley解决方案实现如下:
import numpy as np
def q_average(Q, W=None):
if W is not None:
Q *= W[:, None]
eigvals, eigvecs = np.linalg.eig(Q.T@Q)
return eigvecs[:, eigvals.argmax()]
其中Q
的大小为N x 4。得到的四元数已经被归一化。
在这种情况下,默认情况下权重相等为1。否则,您可以提供一个权重列表,大小为N(每个四元数一个权重)。
就是这样。
这是我的PyTorch实现,用于计算平均四元数。
def mean(Q, weights=None):
if weights is None:
weights = torch.ones(len(Q), device=torch.device("cuda:0")) / len(Q)
A = torch.zeros((4, 4), device=torch.device("cuda:0"))
weight_sum = torch.sum(weights)
oriented_Q = ((Q[:, 0:1] > 0).float() - 0.5) * 2 * Q
A = torch.einsum("bi,bk->bik", (oriented_Q, oriented_Q))
A = torch.sum(torch.einsum("bij,b->bij", (A, weights)), 0)
A /= weight_sum
q_avg = torch.linalg.eigh(A)[1][:, -1]
if q_avg[0] < 0:
return -q_avg
return q_avg
我使用了Markley, F. Landis等人的“平均四元数”算法,该算法在http://tbirdal.blogspot.com/2019/10/i-allocate-this-post-to-providing.html中介绍。
这里有一个GitHub Repo,包含了所建议算法的实现 :) https://github.com/christophhagen/averaging-quaternions
当然非常感谢并致以赞誉给 christophhagen ;)
有另一种基于slerp的方法,当平均四元数时,这可能是您实际想要的。
让我们首先将其与基于特征分析的平均值进行比较:
考虑两个四元数A和B的平均值,它们对应于绕X轴旋转0°和90°度,权重为w_A = 2和w_B = 1。
预期的加权平均值应该对应于绕X轴旋转30°。
基于slerp的加权平均值将返回预期值。
基于特征分析的加权平均值将返回一个26.56°的旋转。
基于特征分析的方法将返回最小化相应旋转矩阵的Frobenius范数的四元数。相反,基于slerp的方法将在四元数的3D旋转空间中计算平均值。
import math
import numpy as np
import quaternion # (pip install numpy-quaternion)
d2r = math.pi/180
r2d = 180/math.pi
def recover_angle(mat):
return np.arctan2(mat[1,0], mat[0,0])
# ground truth
angles = np.array([0,90])
weights = np.array([2,1])
mean_angle = np.sum(angles*(weights/weights.sum()))
quaternion_mean = quaternion.from_euler_angles(mean_angle*d2r,0,0)
# eigen analysis
Q = quaternion.as_float_array(
[
(weight**0.5) * quaternion.from_euler_angles(0,0,angle*d2r)
for angle,weight
in zip(angles,weights)
]
).T
QQT = Q @ Q.T
eigval,eigvec = np.linalg.eig(QQT)
quaternion_mean_eig = quaternion.from_float_array( eigvec[:,np.argmax(eigval)] )
# slerp based
def slerp_avg(quaternions, weights):
# welford's mean in terms of linear mix operations:
toqt = quaternion.from_float_array
mix = lambda a,b,k: quaternion.slerp(toqt(a),toqt(b),0,1,k)
wmean, wsum, num = quaternions[0], weights[0], len(weights)
for i in range(1,num):
wsum += weights[i]
k = (weights[i]/wsum)
# wmean := wmean*(1-k) + quaternions[i]*k
wmean = mix(wmean,quaternions[i],k)
return wmean
quaternion_mean_slerp = slerp_avg(quaternion.as_float_array(
[quaternion.from_euler_angles(0,0,angle*d2r) for angle in angles]
), weights)
mat_mean = quaternion.as_rotation_matrix(quaternion_mean)
mat_mean_eig = quaternion.as_rotation_matrix(quaternion_mean_eig)
mat_mean_slerp = quaternion.as_rotation_matrix(quaternion_mean_slerp)
print("expected", recover_angle(mat_mean)*r2d)
print("eigen", recover_angle(mat_mean_eig)*r2d)
print("slerp", recover_angle(mat_mean_slerp)*r2d)
输出:
expected 29.999999999999996
eigen 26.565051177077994
slerp 30.00000000000001
您可以在此处找到一个零依赖的C++库: https://github.com/xaedes/average_affine_transform_mat/