Metrics#

Common metrics used in 3D Orientation representations.

References

[Huynh] (1,2,3,4,5)

Huynh, D.Q. Metrics for 3D Rotations: Comparison and Analysis. J Math Imaging Vis 35, 155-164 (2009).

[Kuffner]

Kuffner, J.J. Effective Sampling and Distance Metrics for 3D Rigid Body Path Planning. IEEE International Conference on Robotics and Automation (ICRA 2004)

[Hartley] (1,2,3)

R. Hartley, J. Trumpf, Y. Dai, H. Li. Rotation Averaging. International Journal of Computer Vision. Volume 101, Number 2. 2013.

ahrs.utils.metrics.angular_distance(R1: ndarray, R2: ndarray) float#

Angular distance between two rotations \(\mathbf{R}_1\) and \(\mathbf{R}_2\) in SO(3), as defined in [Hartley]:

\[d(\mathbf{R}_1, \mathbf{R}_2) = \|\log(\mathbf{R}_1\mathbf{R}_2^T)\|\]

where \(\|\mathbf{x}\|\) represents the usual euclidean norm of the vector \(\mathbf{x}\).

Parameters:
  • R1 (numpy.ndarray) – 3-by-3 rotation matrix.

  • R2 (numpy.ndarray) – 3-by-3 rotation matrix.

Returns:

d – Angular distance between rotation matrices

Return type:

float

Examples

>>> import ahrs
>>> R1 = ahrs.DCM(rpy=[0.0, 0.0, 0.0])
>>> R2 = ahrs.DCM(rpy=[90.0, 90.0, 90.0])
>>> ahrs.utils.angular_distance(R1, R2)
1.5707963267948966
>>> R1 = ahrs.DCM(rpy=[10.0, -20.0, 30.0])
>>> R2 = ahrs.DCM(rpy=[-10.0, 20.0, -30.0])
>>> ahrs.utils.angular_distance(R1, R2)
1.282213683073497
ahrs.utils.metrics.chordal(R1: ndarray, R2: ndarray) float | ndarray#

Chordal Distance

The chordal distance between two rotations \(\mathbf{R}_1\) and \(\mathbf{R}_2\) in SO(3) is the Euclidean distance between them in the embedding space \(\mathbb{R}^{3\times 3}=\mathbb{R}^9\) [Hartley]:

\[d(\mathbf{R}_1, \mathbf{R}_2) = \|\mathbf{R}_1-\mathbf{R}_2\|_F\]

where \(\|\mathbf{X}\|_F\) represents the Frobenius norm of the matrix \(\mathbf{X}\).

The error lies within: [0, \(2\sqrt{3}\)]

Parameters:
  • R1 (numpy.ndarray) – 3-by-3 rotation matrix.

  • R2 (numpy.ndarray) – 3-by-3 rotation matrix.

Returns:

d – Chordal distance between matrices.

Return type:

float or numpy.ndarray

Examples

>>> import ahrs
>>> R1 = ahrs.DCM(rpy=[0.0, 0.0, 0.0])
>>> R2 = ahrs.DCM(rpy=[90.0, 90.0, 90.0])
>>> ahrs.utils.chordal(R1, R2)
2.0
>>> R1 = ahrs.DCM(rpy=[10.0, -20.0, 30.0])
>>> R2 = ahrs.DCM(rpy=[-10.0, 20.0, -30.0])
>>> ahrs.utils.chordal(R1, R2)
1.6916338074634352
ahrs.utils.metrics.euclidean(x: ndarray, y: ndarray, **kwargs) float#

Euclidean distance between two arrays as described in [Huynh]:

\[d(\mathbf{x}, \mathbf{y}) = \sqrt{(x_0-y_0)^2 + \dots + (x_n-y_n)^2}\]

Accepts the same parameters as the function numpy.linalg.norm().

This metric gives values in the range [0, \(\pi\sqrt{3}\)]

