Среднее значение нескольких кватернионов?

Я пытаюсь переключиться с матриц на кватернионы для скелетной анимации в моей программе OpenGL, но столкнулся с проблемой:

Учитывая количество единичных кватернионов, мне нужно получить кватернион, который при использовании для преобразования вектора даст вектор, являющийся средним значением вектора, преобразованного каждым кватернионом в отдельности. (с матрицами я бы просто сложил матрицы вместе и разделил на количество матриц)


person jonathan    schedule 11.09.2012    source источник
comment
Это не решаемая проблема. Кватернионы не кодируют произвольные линейные преобразования в трехмерном пространстве; они кодируют только ортогональные преобразования. Но среднее нескольких ортогональных преобразований (в смысле: для каждого вектора взять все его преобразования и усреднить их) (в общем случае) не ортогонально, поэтому его нельзя получить с помощью кватерниона. Могут быть некоторые более тонкие понятия среднего, которые сохраняют ортогональность, но вы не можете получить свое понятие среднего.   -  person darij grinberg    schedule 12.09.2012
comment
Вся возможная арифметика с кватернионами находится на этой странице. Если вы не найдете его там, то... удачи.   -  person Jav_Rock    schedule 13.09.2012
comment
Рассматривали ли вы вариант с открытым исходным кодом для скелетной анимации, а не изобретать велосипед? Например, home.gna.org/cal3d   -  person JWWalker    schedule 15.09.2012


Ответы (13)


Вопреки распространенному мнению в индустрии компьютерной графики, для решения этой проблемы существует простой, надежный, точный и простой алгоритм, созданный в аэрокосмической отрасли. Он работает во времени, линейном по количеству усредняемых кватернионов плюс (большой) постоянный коэффициент.

Пусть Q = [a1q1, a2q2, ..., an кн],

где ai — вес i-го кватерниона, а q< sub>i – это усредняемый i-й кватернион в виде вектор-столбца. Таким образом, Q является матрицей 4×N.

Нормализованный собственный вектор, соответствующий наибольшему собственному значению QQT, является средневзвешенным. Поскольку QQT является самосопряженным и, по крайней мере, положительно полуопределенным, доступны быстрые и надежные методы решения этой собственной проблемы. Вычисление произведения матрица-матрица — единственный шаг, который растет с увеличением числа усредняемых элементов.

См. эту техническую заметку в Journal of Guidance, Control, and Dynamics за 2007 г., который представляет собой сводную статью об этом и других методах. В современную эпоху метод, который я цитировал выше, является хорошим компромиссом между надежностью и надежностью реализации и уже был опубликован в учебниках в 1978 году!

person Jonathan    schedule 10.12.2014
comment
Ответ Джонатана здесь описывает то же самое, что закодировано в ссылке Unity3D из ответа Натана. И ответ minorlogic включает в себя несколько нюансов, которые также отражены в примере кода Unity3D. Страница Unity3D Wiki (с кодом) кажется наиболее полным ответом. - person Gabe Halsmer; 26.12.2015
comment
@GabeHalsmer: это неправда. Единичный код подходит, если кватернионы достаточно похожи (это не что иное, как проверка среднего плюс отрицательного кватерниона). В указанной статье предлагается совершенно другое решение, подходящее для всех кватернионов, но со значительно более высокими вычислительными затратами. - person Martin; 26.05.2016
comment
Веса a_1, a_2, .., a_n в сумме составляют 1,0? - person Maksym Ganenko; 26.06.2019
comment
@Максим Ганенко Проверил, вроде нужно. - person ntv1000; 02.12.2019
comment
В этом ответе я предоставляю реализацию этого метода на Java: math.stackexchange.com/a/3435296/365886 - person Luke Hutchison; 31.10.2020

К сожалению, это не очень просто сделать, но возможно. Вот технический документ, объясняющий математику, стоящую за этим: http://ntrs.nasa.gov/archive/nasa/casi.ntrs.nasa.gov/20070017872_2007014421.pdf

Посетите вики-страницу Unity3D (нижеприведенный пример кода взят из той же статьи): http://wiki.unity3d.com/index.php/Averaging_Quaternions_and_Vectors

