Averaging together pairs of quaternions
If you're trying to average just two quaternions, you can use quaternion interpolation (slerp) with an interpolation factor of 0.5 to find the "rotational midpoint" between the two quaternions.
(Note that Eq. 19 of the Markley et al. paper cited below also gives a direct / closed form solution for the average of two quaternions. This may or may not give the same result as slerp with an interpolation factor of 0.5, I haven't looked at it closely.)
Averaging together more than two quaternions
There are at least two methods for averaging together more than two quaternions.
1. Direct averaging (fast/approximate)
If quaternions represent similar rotations, and the quaternions are normalized, and a correction has been applied for the "double-cover problem", then the quaternions can be directly averaged and then the result normalized again, treating them as 4-dimensional vectors, to produce a quaternion representing a roughly-average rotation.
The double-cover problem is where $q$ and $-q \,$ represent the same rotation, as the original poster pointed out. This problem can be addressed by negating some subset of the quaternions to ensure that the dot product of any pair of quaternions is positive (or, w.l.o.g., negative).
Note that the more dissimilar the quaternions involved, the more inaccurate this method will be, since this is only an approximation to the eigendecomposition method -- so you only want to average together quaternions that are already fairly similar in terms of the rotations they represent.
I measured this method at 2.56x faster than the eigendecomposition method.
(Syntax used is for the JOML Java library)
public static Quaterniond averageApprox(Quaterniond[] quats, double[] weights) {
if (weights != null && quats.length != weights.length) {
throw new IllegalArgumentException("Args are of different length");
}
Quaterniond qAvg = new Quaterniond(0.0, 0.0, 0.0, 0.0);
for (int i = 0; i < quats.length; i++) {
Quaterniond q = quats[i];
double weight = weights == null ? 1.0 : weights[i];
// Correct for double cover, by ensuring that dot product
// of quats[i] and quats[0] is positive
if (i > 0 && quats[i].dot(quats[0]) < 0.0) {
weight = -weight;
}
qAvg.add(weight * q.x, weight * q.y, weight * q.z, weight * q.w);
}
return qAvg.normalize();
}
2. Eigendecomposition of sum of outer products (maximum likelihood method)
This method is based on Markley et al., Averaging Quaternions,
Journal of Guidance, Control, and Dynamics, 30(4):1193-1196, June 2007, Eqs. (12) and (13). The authors proved that the solution is unique, and that it corresponds to a maximum likelihood criterion. (tbirdal's implementation of this same algorithm, plus some more powerful variants, were linked from a couple of other answers.)
(Syntax used is for the Java JOML library and the Apache Commons Math Linear Algebra library)
public static Quaterniond average(Quaterniond[] quats, double[] weights) {
if (weights != null && quats.length != weights.length) {
throw new IllegalArgumentException("Args are of different length");
}
RealMatrix accum = MatrixUtils.createRealMatrix(4, 4);
for (int i = 0; i < quats.length; i++) {
Quaterniond q = quats[i];
double weight = weights == null ? 1.0 : weights[i];
RealVector qVec = MatrixUtils.createRealVector(
new double[] { q.x, q.y, q.z, q.w });
RealMatrix qOuterProdWeighted = qVec.outerProduct(qVec)
.scalarMultiply(weight);
accum = accum.add(qOuterProdWeighted);
}
EigenDecomposition eigDecomp = new EigenDecomposition(accum);
double[] ev0 = eigDecomp.getEigenvector(0).toArray();
return new Quaterniond(ev0[0], ev0[1], ev0[2], ev0[3]).normalize();
}