Parameters:
  • x (array) – M-by-N array to compare. Usually a reference array.

  • y (array) – M-by-N array to compare.

  • mode (str) – Mode of distance computation.

Returns:

d – Distance or difference between arrays.

Return type:

float

Examples

>>> import numpy as np
>>> from ahrs.utils.metrics import euclidean
>>> num_samples = 5
>>> angles = np.random.uniform(low=-180.0, high=180.0, size=(num_samples, 3))
>>> noisy = angles + np.random.randn(num_samples, 3)
>>> euclidean(angles, noisy)
2.585672169476804
>>> euclidean(angles, noisy, axis=0)
array([1.36319772, 1.78554071, 1.28032688])
>>> euclidean(angles, noisy, axis=1)     # distance per sample
array([0.88956871, 1.19727356, 1.5243858 , 0.68765523, 1.29007067])
ahrs.utils.metrics.identity_deviation(R1: ndarray, R2: ndarray) float#

Deviation from Identity Matrix as defined in [Huynh]:

\[d(\mathbf{R}_1, \mathbf{R}_2) = \|\mathbf{I}-\mathbf{R}_1\mathbf{R}_2^T\|_F\]

where \(\|\mathbf{X}\|_F\) represents the Frobenius norm of the matrix \(\mathbf{X}\).

The error lies within: [0, \(2\sqrt{3}\)]

Parameters:
  • R1 (numpy.ndarray) – 3-by-3 rotation matrix.

  • R2 (numpy.ndarray) – 3-by-3 rotation matrix.

Returns:

d – Deviation from identity matrix.

Return type:

float

Examples

>>> import ahrs
>>> R1 = ahrs.DCM(rpy=[0.0, 0.0, 0.0])
>>> R2 = ahrs.DCM(rpy=[90.0, 90.0, 90.0])
>>> ahrs.utils.identity_deviation(R1, R2)
2.0
>>> R1 = ahrs.DCM(rpy=[10.0, -20.0, 30.0])
>>> R2 = ahrs.DCM(rpy=[-10.0, 20.0, -30.0])
>>> ahrs.utils.identity_deviation(R1, R2)
1.6916338074634352
ahrs.utils.metrics.qad(q1: ndarray, q2: ndarray) float#

Quaternion Angle Difference as defined in [Thibaud]:

\[d(\mathbf{q}_1, \mathbf{q}_2) = \arccos(2\langle\mathbf{q}_1,\mathbf{q}_2\rangle^2-1)\]

The error lies within: [0, \(\pi\)]

Parameters:
  • q1 (numpy.ndarray) – First quaternion, or set of quaternions, to compare.

  • q2 (numpy.ndarray) – Second quaternion, or set of quaternions, to compare.

Returns:

d – Angle difference between given unit quaternions.

Return type:

float

Examples

>>> q1 = ahrs.Quaternion(random=True)
>>> q1.view()
Quaternion([ 0.94185064,  0.04451339, -0.00622856,  0.33301221])
>>> q2 = ahrs.Quaternion(random=True)
>>> q2.view()
Quaternion([-0.51041283, -0.38336653,  0.76929238, -0.0264211 ])
>>> ahrs.utils.qad(q1, q2)
2.0679949008393335
>>> ahrs.utils.qad(q1, -q1)
0.0

References

[Thibaud]

Thibaud Michel, Pierre Genevès, Hassen Fourati, Nabil Layaïda. On Attitude Estimation with Smartphones. IEEE International Conference on Pervasive Computing and Communications, Mar 2017, Kona, United States. ⟨hal-01376745v2⟩

ahrs.utils.metrics.qcip(q1: ndarray, q2: ndarray) float#

Cosine of inner products as defined in [Huynh]:

\[d(\mathbf{q}_1, \mathbf{q}_2) = \arccos(|\mathbf{q}_1\cdot\mathbf{q}_2|)\]

The error lies within: [0, \(\frac{\pi}{2}\)]