//Get an average (mean) from more then two quaternions (with two, slerp would be used).
//Note: this only works if all the quaternions are relatively close together.
//Usage: 
//-Cumulative is an external Vector4 which holds all the added x y z and w components.
//-newRotation is the next rotation to be added to the average pool
//-firstRotation is the first quaternion of the array to be averaged
//-addAmount holds the total amount of quaternions which are currently added
//This function returns the current average quaternion
public static Quaternion AverageQuaternion(ref Vector4 cumulative, Quaternion newRotation, Quaternion firstRotation, int addAmount){

    float w = 0.0f;
    float x = 0.0f;
    float y = 0.0f;
    float z = 0.0f;

    //Before we add the new rotation to the average (mean), we have to check whether the quaternion has to be inverted. Because
    //q and -q are the same rotation, but cannot be averaged, we have to make sure they are all the same.
    if(!Math3d.AreQuaternionsClose(newRotation, firstRotation)){

        newRotation = Math3d.InverseSignQuaternion(newRotation);    
    }

    //Average the values
    float addDet = 1f/(float)addAmount;
    cumulative.w += newRotation.w;
    w = cumulative.w * addDet;
    cumulative.x += newRotation.x;
    x = cumulative.x * addDet;
    cumulative.y += newRotation.y;
    y = cumulative.y * addDet;
    cumulative.z += newRotation.z;
    z = cumulative.z * addDet;      

    //note: if speed is an issue, you can skip the normalization step
    return NormalizeQuaternion(x, y, z, w);
}

public static Quaternion NormalizeQuaternion(float x, float y, float z, float w){

    float lengthD = 1.0f / (w*w + x*x + y*y + z*z);
    w *= lengthD;
    x *= lengthD;
    y *= lengthD;
    z *= lengthD;

    return new Quaternion(x, y, z, w);
}

//Changes the sign of the quaternion components. This is not the same as the inverse.
public static Quaternion InverseSignQuaternion(Quaternion q){

    return new Quaternion(-q.x, -q.y, -q.z, -q.w);
}

//Returns true if the two input quaternions are close to each other. This can
//be used to check whether or not one of two quaternions which are supposed to
//be very similar but has its component signs reversed (q has the same rotation as
//-q)
public static bool AreQuaternionsClose(Quaternion q1, Quaternion q2){

    float dot = Quaternion.Dot(q1, q2);

    if(dot < 0.0f){

        return false;                   
    }

    else{

        return true;
    }
}

Также этот пост: http://forum.unity3d.com/threads/86898-Average-quaternions

person Nathan Monteleone    schedule 27.11.2013
comment
В качестве примечания, вот реализация Matlab au.mathworks.com/matlabcentral/fileexchange / - person R.Falque; 23.01.2019
comment
Не могли бы вы объяснить логику AreQuaternionsClose? - person GalDude33; 05.10.2020
comment
@GalDude33 Скалярное произведение двух векторов также равно косинусу угла между ними, умноженному на их длины (см. раздел определения геометрии в точечный продукт). Длины всегда положительны, поэтому знак скалярного произведения равен знаку косинуса угла между векторами, что само по себе является сложным значением, но оно скажет вам, если два кватерниона... - person Jason C; 04.07.2021
comment
... указывали в одном направлении (если рассматривать их как 4-мерные точки, а не кватернионы), что является лишь приблизительным ответом на вопрос, являются ли знаки каждой компоненты инвертированными. Если это имеет смысл. Это просто, но мне как-то сложно объяснить в комментарии, хех. - person Jason C; 04.07.2021
comment
То есть: если вы представляете кватернионы как просто старые четырехмерные векторы в четырехмерном пространстве, их скалярное произведение будет отрицательным, если они указывают в противоположных направлениях (если угол между ними > 90 градусов). Да. Это более простое объяснение. Может быть. В точечных продуктах скрыто много информации. - person Jason C; 04.07.2021

Вот реализация функции MATLAB, которую я использую для усреднения кватернионов для оценки ориентации. MATLAB легко преобразовать в любой другой язык, за исключением того, что этот конкретный метод (Markley 2007) требует вычисления собственных векторов и собственных значений. Есть много библиотек (включая Eigen C++), которые могут сделать это за вас.

Вы можете прочитать описание/заголовок файла, чтобы увидеть математику из оригинальной статьи.

файл matlab, взятый с http://www.mathworks.com/matlabcentral/fileexchange/40098-tolgabirdal-averaging-quaternions :

% 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
person Gouda    schedule 28.03.2015
comment
Зачем нужен цикл for i=1:M? Разве вы не можете просто взвесить значения Q, а затем вычислить A=Q'*Q? - person fdermishin; 27.01.2018
comment
@fdermishin: Да, я проверил это, и он выдает те же значения с ошибкой около 10 ^ -15. Плюс, вероятно, намного быстрее. Вот код: Q = (weights .* Q) ./ sum(weights); A = transpose(Q) * Q; - person Fritz; 05.02.2019
comment
Для чего применяется масштаб, он влияет только на величину собственных значений. не собственные векторы % шкалы A=(1.0/wSum)*A; - person minorlogic; 17.06.2021

