OpenGLプログラムで骨格アニメーションのマトリックスからクォータニオンへの切り替えを試みていますが、問題が発生しました。
多数のユニットクォータニオンが与えられた場合、ベクトルの変換に使用すると、各クォータニオンによって個別に変換されたベクトルの平均であるベクトルが得られるクォータニオンを取得する必要があります。 (マトリックスでは、単純にマトリックスを加算し、マトリックスの数で除算します)
here のようにクォータニオンをSlerpingしようとしましたが、それは私がやろうとしていることでは機能しませんでした(モデルが歪んでいた)ので、各クォータニオンごとにベクトルを変換し、平均(より良い解決策が見つかるまで)。
コンピュータグラフィックス業界で一般的な信念に反して、この問題を解決するための簡単なアルゴリズムがあります。このアルゴリズムは、航空宇宙業界に由来する堅牢で、正確で、シンプルです。これは、平均化される四元数の数に(Laggish)定数係数を加えた時間で線形に実行されます。
Q = [a_1 * q_1 a_2 * q_2 ... a_n * q_n]とする
ここで、a_iはi番目の四元数の重みであり、q_iは列ベクトルとして平均化されるi番目の四元数です。したがって、Qは4xN行列です。
Q * Q ^ Tの最大固有値に対応する正規化された固有ベクトルは、加重平均です。 Q * Q ^ Tは自己随伴であり、少なくとも正の半正定であるため、その固有問題を解く高速かつ堅牢な方法が利用可能です。マトリックス-マトリックス積の計算は、平均化される要素の数とともに増加する唯一のステップです。
これを参照してください 2007年のJournal of Guidance、Control、and Dynamicsのテクニカルノート 。これは、この方法と他の方法の要約論文です。近代では、上記で引用した方法は実装の信頼性と堅牢性の良いトレードオフとなり、1978年に教科書ですでに公開されていました!
残念ながら、それはそれほど簡単ではありませんが、可能です。その背後にある数学を説明するホワイトペーパーを次に示します。 http://ntrs.nasa.gov/archive/nasa/casi.ntrs.nasa.gov/20070017872_2007014421.pdf
Unity3D Wikiページ(コード付き)をご覧ください。 http://wiki.unity3d.com/index.php/Averaging_Quaternions_and_Vectors
また、この投稿: http://forum.unity3d.com/threads/86898-Average-quaternions
ここに実装があります 方向推定のためにクォータニオンを平均化するために使用するMATLAB関数の場合。この特定の方法(Markley 2007)が固有ベクトルと固有値の計算を必要とすることを除いて、MATLABを他の言語に変換するのは簡単です。これを行うことができる多くのライブラリ(Eigen C++を含む)があります。
ファイルの説明/ヘッダーを読んで、元の論文の数学を見ることができます。
http://www.mathworks.com/matlabcentral/fileexchange/40098-tolgabirdal-averaging-quaternions から取得したmatlabファイル:
% by Tolga Birdal
% Q is an Mx4 matrix of quaternions. weights is an Mx1 vector, a weight for
% each quaternion.
% Qavg is the weightedaverage quaternion
% This function is especially useful for example when clustering poses
% after a matching process. In such cases a form of weighting per rotation
% is available (e.g. number of votes), which can guide the trust towards a
% specific pose. weights might then be interpreted as the vector of votes
% per pose.
% Markley, F. Landis, Yang Cheng, John Lucas Crassidis, and Yaakov Oshman.
% "Averaging quaternions." Journal of Guidance, Control, and Dynamics 30,
% no. 4 (2007): 1193-1197.
function [Qavg]=quatWAvgMarkley(Q, weights)
% Form the symmetric accumulator matrix
A=zeros(4,4);
M=size(Q,1);
wSum = 0;
for i=1:M
q = Q(i,:)';
w_i = weights(i);
A=w_i.*(q*q')+A; % rank 1 update
wSum = wSum + w_i;
end
% scale
A=(1.0/wSum)*A;
% Get the eigenvector corresponding to largest eigen value
[Qavg, ~]=eigs(A,1);
end
これは、Tolga Birdalのアルゴリズムのpythonでの私の実装です。
import numpy as np
def quatWAvgMarkley(Q, weights):
'''
Averaging Quaternions.
Arguments:
Q(ndarray): an Mx4 ndarray of quaternions.
weights(list): an M elements list, a weight for each quaternion.
'''
# Form the symmetric accumulator matrix
A = np.zeros((4, 4))
M = Q.shape[0]
wSum = 0
for i in range(M):
q = Q[i, :]
w_i = weights[i]
A += w_i * (np.outer(q, q)) # rank 1 update
wSum += w_i
# scale
A /= wSum
# Get the eigenvector corresponding to largest eigen value
return np.linalg.eigh(A)[1][:, -1]
クォータニオンを追加することはできません。できることは、途中を含む2つの角度の間で連続的に回転するクォータニオンを見つけることです。四元数補間は「slerp」として知られ、ウィキペディアのページがあります。これは、アニメーションに非常に役立つトリックです。いくつかの点で、slerpはコンピューターグラフィックスで四元数を使用する主な理由です。
2001年のテクニカルレポート があります。これは、四元数が近くにある場合、平均は実際には非常に良い近似であることを示しています。 (-q = qの場合、すべての四元数が同じ半球での生命に関与するように、-1を事前に乗算することで、他の方向を指しているものを反転させることができます。
さらに良いアプローチは、SVDの使用を含む 2007年のこのペーパー でスケッチされています。これは、ネイサンが参照したのと同じ論文です。 C++だけでなく、 Matlabの実装 もあることを付け加えます。 matlabコードに付属のテストスクリプトを実行すると、関連するクォータニオンの小さな摂動(0.004 *均一ノイズ)に対して非常に良い結果が得られると言えます。
qinit=Rand(4,1);
Q=repmat(qinit,1,10);
% apply small perturbation to the quaternions
perturb=0.004;
Q2=Q+Rand(size(Q))*perturb;
ここにはさまざまなアプローチがあるため、それらを比較するMatlabスクリプトを作成しました。これらの結果は、クォータニオンを単純に平均化および正規化することを示唆しているようです(Unity Wikiからのアプローチ、simple_average
ここ)は、四元数が十分に類似しており、小さな偏差が許容される場合には十分かもしれません。
出力は次のとおりです。
everything okay, max angle offset == 9.5843
qinit to average: 0.47053 degrees
qinit to simple_average: 0.47059 degrees
average to simple_average: 0.00046228 degrees
loop implementation to matrix implementation: 3.4151e-06 degrees
そして、ここにコードがあります:
%% Generate random unity quaternion
rng(42); % set arbitrary seed for random number generator
M = 100;
qinit=Rand(1,4) - 0.5;
qinit=qinit/norm(qinit);
Qinit=repmat(qinit,M,1);
%% apply small perturbation to the quaternions
perturb=0.05; % 0.05 => +- 10 degrees of rotation (see angles_deg)
Q = Qinit + 2*(Rand(size(Qinit)) - 0.5)*perturb;
Q = Q ./ vecnorm(Q, 2, 2); % Normalize perturbed quaternions
Q_inv = Q * diag([1 -1 -1 -1]); % calculated inverse perturbed rotations
%% Test if everything worked as expected: assert(Q2 * Q2_inv = unity)
unity = quatmultiply(Q, Q_inv);
Q_diffs = quatmultiply(Qinit, Q_inv);
angles = 2*acos(Q_diffs(:,1));
angles_deg = wrapTo180(rad2deg(angles));
if sum(sum(abs(unity - repmat([1 0 0 0], M, 1)))) > 0.0001
disp('error, quaternion inversion failed for some reason');
else
disp(['everything okay, max angle offset == ' num2str(max(angles_deg))])
end
%% Calculate average using matrix implementation of eigenvalues algorithm
[average,~] = eigs(transpose(Q) * Q, 1);
average = transpose(average);
diff = quatmultiply(qinit, average * diag([1 -1 -1 -1]));
diff_angle = 2*acos(diff(1));
%% Calculate average using algorithm from https://stackoverflow.com/a/29315869/1221661
average2 = quatWAvgMarkley(Q, ones(M,1));
diff2 = quatmultiply(average, average2 * diag([1 -1 -1 -1]));
diff2_angle = 2*acos(diff2(1));
%% Simply add coefficients and normalize the result
simple_average = sum(Q) / norm(sum(Q));
simple_diff = quatmultiply(qinit, simple_average * diag([1 -1 -1 -1]));
simple_diff_angle = 2*acos(simple_diff(1));
simple_to_complex = quatmultiply(simple_average, average * diag([1 -1 -1 -1]));
simple_to_complex_angle = 2*acos(simple_to_complex(1));
%% Compare results
disp(['qinit to average: ' num2str(wrapTo180(rad2deg(diff_angle))) ' degrees']);
disp(['qinit to simple_average: ' num2str(wrapTo180(rad2deg(simple_diff_angle))) ' degrees']);
disp(['average to simple_average: ' num2str(wrapTo180(rad2deg(simple_to_complex_angle))) ' degrees']);
disp(['loop implementation to matrix implementation: ' num2str(wrapTo180(rad2deg(diff2_angle))) ' degrees']);
クォータニオンを使用しても同じことができますが、わずかな修正を行います。 2.ライブラリがユニットクォータニオンで動作する場合、平均化の最後である平均クォータニオンを正規化します。
平均四元数は、ほぼ平均的な回転(最大誤差約5度)を表します。
警告:回転があまりにも異なる場合、異なる方向の平均マトリックスが壊れる可能性があります。
四元数は、制約のない平均を計算するときに回転に使用する理想的なDOFのセットではありません。
これが私がよく使うものです(
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector3 ToAngularVelocity( this Quaternion q )
{
if ( abs(q.w) > 1023.5f / 1024.0f)
return new Vector3();
var angle = acos( abs(q.w) );
var gain = Sign(q.w)*2.0f * angle / Sin(angle);
return new Vector3(q.x * gain, q.y * gain, q.z * gain);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Quaternion FromAngularVelocity( this Vector3 w )
{
var mag = w.magnitude;
if (mag <= 0)
return Quaternion.identity;
var cs = cos(mag * 0.5f);
var siGain = sin(mag * 0.5f) / mag;
return new Quaternion(w.x * siGain, w.y * siGain, w.z * siGain, cs);
}
internal static Quaternion Average(this Quaternion refence, Quaternion[] source)
{
var refernceInverse = refence.Inverse();
Assert.IsFalse(source.IsNullOrEmpty());
Vector3 result = new Vector3();
foreach (var q in source)
{
result += (refernceInverse*q).ToAngularVelocity();
}
return reference*((result / source.Length).FromAngularVelocity());
}
internal static Quaternion Average(Quaternion[] source)
{
Assert.IsFalse(source.IsNullOrEmpty());
Vector3 result = new Vector3();
foreach (var q in source)
{
result += q.ToAngularVelocity();
}
return (result / source.Length).FromAngularVelocity();
}
internal static Quaternion Average(Quaternion[] source, int iterations)
{
Assert.IsFalse(source.IsNullOrEmpty());
var reference = Quaternion.identity;
for(int i = 0;i < iterations;i++)
{
reference = Average(reference,source);
}
return reference;
}`