Parameters:
  • q1 (numpy.ndarray) – First quaternion, or set of quaternions, to compare.

  • q2 (numpy.ndarray) – Second quaternion, or set of quaternions, to compare.

Returns:

d – Cosine of inner products of quaternions.

Return type:

float

Examples

>>> q1 = ahrs.Quaternion(random=True)
>>> q1.view()
Quaternion([ 0.94185064,  0.04451339, -0.00622856,  0.33301221])
>>> q2 = ahrs.Quaternion(random=True)
>>> q2.view()
Quaternion([-0.51041283, -0.38336653,  0.76929238, -0.0264211 ])
>>> ahrs.utils.qcip(q1, q2)
1.0339974504196667
>>> ahrs.utils.qcip(q1, -q1)
0.0
ahrs.utils.metrics.qdist(q1: ndarray, q2: ndarray) float#

Euclidean distance between two unit quaternions as defined in [Huynh] and [Hartley]:

\[d(\mathbf{q}_1, \mathbf{q}_2) = \mathrm{min} \{ \|\mathbf{q}_1-\mathbf{q}_2\|, \|\mathbf{q}_1-\mathbf{q}_2\|\}\]

The error lies within [0, \(\sqrt{2}\)]

Parameters:
  • q1 (numpy.ndarray) – First quaternion, or set of quaternions, to compare.

  • q2 (numpy.ndarray) – Second quaternion, or set of quaternions, to compare.

Returns:

d – Euclidean distance between given unit quaternions.

Return type:

float

Examples

>>> q1 = ahrs.Quaternion(random=True)
>>> q1.view()
Quaternion([ 0.94185064,  0.04451339, -0.00622856,  0.33301221])
>>> q2 = ahrs.Quaternion(random=True)
>>> q2.view()
Quaternion([-0.51041283, -0.38336653,  0.76929238, -0.0264211 ])
>>> ahrs.utils.qdist(q1, q2)
0.9885466801358284
>>> ahrs.utils.qdist(q1, -q1)
0.0
ahrs.utils.metrics.qeip(q1: ndarray, q2: ndarray) float#

Euclidean distance of inner products as defined in [Huynh] and [Kuffner]:

\[d(\mathbf{q}_1, \mathbf{q}_2) = 1 - |\mathbf{q}_1\cdot\mathbf{q}_2|\]

The error lies within: [0, 1]

Parameters:
  • q1 (numpy.ndarray) – First quaternion, or set of quaternions, to compare.

  • q2 (numpy.ndarray) – Second quaternion, or set of quaternions, to compare.

Returns:

d – Euclidean distance of inner products between given unit quaternions.

Return type:

float

Examples

>>> q1 = ahrs.Quaternion(random=True)
>>> q1.view()
Quaternion([ 0.94185064,  0.04451339, -0.00622856,  0.33301221])
>>> q2 = ahrs.Quaternion(random=True)
>>> q2.view()
Quaternion([-0.51041283, -0.38336653,  0.76929238, -0.0264211 ])
>>> ahrs.utils.qeip(q1, q2)
0.48861226940378377
>>> ahrs.utils.qeip(q1, -q1)
0.0
ahrs.utils.metrics.rmse(x: ndarray, y: ndarray)#

Root Mean Squared Error

It is computed as:

\[d(\mathbf{x}, \mathbf{y}) = \sqrt{\frac{1}{N}\sum_{i=1}^N (x_i-y_i)^2}\]

where \(N\) is the number of elements in \(\mathbf{x}\) and \(\mathbf{y}\).

If \(\mathbf{x}\) and \(\mathbf{y}\) are \(M \times N\) matrices, then the RMSE is computed for each row, yielding a vector of length \(M\).

Parameters:
  • x (numpy.ndarray) – First set of values to compare.

  • y (numpy.ndarray) – Second set of values to compare.

Returns:

d – Root mean squared error between given values.

Return type:

float

ahrs.utils.metrics.rmse_matrices(A: ndarray, B: ndarray, element_wise: bool = False) ndarray#