Я попытался использовать кватернионы, как было предложено здесь, но это не помогло. работаю над тем, что я пытаюсь сделать (модель была искажена), поэтому я просто преобразовал векторы по каждому кватерниону, а затем сделал среднее (пока не найду лучшее решение).

person jonathan    schedule 15.09.2012
comment
метод slerping в ссылке, которую вы предоставили, действительно сработал для меня. однако кватернионы были относительно близки. - person Eran; 06.11.2018

Вы не можете добавлять кватернионы. Что вы можете сделать, так это найти кватернион, который непрерывно вращается между двумя углами, в том числе наполовину. Интерполяция кватернионов известна как «slerp» и имеет страницу в Википедии. Это очень полезный трюк для анимации. В некотором отношении slerp является основной причиной использования кватернионов в компьютерной графике.

person burningbright    schedule 14.09.2012
comment
Вы можете добавлять кватернионы, но сумма двух единичных кватернионов не будет единичным кватернионом. - person JWWalker; 15.09.2012
comment
Но вы можете усреднить их, как показывает Гауда выше - stackoverflow.com/a/29315869/1200764 - person WillC; 07.04.2017

Это моя реализация алгоритма Толги Бердала на питоне:

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]
person consultit    schedule 06.04.2018
comment
В качестве дополнения вы можете сделать это с помощью np.linalg.eigh(np.einsum('ij,ik,i->...jk', q, q, w))[1][:, -1], что в сотни раз быстрее для больших наборов кватернионов. Масштабирование аккумулятора осуществляется только собственными значениями (не ортонормированными собственными векторами) и может быть опущено. - person reve_etrange; 22.04.2018
comment
@reve_etrange: предназначена ли эта строка для замены всей функции, чтобы ваш q был Q в ответе выше (а w был weights)? - person ketza; 10.05.2021
comment
Хорошо, теперь могу подтвердить, что он действительно заменяет всю функцию и дает те же результаты до 7-го десятичного знака или около того. - person ketza; 10.05.2021
comment
@кетц, да! Рад, что это помогло, это правильный способ сделать это ИМО. Я думаю, что различия в высокой точности - это просто разные ошибки с плавающей запятой, а не алгоритмические. - person reve_etrange; 11.05.2021

Существует технический отчет за 2001 год, в котором говорится, что среднее на самом деле является довольно хорошим приближением, при условии, что кватернионы лежат близко друг к другу. (для случая -q=q можно просто перевернуть те, которые указывают в другом направлении, предварительно умножив их на -1, чтобы все кватернионы включали жизнь в одну и ту же полусферу.

В этой статье 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;
person adrelino    schedule 20.10.2014
comment
Это те же вопросы на math.stackexchange.com - person adrelino; 20.10.2014

С кватернионами можно сделать то же самое, но с небольшой поправкой: 1. Отменить кватернион перед усреднением, если его скалярное произведение с предыдущей суммой отрицательно. 2. Нормировать средний кватернион, конец усреднения, если ваша библиотека работает с единичными кватернионами.

Средний кватернион будет представлять примерно среднее вращение (максимальная ошибка около 5 градусов).

ВНИМАНИЕ: Средняя матрица разных ориентаций может быть нарушена, если повороты слишком разные.

person minorlogic    schedule 26.03.2014

Кватернионы не являются идеальным набором степеней свободы для вращения при вычислении неограниченного среднего.

Вот чем я чаще всего пользуюсь(

[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;

    }`
person user3290232    schedule 30.09.2018

Поскольку здесь есть разные подходы, я написал скрипт Matlab для их сравнения. Эти результаты, кажется, предполагают, что простого усреднения и нормализации кватернионов (подход из вики Unity, называемый здесь 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']);
person Fritz    schedule 05.02.2019

Проверьте здесь мое решение взвешенного усреднения, а также медиану Lp кватернионов.

person Tolga Birdal    schedule 17.04.2020

Самая простая реализация (с Numpy в Python›=3.6) решения Маркли:

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 на 4. Полученный кватернион уже нормализован.

В этом случае веса по умолчанию равны 1. В противном случае вы можете дать список весов размера N (по одному на кватернион).

Вот и все.

person Mario García    schedule 03.04.2020
comment
Ограничение версии связано с оператором @, который введен в PEP465. python.org/dev/peps/pep-0465. Оператор @ представляет собой умножение матриц. - person Zailux; 24.07.2020
comment
Обратите внимание, что вы взвешиваете Q вместо Q*Q' . так что ваши веса - это квадрат разных версий алгоритма - person minorlogic; 17.06.2021

Вот репозиторий GitHub с реализацией предложенного алгоритма :) https://github.com/christophhagen/averaging-quaternions

Спасибо и кредиты christophhagen ofc ;)

person Zailux    schedule 24.07.2020