mirror of
https://github.com/facebookresearch/pytorch3d.git
synced 2026-08-18 22:05:43 +08:00
Summary: GitHub issue #2002 (https://github.com/facebookresearch/pytorch3d/issues/2002) points out that the near-pi branch of `matrix_to_axis_angle(..., fast=True)` normalizes with `torch.norm(n)`, which reduces over the whole batch. That is real, and there were two further problems next to it. `matrix_to_axis_angle(..., fast=True)`: - The axis was read from row 0 of `(R + I) / 2`, which is `n_x * n`. That is exactly zero whenever the axis is perpendicular to x, giving `nan` (for example for an exact rotation by pi about y), and is dominated by rounding when `abs(n_x)` is small. It is now read from `R + R^T - 2*cos(angle)*I == 2*(1 - cos(angle))*nnT`, taking the column whose diagonal entry is largest so that the multiplier is at least `1/sqrt(3)`. Symmetrizing also makes the identity exact at every angle rather than only at pi. The sign, which `nnT` does not determine, comes from `omegas`. - `torch.norm(n)` becomes a per-row `torch.linalg.vector_norm`, which is the reported bug: with more than one near-pi rotation in a batch, every one of them was scaled wrongly. - The branch threshold was `isclose(angle, pi)`. `omegas` is `2*sin(angle)` times the axis, so the other branch loses relative precision like `1/(pi - angle)`, and just outside `isclose` the float32 round-trip error reached 9.6e-4. The threshold is now `pi - 1e-2`, which leaves under 1% of uniformly random rotations on the more expensive branch. - The `torch.isclose(angles, 0)` guard on `omegas` is removed. `torch.sinc(0)` is 1, so a zero angle was never a special case, and `torch.norm` has a zero rather than `nan` gradient at zero. The guard only zeroed the answer for angles below its `atol` of 1e-8, returning 0 instead of an exact 1e-9 rotation, and cost 24% at a batch of 100k. Worst-case float32 round-trip matrix error over random axes, before -> after: exactly pi `nan` or 2.0 -> 3.6e-07; `pi - 1e-5` 2.0 -> 4.2e-07; `pi - 1e-4` 9.6e-04 -> 3.6e-07. The worst case at any angle is now 9.9e-06, at the branch boundary, against 4.8e-07 for `fast=False`. On the choice of `pi - 1e-2`: the new near-pi branch is accurate at any threshold, so the threshold only decides how much of the batch takes the slower branch, and the worst case is always the angle just below it. Measured on one batch of 100k random float32 rotations on CPU, where `fast=False` took 11.4 ms, threshold against fraction of uniformly random rotations selected, time, and worst-case round-trip error: - `isclose(angle, pi)`, the previous behaviour: 0.003%, 8.6 ms, 1e-03 - `pi - 1e-3`: 0.1%, 8.9 ms, 9e-05 - `pi - 1e-2`, chosen: 0.7%, 8.9 ms, 1e-05 - `pi - 0.05`: 3.2%, 10.2 ms, 3e-06 - `3.0` radians: 9.0%, 17.6 ms, 9e-07 So `pi - 1e-2` buys two orders of magnitude of accuracy over the old threshold for no measurable time, and it is the last threshold that is free; at `3.0` radians `fast=True` would be slower than `fast=False`. The errors are sampled over random axes, so they move in the last digit between runs. `axis_angle_to_matrix(..., fast=True)` is Rodrigues' formula rewritten with `cross_product_matrix^2 == outer_product - angle^2 * I` and `(1 - cos(angle)) / angle^2 == sinc(angle / (2*pi))^2 / 2`. The first removes a batched 3x3 matrix multiplication, which `bmm` serves poorly at that size, and folds the leftover `-angle^2 * I` into the identity term as `cos(angle) * I`; the second is defined at zero, so the `angles_sqrd == 0` special case goes away. Values are unchanged to 8.9e-16. `matrix_to_axis_angle` also used eight separate boolean mask indexes, each of which re-runs `nonzero` over the whole batch and, on CUDA, synchronizes. The cheap branch is now evaluated densely and the near-pi minority is selected with a single `nonzero`. Evaluating the near-pi branch densely too, which would remove the last `nonzero` and so every data-dependent shape, was tried and rejected: against this version it is 1.24x to 1.60x faster on CUDA at batches of 1k to 100k but 0.88x at 1M, and about 2x slower on CPU, and it does not survive `torch.jit.script` without further work. It is the version to revisit if export or `torch.compile` friendliness ever matters more than speed. Speedups, before -> after: - `matrix_to_axis_angle`: CUDA 1.88x at a batch of 1k, 1.66x at 100k, 1.47x at 1M; CPU 1.26x at 1k, 2.01x at 100k, 1.71x at 1M. - `axis_angle_to_matrix`: CUDA 1.26x at 1k, 1.81x at 100k, 5.49x at 1M; CPU unchanged. Before this change `matrix_to_axis_angle(..., fast=True)` was slower than `fast=False` on CUDA at a batch of 100k, 0.587 against 0.463 ms, because the selection synchronized. Rewriting `axis_angle_to_matrix` to compute all nine entries in one `torch.stack`, as `quaternion_to_matrix` does, was also tried and is not faster: 0.96x, 0.74x and 1.10x on CUDA at 1k, 100k and 1M, because the extra kernel launches cost about what the saved intermediates gain. Reviewed By: MichaelRamamonjisoa Differential Revision: D115714860 fbshipit-source-id: cf19695f67bf2e2e6f8719419e9f902d5c58c309
466 lines
19 KiB
Python
466 lines
19 KiB
Python
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
# All rights reserved.
|
|
#
|
|
# This source code is licensed under the BSD-style license found in the
|
|
# LICENSE file in the root directory of this source tree.
|
|
|
|
|
|
import itertools
|
|
import math
|
|
import unittest
|
|
from typing import Optional, Union
|
|
|
|
import numpy as np
|
|
import torch
|
|
from pytorch3d.transforms.rotation_conversions import (
|
|
axis_angle_to_matrix,
|
|
axis_angle_to_quaternion,
|
|
euler_angles_to_matrix,
|
|
matrix_to_axis_angle,
|
|
matrix_to_euler_angles,
|
|
matrix_to_quaternion,
|
|
matrix_to_rotation_6d,
|
|
quaternion_apply,
|
|
quaternion_multiply,
|
|
quaternion_to_axis_angle,
|
|
quaternion_to_matrix,
|
|
random_quaternions,
|
|
random_rotation,
|
|
random_rotations,
|
|
rotation_6d_to_matrix,
|
|
)
|
|
|
|
from .common_testing import TestCaseMixin
|
|
|
|
|
|
class TestRandomRotation(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
super().setUp()
|
|
torch.manual_seed(1)
|
|
|
|
def test_random_rotation_invariant(self):
|
|
"""The image of the x-axis isn't biased among quadrants."""
|
|
N = 1000
|
|
base = random_rotation()
|
|
quadrants = list(itertools.product([False, True], repeat=3))
|
|
|
|
matrices = random_rotations(N)
|
|
transformed = torch.matmul(base, matrices)
|
|
transformed2 = torch.matmul(matrices, base)
|
|
|
|
for k, results in enumerate([matrices, transformed, transformed2]):
|
|
counts = {i: 0 for i in quadrants}
|
|
for j in range(N):
|
|
counts[tuple(i.item() > 0 for i in results[j, 0])] += 1
|
|
average = N / 8.0
|
|
counts_tensor = torch.tensor(list(counts.values()))
|
|
chisquare_statistic = torch.sum(
|
|
(counts_tensor - average) * (counts_tensor - average) / average
|
|
)
|
|
# The 0.1 significance level for chisquare(8-1) is
|
|
# scipy.stats.chi2(7).ppf(0.9) == 12.017.
|
|
self.assertLess(chisquare_statistic, 12, (counts, chisquare_statistic, k))
|
|
|
|
|
|
class TestRotationConversion(TestCaseMixin, unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
super().setUp()
|
|
torch.manual_seed(1)
|
|
|
|
def test_from_quat(self):
|
|
"""quat -> mtx -> quat"""
|
|
data = random_quaternions(13, dtype=torch.float64)
|
|
mdata = matrix_to_quaternion(quaternion_to_matrix(data))
|
|
self._assert_quaternions_close(data, mdata)
|
|
|
|
def test_to_quat(self):
|
|
"""mtx -> quat -> mtx"""
|
|
data = random_rotations(13, dtype=torch.float64)
|
|
mdata = quaternion_to_matrix(matrix_to_quaternion(data))
|
|
self.assertClose(data, mdata)
|
|
|
|
def test_quat_grad_exists(self):
|
|
"""Quaternion calculations are differentiable."""
|
|
rotation = random_rotation()
|
|
rotation.requires_grad = True
|
|
modified = quaternion_to_matrix(matrix_to_quaternion(rotation))
|
|
[g] = torch.autograd.grad(modified.sum(), rotation)
|
|
self.assertTrue(torch.isfinite(g).all())
|
|
|
|
def _tait_bryan_conventions(self):
|
|
return map("".join, itertools.permutations("XYZ"))
|
|
|
|
def _proper_euler_conventions(self):
|
|
letterpairs = itertools.permutations("XYZ", 2)
|
|
return (l0 + l1 + l0 for l0, l1 in letterpairs)
|
|
|
|
def _all_euler_angle_conventions(self):
|
|
return itertools.chain(
|
|
self._tait_bryan_conventions(), self._proper_euler_conventions()
|
|
)
|
|
|
|
def test_conventions(self):
|
|
"""The conventions listings have the right length."""
|
|
all = list(self._all_euler_angle_conventions())
|
|
self.assertEqual(len(all), 12)
|
|
self.assertEqual(len(set(all)), 12)
|
|
|
|
def test_from_euler(self):
|
|
"""euler -> mtx -> euler"""
|
|
n_repetitions = 10
|
|
# tolerance is how much we keep the middle angle away from the extreme
|
|
# allowed values which make the calculation unstable (Gimbal lock).
|
|
tolerance = 0.04
|
|
half_pi = math.pi / 2
|
|
data = torch.zeros(n_repetitions, 3)
|
|
data.uniform_(-math.pi, math.pi)
|
|
|
|
data[:, 1].uniform_(-half_pi + tolerance, half_pi - tolerance)
|
|
for convention in self._tait_bryan_conventions():
|
|
matrices = euler_angles_to_matrix(data, convention)
|
|
mdata = matrix_to_euler_angles(matrices, convention)
|
|
self.assertClose(data, mdata)
|
|
|
|
data[:, 1] += half_pi
|
|
for convention in self._proper_euler_conventions():
|
|
matrices = euler_angles_to_matrix(data, convention)
|
|
mdata = matrix_to_euler_angles(matrices, convention)
|
|
self.assertClose(data, mdata)
|
|
|
|
def test_to_euler(self):
|
|
"""mtx -> euler -> mtx"""
|
|
data = random_rotations(13, dtype=torch.float64)
|
|
for convention in self._all_euler_angle_conventions():
|
|
euler_angles = matrix_to_euler_angles(data, convention)
|
|
mdata = euler_angles_to_matrix(euler_angles, convention)
|
|
self.assertClose(data, mdata)
|
|
|
|
def test_euler_grad_exists(self):
|
|
"""Euler angle calculations are differentiable."""
|
|
rotation = random_rotation(dtype=torch.float64)
|
|
rotation.requires_grad = True
|
|
for convention in self._all_euler_angle_conventions():
|
|
euler_angles = matrix_to_euler_angles(rotation, convention)
|
|
mdata = euler_angles_to_matrix(euler_angles, convention)
|
|
[g] = torch.autograd.grad(mdata.sum(), rotation)
|
|
self.assertTrue(torch.isfinite(g).all())
|
|
|
|
def test_quaternion_multiplication(self):
|
|
"""Quaternion and matrix multiplication are equivalent."""
|
|
a = random_quaternions(15, torch.float64).reshape((3, 5, 4))
|
|
b = random_quaternions(21, torch.float64).reshape((7, 3, 1, 4))
|
|
ab = quaternion_multiply(a, b)
|
|
self.assertEqual(ab.shape, (7, 3, 5, 4))
|
|
a_matrix = quaternion_to_matrix(a)
|
|
b_matrix = quaternion_to_matrix(b)
|
|
ab_matrix = torch.matmul(a_matrix, b_matrix)
|
|
ab_from_matrix = matrix_to_quaternion(ab_matrix)
|
|
self._assert_quaternions_close(ab, ab_from_matrix)
|
|
|
|
def test_matrix_to_quaternion_corner_case(self):
|
|
"""Check no bad gradients from sqrt(0)."""
|
|
matrix = torch.eye(3, requires_grad=True)
|
|
target = torch.Tensor([0.984808, 0, 0.174, 0])
|
|
|
|
optimizer = torch.optim.Adam([matrix], lr=0.05)
|
|
optimizer.zero_grad()
|
|
q = matrix_to_quaternion(matrix)
|
|
loss = torch.sum((q - target) ** 2)
|
|
loss.backward()
|
|
optimizer.step()
|
|
|
|
self.assertClose(matrix, matrix, msg="Result has non-finite values")
|
|
delta = 1e-2
|
|
self.assertLess(
|
|
matrix.trace(),
|
|
3.0 - delta,
|
|
msg="Identity initialisation unchanged by a gradient step",
|
|
)
|
|
|
|
def test_matrix_to_quaternion_by_pi(self):
|
|
# We check that rotations by pi around each of the 26
|
|
# nonzero vectors containing nothing but 0, 1 and -1
|
|
# are mapped to the right quaternions.
|
|
# This is representative across the directions.
|
|
options = [0.0, -1.0, 1.0]
|
|
axes = [
|
|
torch.tensor(vec)
|
|
for vec in itertools.islice( # exclude [0, 0, 0]
|
|
itertools.product(options, options, options), 1, None
|
|
)
|
|
]
|
|
|
|
axes = torch.nn.functional.normalize(torch.stack(axes), dim=-1)
|
|
# Rotation by pi around unit vector x is given by
|
|
# the matrix 2 x x^T - Id.
|
|
R = 2 * torch.matmul(axes[..., None], axes[..., None, :]) - torch.eye(3)
|
|
quats_hat = matrix_to_quaternion(R)
|
|
R_hat = quaternion_to_matrix(quats_hat)
|
|
self.assertClose(R, R_hat, atol=1e-3)
|
|
|
|
def test_from_axis_angle(self):
|
|
"""axis_angle -> mtx -> axis_angle"""
|
|
n_repetitions = 20
|
|
data = torch.rand(n_repetitions, 3)
|
|
matrices = axis_angle_to_matrix(data)
|
|
self.assertClose(data, matrix_to_axis_angle(matrices), atol=2e-6)
|
|
self.assertClose(data, matrix_to_axis_angle(matrices, fast=True), atol=2e-6)
|
|
matrices = axis_angle_to_matrix(data, fast=True)
|
|
mdata = matrix_to_axis_angle(matrices)
|
|
self.assertClose(data, mdata, atol=2e-6)
|
|
|
|
def test_from_axis_angle_has_grad(self):
|
|
n_repetitions = 20
|
|
data = torch.rand(n_repetitions, 3, requires_grad=True)
|
|
matrices = axis_angle_to_matrix(data)
|
|
mdata = matrix_to_axis_angle(matrices)
|
|
quats = axis_angle_to_quaternion(data)
|
|
mdata2 = quaternion_to_axis_angle(quats)
|
|
(grad,) = torch.autograd.grad(mdata.sum() + mdata2.sum(), data)
|
|
self.assertTrue(torch.isfinite(grad).all())
|
|
|
|
def test_to_axis_angle(self):
|
|
"""mtx -> axis_angle -> mtx"""
|
|
data = random_rotations(13, dtype=torch.float64)
|
|
euler_angles = matrix_to_axis_angle(data)
|
|
euler_angles_fast = matrix_to_axis_angle(data)
|
|
self.assertClose(data, axis_angle_to_matrix(euler_angles))
|
|
self.assertClose(data, axis_angle_to_matrix(euler_angles_fast))
|
|
self.assertClose(data, axis_angle_to_matrix(euler_angles, fast=True))
|
|
|
|
def test_axis_angle_by_pi(self):
|
|
"""Rotations by exactly pi are recovered, and don't depend on the batch."""
|
|
options = [0.0, -1.0, 1.0]
|
|
axes = torch.nn.functional.normalize(
|
|
torch.stack(
|
|
[
|
|
torch.tensor(vec, dtype=torch.float64)
|
|
for vec in itertools.islice( # exclude [0, 0, 0]
|
|
itertools.product(options, options, options), 1, None
|
|
)
|
|
]
|
|
),
|
|
dim=-1,
|
|
)
|
|
# Rotation by pi around unit vector x is given by 2 x x^T - Id.
|
|
R = 2 * torch.matmul(axes[..., None], axes[..., None, :]) - torch.eye(
|
|
3, dtype=torch.float64
|
|
)
|
|
for fast in [False, True]:
|
|
axis_angles = matrix_to_axis_angle(R, fast=fast)
|
|
self.assertClose(
|
|
axis_angles.norm(dim=-1),
|
|
torch.full(axes.shape[:1], math.pi, dtype=torch.float64),
|
|
)
|
|
self.assertClose(axis_angle_to_matrix(axis_angles), R)
|
|
# The axis of one rotation cannot depend on the others.
|
|
singly = torch.stack(
|
|
[matrix_to_axis_angle(r, fast=fast) for r in R.unbind()]
|
|
)
|
|
self.assertClose(singly, axis_angles)
|
|
|
|
def test_axis_angle_near_pi(self):
|
|
"""The fast path stays accurate as the angle approaches pi."""
|
|
for dtype, atol in [(torch.float64, 1e-9), (torch.float32, 1e-4)]:
|
|
axes = torch.nn.functional.normalize(
|
|
torch.randn(50, 3, dtype=dtype), dim=-1
|
|
)
|
|
for angle in [
|
|
math.pi,
|
|
math.pi - 1e-7,
|
|
math.pi - 1e-5,
|
|
math.pi - 1e-3,
|
|
math.pi - 1e-2,
|
|
math.pi - 0.1,
|
|
2.0,
|
|
]:
|
|
data = axes * angle
|
|
R = axis_angle_to_matrix(data)
|
|
axis_angles = matrix_to_axis_angle(R, fast=True)
|
|
self.assertClose(axis_angle_to_matrix(axis_angles), R, atol=atol)
|
|
self.assertClose(
|
|
axis_angles.norm(dim=-1),
|
|
torch.full(axes.shape[:1], angle, dtype=dtype),
|
|
atol=atol,
|
|
)
|
|
if angle < math.pi - 1e-2:
|
|
# Closer to pi than this, the sign of the axis is not
|
|
# determined to float32 precision (and both signs describe
|
|
# the same rotation to within the tolerance above).
|
|
self.assertClose(axis_angles, data, atol=atol)
|
|
|
|
def test_axis_angle_to_matrix_fast(self):
|
|
"""The Rodrigues implementation agrees with the quaternion one."""
|
|
data = torch.randn(100, 3, dtype=torch.float64)
|
|
normalized = torch.nn.functional.normalize(data, dim=-1)
|
|
data[:5] = 0.0
|
|
data[5:10] = normalized[5:10] * math.pi
|
|
data[10:15] = normalized[10:15] * 1e-12
|
|
self.assertClose(
|
|
axis_angle_to_matrix(data, fast=True),
|
|
axis_angle_to_matrix(data),
|
|
atol=1e-14,
|
|
)
|
|
|
|
# A zero angle is not a special case, but must still have gradients.
|
|
zeros = torch.zeros(1, 3, dtype=torch.float64, requires_grad=True)
|
|
matrices = axis_angle_to_matrix(zeros, fast=True)
|
|
self.assertClose(matrices, torch.eye(3, dtype=torch.float64)[None])
|
|
(grad,) = torch.autograd.grad(matrices.sum(), zeros)
|
|
self.assertTrue(torch.isfinite(grad).all())
|
|
|
|
def test_axis_angle_tiny(self):
|
|
"""Tiny angles are preserved, not flattened to zero, and have grads."""
|
|
axis = torch.nn.functional.normalize(
|
|
torch.tensor([[0.3, -0.5, 0.81]], dtype=torch.float64), dim=-1
|
|
)
|
|
for angle in [1e-3, 1e-8, 1e-9, 1e-12]:
|
|
data = axis * angle
|
|
R = axis_angle_to_matrix(data)
|
|
for fast in [False, True]:
|
|
# atol must be 0 here: the default would accept zero.
|
|
self.assertClose(matrix_to_axis_angle(R, fast=fast), data, atol=0)
|
|
|
|
identity = torch.eye(3, dtype=torch.float64)
|
|
for fast in [False, True]:
|
|
R = identity.clone().requires_grad_(True)
|
|
axis_angles = matrix_to_axis_angle(R, fast=fast)
|
|
self.assertClose(axis_angles, torch.zeros(3, dtype=torch.float64))
|
|
(grad,) = torch.autograd.grad(axis_angles.sum(), R)
|
|
self.assertTrue(torch.isfinite(grad).all())
|
|
|
|
def test_axis_angle_batch_independence(self):
|
|
"""Every matrix in a batch is converted independently of the others."""
|
|
angles = torch.tensor(
|
|
[math.pi, math.pi, math.pi - 1e-9, math.pi - 1e-3, 3.0, 1.0, 0.5, 0.0],
|
|
dtype=torch.float64,
|
|
)
|
|
axes = torch.nn.functional.normalize(
|
|
torch.randn(angles.shape[0], 3, dtype=torch.float64), dim=-1
|
|
)
|
|
R = axis_angle_to_matrix(axes * angles[:, None])
|
|
for fast in [False, True]:
|
|
batched = matrix_to_axis_angle(R, fast=fast)
|
|
singly = torch.stack(
|
|
[matrix_to_axis_angle(r, fast=fast) for r in R.unbind()]
|
|
)
|
|
self.assertClose(batched, singly)
|
|
|
|
def test_axis_angle_shapes(self):
|
|
"""Arbitrary leading dimensions, including none and zero."""
|
|
R = random_rotations(24, dtype=torch.float64).reshape(2, 3, 4, 3, 3)
|
|
# A rotation by pi, so that the near-pi branch is used too.
|
|
axis = torch.tensor([0.0, 0.6, -0.8], dtype=torch.float64)
|
|
R[1, 2, 0] = 2 * torch.outer(axis, axis) - torch.eye(3, dtype=torch.float64)
|
|
for fast in [False, True]:
|
|
axis_angles = matrix_to_axis_angle(R, fast=fast)
|
|
self.assertEqual(axis_angles.shape, (2, 3, 4, 3))
|
|
self.assertClose(axis_angle_to_matrix(axis_angles), R)
|
|
|
|
self.assertEqual(matrix_to_axis_angle(R[0, 0, 0], fast=fast).shape, (3,))
|
|
empty = torch.zeros(0, 3, 3, dtype=torch.float64)
|
|
self.assertEqual(matrix_to_axis_angle(empty, fast=fast).shape, (0, 3))
|
|
|
|
def test_axis_angle_near_pi_grad(self):
|
|
"""Round tripping is the identity, so its derivative is too."""
|
|
axis = torch.nn.functional.normalize(
|
|
torch.tensor([0.3, -0.5, 0.81], dtype=torch.float64), dim=-1
|
|
)
|
|
eye = torch.eye(3, dtype=torch.float64)
|
|
for angle in [0.5, 2.0, math.pi - 0.02, math.pi - 1e-4]:
|
|
data = axis * angle
|
|
for fast in [False, True]:
|
|
jacobian = torch.autograd.functional.jacobian(
|
|
lambda x, fast=fast: matrix_to_axis_angle(
|
|
axis_angle_to_matrix(x), fast=fast
|
|
),
|
|
data,
|
|
)
|
|
self.assertClose(jacobian, eye, atol=1e-8)
|
|
|
|
# At exactly pi the axis, and so the derivative, is ambiguous, but the
|
|
# result must still be usable.
|
|
R = (2 * torch.outer(axis, axis) - eye).requires_grad_(True)
|
|
for fast in [False, True]:
|
|
(grad,) = torch.autograd.grad(
|
|
matrix_to_axis_angle(R, fast=fast).sum(), R, retain_graph=True
|
|
)
|
|
self.assertTrue(torch.isfinite(grad).all())
|
|
|
|
def test_quaternion_application(self):
|
|
"""Applying a quaternion is the same as applying the matrix."""
|
|
quaternions = random_quaternions(3, torch.float64)
|
|
quaternions.requires_grad = True
|
|
matrices = quaternion_to_matrix(quaternions)
|
|
points = torch.randn(3, 3, dtype=torch.float64, requires_grad=True)
|
|
transform1 = quaternion_apply(quaternions, points)
|
|
transform2 = torch.matmul(matrices, points[..., None])[..., 0]
|
|
self.assertClose(transform1, transform2)
|
|
|
|
[p, q] = torch.autograd.grad(transform1.sum(), [points, quaternions])
|
|
self.assertTrue(torch.isfinite(p).all())
|
|
self.assertTrue(torch.isfinite(q).all())
|
|
|
|
def test_6d(self):
|
|
"""Converting to 6d and back"""
|
|
r = random_rotations(13, dtype=torch.float64)
|
|
|
|
# 6D representation is not unique,
|
|
# but we implement it by taking the first two rows of the matrix
|
|
r6d = matrix_to_rotation_6d(r)
|
|
self.assertClose(r6d, r[:, :2, :].reshape(-1, 6))
|
|
|
|
# going to 6D and back should not change the matrix
|
|
r_hat = rotation_6d_to_matrix(r6d)
|
|
self.assertClose(r_hat, r)
|
|
|
|
# moving the second row R2 in the span of (R1, R2) should not matter
|
|
r6d[:, 3:] += 2 * r6d[:, :3]
|
|
r6d[:, :3] *= 3.0
|
|
r_hat = rotation_6d_to_matrix(r6d)
|
|
self.assertClose(r_hat, r)
|
|
|
|
# check that we map anything to a valid rotation
|
|
r6d = torch.rand(13, 6)
|
|
r6d[:4, :] *= 3.0
|
|
r6d[4:8, :] -= 0.5
|
|
r = rotation_6d_to_matrix(r6d)
|
|
self.assertClose(
|
|
torch.matmul(r, r.permute(0, 2, 1)), torch.eye(3).expand_as(r), atol=1e-6
|
|
)
|
|
|
|
def test_scriptable(self):
|
|
torch.jit.script(axis_angle_to_matrix)
|
|
torch.jit.script(axis_angle_to_quaternion)
|
|
torch.jit.script(euler_angles_to_matrix)
|
|
torch.jit.script(matrix_to_axis_angle)
|
|
torch.jit.script(matrix_to_euler_angles)
|
|
torch.jit.script(matrix_to_quaternion)
|
|
torch.jit.script(matrix_to_rotation_6d)
|
|
torch.jit.script(quaternion_apply)
|
|
torch.jit.script(quaternion_multiply)
|
|
torch.jit.script(quaternion_to_matrix)
|
|
torch.jit.script(quaternion_to_axis_angle)
|
|
torch.jit.script(random_quaternions)
|
|
torch.jit.script(random_rotation)
|
|
torch.jit.script(random_rotations)
|
|
torch.jit.script(random_quaternions)
|
|
torch.jit.script(rotation_6d_to_matrix)
|
|
|
|
def _assert_quaternions_close(
|
|
self,
|
|
input: Union[torch.Tensor, np.ndarray],
|
|
other: Union[torch.Tensor, np.ndarray],
|
|
*,
|
|
rtol: float = 1e-05,
|
|
atol: float = 1e-08,
|
|
equal_nan: bool = False,
|
|
msg: Optional[str] = None,
|
|
):
|
|
self.assertEqual(np.shape(input), np.shape(other))
|
|
dot = (input * other).sum(-1)
|
|
ones = torch.ones_like(dot)
|
|
self.assertClose(
|
|
dot.abs(), ones, rtol=rtol, atol=atol, equal_nan=equal_nan, msg=msg
|
|
)
|