Root Mean Square Error between two arrays (matrices)

Parameters:
  • A (np.ndarray) – First M-by-N matrix or array of k M-by-N matrices.

  • B (np.ndarray) – Second M-by-N matrix or array of k M-by-N matrices.

  • element_wise (bool, default: False) – If True, calculate RMSE element-wise, and return an M-by-N array of RMSEs.

Returns:

rmse – Root Mean Square Error between the two matrices, or array of k RMSEs between the two arrays of matrices.

Return type:

float or np.ndarray

Raises:

ValueError – If the comparing arrays do not have the same shape.

Notes

If the input arrays are 2-dimensional matrices, the RMSE is calculated as:

\[RMSE = \sqrt{\frac{1}{MN}\sum_{i=1}^{M}\sum_{j=1}^{N}(A_{ij} - B_{ij})^2}\]

If the input arrays are arrays of 2-dimensional matrices (3-dimensional array), the RMSE is calculated as:

\[RMSE = \sqrt{\frac{1}{k}\sum_{l=1}^{k}\frac{1}{MN}\sum_{i=1}^{M}\sum_{j=1}^{N}(A_{ij}^{(l)} - B_{ij}^{(l)})^2}\]

where \(k\) is the number of matrices in the arrays.

If the option element_wise is set to True, the RMSE is calculated element-wise, and an M-by-N array of RMSEs is returned. The following calls are equivalent:

rmse = rmse_matrices(A, B, element_wise=True)
rmse = np.sqrt(np.nanmean((A-B)**2, axis=0))

If the inputs are arrays of matrices (3-dimensional arrays), its call is also equivalent to:

rmse = np.zeros_like(A[0])
for i in range(A.shape[1]):
    for j in range(A.shape[2]):
        rmse[i, j] = np.sqrt(np.nanmean((A[:, i, j]-B[:, i, j])**2))

If the inputs are 2-dimensional matrices, the following calls would return the same result:

rmse_matrices(A, B)
rmse_matrices(A, B, element_wise=False)
rmse_matrices(A, B, element_wise=True)

Examples

>>> C = np.random.random((4, 3, 2))     # Array of four 3-by-2 matrices
>>> C.view()
array([[[0.2816407 , 0.30850589],
        [0.44618209, 0.33081522],
        [0.7994625 , 0.07377569]],

       [[0.35549399, 0.47050713],
        [0.94168683, 0.50388058],
        [0.70023837, 0.77216167]],

       [[0.79897129, 0.28555452],
        [0.892488  , 0.71476669],
        [0.19071524, 0.4123666 ]],

       [[0.86301978, 0.14686002],
        [0.98784823, 0.26129908],
        [0.46982206, 0.88037599]]])
>>> D = np.random.random((4, 3, 2))     # Array of four 3-by-2 matrices
>>> D.view()
array([[[0.71560918, 0.34100321],
        [0.92518341, 0.50741267],
        [0.30730944, 0.19173378]],

       [[0.31846657, 0.08578454],
        [0.62643489, 0.84014104],
        [0.7111152 , 0.95428613]],

       [[0.8101591 , 0.9584096 ],
        [0.91118705, 0.71203119],
        [0.58217189, 0.45598271]],

       [[0.79837603, 0.09954558],
        [0.26532781, 0.55711476],
        [0.03909648, 0.10787888]]])
>>> rmse_matrices(C[0], D[0])       # RMSE between first matrices
0.3430603410873006
>>> rmse_matrices(C, D)             # RMSE between each of the four matrices
array([0.34306034, 0.25662067, 0.31842239, 0.48274156])
>>> rmse_matrices(C, D, element_wise=True)  # RMSE element-wise along first dimension
array([[0.22022923, 0.3886001 ],
       [0.46130561, 0.2407136 ],
       [0.38114819, 0.40178899]])
>>> rmse_matrices(C[0], D[0], element_wise=True)
0.3430603410873006
>>> rmse_matrices(C[0], D[0])
0.3430603410873006