mirror of
https://github.com/facebookresearch/pytorch3d.git
synced 2026-10-25 07:29:59 +08:00
Compare commits
2 Commits
3143b3baf8
...
2bce7110d5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bce7110d5 | ||
|
|
a5093b158b |
@@ -34,6 +34,18 @@ std::tuple<at::Tensor, at::Tensor, at::Tensor> MarchingCubesCpu(
|
|||||||
const int H = vol.size(1);
|
const int H = vol.size(1);
|
||||||
const int W = vol.size(2);
|
const int W = vol.size(2);
|
||||||
|
|
||||||
|
// The edge-id hash packs two grid-point ids as v1_id * (W*H*D) + v2_id, whose
|
||||||
|
// maximum value is (W*H*D)^2 - 1. Guard against signed int64 overflow so an
|
||||||
|
// oversized volume fails loudly instead of silently producing colliding edge
|
||||||
|
// ids and a corrupted mesh. floor(sqrt(INT64_MAX)) == 3037000499 (~1448^3).
|
||||||
|
const int64_t num_grid_points = (int64_t)W * H * D;
|
||||||
|
TORCH_CHECK(
|
||||||
|
num_grid_points <= 3037000499LL,
|
||||||
|
"Volume too large for CPU marching cubes: W*H*D (",
|
||||||
|
num_grid_points,
|
||||||
|
") exceeds 3037000499 (~1448^3), which would overflow the int64 edge-id "
|
||||||
|
"hash and corrupt the mesh.");
|
||||||
|
|
||||||
// Create tensor accessors
|
// Create tensor accessors
|
||||||
auto vol_a = vol.accessor<float, 3>();
|
auto vol_a = vol.accessor<float, 3>();
|
||||||
// edge_id_to_v maps from an edge id to a vertex position
|
// edge_id_to_v maps from an edge id to a vertex position
|
||||||
@@ -66,10 +78,11 @@ std::tuple<at::Tensor, at::Tensor, at::Tensor> MarchingCubesCpu(
|
|||||||
tri.push_back(edge);
|
tri.push_back(edge);
|
||||||
ps.push_back(interp_points[e]);
|
ps.push_back(interp_points[e]);
|
||||||
|
|
||||||
|
if (ps.size() == 3) {
|
||||||
// Check if the triangle face is degenerate. A triangle face
|
// Check if the triangle face is degenerate. A triangle face
|
||||||
// is degenerate if any of the two verices share the same 3D position
|
// is degenerate if any of the two vertices share the same 3D
|
||||||
if ((j + 1) % 3 == 0 && ps[0] != ps[1] && ps[1] != ps[2] &&
|
// position
|
||||||
ps[2] != ps[0]) {
|
if (ps[0] != ps[1] && ps[1] != ps[2] && ps[2] != ps[0]) {
|
||||||
for (int k = 0; k < 3; k++) {
|
for (int k = 0; k < 3; k++) {
|
||||||
int64_t v = tri.at(k);
|
int64_t v = tri.at(k);
|
||||||
edge_id_to_v[v] = ps.at(k);
|
edge_id_to_v[v] = ps.at(k);
|
||||||
@@ -79,6 +92,9 @@ std::tuple<at::Tensor, at::Tensor, at::Tensor> MarchingCubesCpu(
|
|||||||
}
|
}
|
||||||
faces.push_back(uniq_edge_id[v]);
|
faces.push_back(uniq_edge_id[v]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Clear unconditionally - a rejected degenerate triangle must not
|
||||||
|
// corrupt the buffer for the next triangle in this cube.
|
||||||
tri.clear();
|
tri.clear();
|
||||||
ps.clear();
|
ps.clear();
|
||||||
} // endif
|
} // endif
|
||||||
|
|||||||
@@ -126,8 +126,7 @@ struct Cube {
|
|||||||
// edge with an integer to address floating point precision issue.
|
// edge with an integer to address floating point precision issue.
|
||||||
//
|
//
|
||||||
// Args:
|
// Args:
|
||||||
// v1_id: global id of vertex 1
|
// edge: edge (ID) whose two endpoint vertices are hashed
|
||||||
// v2_id: global id of vertex 2
|
|
||||||
// W: width of the 3d grid
|
// W: width of the 3d grid
|
||||||
// H: height of the 3d grid
|
// H: height of the 3d grid
|
||||||
// D: depth of the 3d grid
|
// D: depth of the 3d grid
|
||||||
@@ -135,11 +134,19 @@ struct Cube {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// hashing for a pair of vertex ids
|
// hashing for a pair of vertex ids
|
||||||
//
|
//
|
||||||
int64_t HashVpair(const int edge, int W, int H, int D) {
|
int64_t HashVpair(const int edge, int64_t W, int64_t H, int64_t D) const {
|
||||||
const int v1 = _EDGE_TO_VERTICES[edge][0];
|
const int v1 = _EDGE_TO_VERTICES[edge][0];
|
||||||
const int v2 = _EDGE_TO_VERTICES[edge][1];
|
const int v2 = _EDGE_TO_VERTICES[edge][1];
|
||||||
const int v1_id = p[v1].x + p[v1].y * W + p[v1].z * W * H;
|
// p[v].x/y/z hold integral corner coordinates, so casting to int64_t is
|
||||||
const int v2_id = p[v2].x + p[v2].y * W + p[v2].z * W * H;
|
// exact. We cast before multiplication to avoid float32 precision loss at
|
||||||
return (int64_t)v1_id * (W + W * H + W * H * D) + (int64_t)v2_id;
|
// grids > 256^3.
|
||||||
|
const int64_t v1_id =
|
||||||
|
(int64_t)p[v1].x + (int64_t)p[v1].y * W + (int64_t)p[v1].z * W * H;
|
||||||
|
const int64_t v2_id =
|
||||||
|
(int64_t)p[v2].x + (int64_t)p[v2].y * W + (int64_t)p[v2].z * W * H;
|
||||||
|
// v_id max is (W*H*D - 1). A stride of W*H*D perfectly separates v1 and v2.
|
||||||
|
// Note: This hash fits in int64_t safely up to grids of ~1448^3.
|
||||||
|
const int64_t stride = W * H * D;
|
||||||
|
return v1_id * stride + v2_id;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -159,7 +159,6 @@ def marching_cubes_naive(
|
|||||||
"""
|
"""
|
||||||
batched_verts, batched_faces = [], []
|
batched_verts, batched_faces = [], []
|
||||||
D, H, W = vol_batch.shape[1:]
|
D, H, W = vol_batch.shape[1:]
|
||||||
|
|
||||||
# each edge is represented with its two endpoints (represented with global id)
|
# each edge is represented with its two endpoints (represented with global id)
|
||||||
for i in range(len(vol_batch)):
|
for i in range(len(vol_batch)):
|
||||||
vol = vol_batch[i]
|
vol = vol_batch[i]
|
||||||
@@ -184,7 +183,7 @@ def marching_cubes_naive(
|
|||||||
# triangle vertex IDs and positions
|
# triangle vertex IDs and positions
|
||||||
tri = []
|
tri = []
|
||||||
ps = []
|
ps = []
|
||||||
for i, edge in enumerate(edge_indices):
|
for edge in edge_indices:
|
||||||
interp_points[edge] = cube.vert_interp(thresh, edge, vol)
|
interp_points[edge] = cube.vert_interp(thresh, edge, vol)
|
||||||
|
|
||||||
# Bind interpolated vertex with a global edge_id, which
|
# Bind interpolated vertex with a global edge_id, which
|
||||||
@@ -196,20 +195,19 @@ def marching_cubes_naive(
|
|||||||
)
|
)
|
||||||
tri.append(edge_id)
|
tri.append(edge_id)
|
||||||
ps.append(interp_points[edge])
|
ps.append(interp_points[edge])
|
||||||
# when the isolevel are the same as the edge endpoints, the interploated
|
if len(ps) == 3:
|
||||||
# vertices can share the same values, and lead to degenerate triangles.
|
# when the isolevel are the same as the edge
|
||||||
if (
|
# endpoints, the interpolated vertices can share
|
||||||
(i + 1) % 3 == 0
|
# the same values, and lead to degenerate
|
||||||
and ps[0] != ps[1]
|
# triangles.
|
||||||
and ps[1] != ps[2]
|
if ps[0] != ps[1] and ps[1] != ps[2] and ps[2] != ps[0]:
|
||||||
and ps[2] != ps[0]
|
|
||||||
):
|
|
||||||
for j, edge_id in enumerate(tri):
|
for j, edge_id in enumerate(tri):
|
||||||
edge_id_to_v[edge_id] = ps[j]
|
edge_id_to_v[edge_id] = ps[j]
|
||||||
if edge_id not in uniq_edge_id:
|
if edge_id not in uniq_edge_id:
|
||||||
uniq_edge_id[edge_id] = len(verts)
|
uniq_edge_id[edge_id] = len(verts)
|
||||||
verts.append(edge_id_to_v[edge_id])
|
verts.append(edge_id_to_v[edge_id])
|
||||||
faces.append([uniq_edge_id[tri[j]] for j in range(3)])
|
faces.append([uniq_edge_id[tri[j]] for j in range(3)])
|
||||||
|
|
||||||
tri = []
|
tri = []
|
||||||
ps = []
|
ps = []
|
||||||
|
|
||||||
|
|||||||
@@ -489,15 +489,18 @@ def axis_angle_to_matrix(axis_angle: torch.Tensor, fast: bool = False) -> torch.
|
|||||||
cross_product_matrix = torch.stack(
|
cross_product_matrix = torch.stack(
|
||||||
[zeros, -rz, ry, rz, zeros, -rx, -ry, rx, zeros], dim=-1
|
[zeros, -rz, ry, rz, zeros, -rx, -ry, rx, zeros], dim=-1
|
||||||
).view(shape + (3,))
|
).view(shape + (3,))
|
||||||
cross_product_matrix_sqrd = cross_product_matrix @ cross_product_matrix
|
outer_product = axis_angle.unsqueeze(-1) * axis_angle.unsqueeze(-2)
|
||||||
|
|
||||||
|
# This is the Rodrigues formula written with the identities
|
||||||
|
# cross_product_matrix^2 == outer_product - angle^2 * I
|
||||||
|
# (1 - cos(angle)) / angle^2 == sinc(angle / (2*pi))^2 / 2
|
||||||
|
# which save a matrix multiplication and, as sinc is defined at zero,
|
||||||
|
# remove the need to special-case a zero angle.
|
||||||
identity = torch.eye(3, dtype=dtype, device=device)
|
identity = torch.eye(3, dtype=dtype, device=device)
|
||||||
angles_sqrd = angles * angles
|
|
||||||
angles_sqrd = torch.where(angles_sqrd == 0, 1, angles_sqrd)
|
|
||||||
return (
|
return (
|
||||||
identity.expand(cross_product_matrix.shape)
|
torch.cos(angles) * identity
|
||||||
+ torch.sinc(angles / torch.pi) * cross_product_matrix
|
+ torch.sinc(angles / torch.pi) * cross_product_matrix
|
||||||
+ ((1 - torch.cos(angles)) / angles_sqrd) * cross_product_matrix_sqrd
|
+ 0.5 * torch.sinc(angles / (2 * torch.pi)) ** 2 * outer_product
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -536,24 +539,52 @@ def matrix_to_axis_angle(matrix: torch.Tensor, fast: bool = False) -> torch.Tens
|
|||||||
traces = torch.diagonal(matrix, dim1=-2, dim2=-1).sum(-1).unsqueeze(-1)
|
traces = torch.diagonal(matrix, dim1=-2, dim2=-1).sum(-1).unsqueeze(-1)
|
||||||
angles = torch.atan2(norms, traces - 1)
|
angles = torch.atan2(norms, traces - 1)
|
||||||
|
|
||||||
zeros = torch.zeros(3, dtype=matrix.dtype, device=matrix.device)
|
# omegas is 2*sin(angle) times the axis, so the relative precision of the
|
||||||
omegas = torch.where(torch.isclose(angles, torch.zeros_like(angles)), zeros, omegas)
|
# axis it gives decays like 1/(pi - angle). Above the following angle the
|
||||||
|
# axis is taken from the symmetric part of the matrix instead. Switching
|
||||||
|
# over 0.01 radians before pi keeps the error close to that of the slow
|
||||||
|
# implementation, while leaving under 1% of uniformly random rotations on
|
||||||
|
# the more expensive branch.
|
||||||
|
near_pi = angles > torch.pi - 1e-2
|
||||||
|
|
||||||
near_pi = angles.isclose(angles.new_full((1,), torch.pi)).squeeze(-1)
|
# An angle of zero needs no special case: torch.sinc(0) is 1, and omegas
|
||||||
|
# is then zero, which is the right answer. torch.sinc is instead zero at
|
||||||
|
# an angle of pi, so those entries are given a harmless denominator here
|
||||||
|
# and overwritten below.
|
||||||
|
sincs = torch.sinc(angles / torch.pi)
|
||||||
|
axis_angles = omegas * (0.5 / torch.where(near_pi, 1.0, sincs))
|
||||||
|
|
||||||
axis_angles = torch.empty_like(omegas)
|
# Rotations by nearly pi are a small minority, so it is worth selecting
|
||||||
axis_angles[~near_pi] = (
|
# them rather than evaluating the following densely. The selection index is
|
||||||
0.5 * omegas[~near_pi] / torch.sinc(angles[~near_pi] / torch.pi)
|
# computed once, because each boolean mask index would recompute it (and,
|
||||||
|
# on the GPU, synchronize).
|
||||||
|
flat_axis_angles = axis_angles.reshape(-1, 3)
|
||||||
|
index = near_pi.reshape(-1).nonzero().squeeze(-1)
|
||||||
|
near_pi_matrix = matrix.reshape(-1, 3, 3).index_select(0, index)
|
||||||
|
near_pi_omegas = omegas.reshape(-1, 3).index_select(0, index)
|
||||||
|
|
||||||
|
# this derives from: R + R^T - 2*cos(angle)*I = 2*(1 - cos(angle))*nnT,
|
||||||
|
# together with trace(R) - 1 == 2*cos(angle)
|
||||||
|
double_cosines = traces.reshape(-1, 1, 1).index_select(0, index) - 1
|
||||||
|
nnT = (
|
||||||
|
near_pi_matrix
|
||||||
|
+ near_pi_matrix.transpose(-1, -2)
|
||||||
|
- double_cosines * torch.eye(3, dtype=matrix.dtype, device=matrix.device)
|
||||||
)
|
)
|
||||||
|
# Every column of nnT is a multiple of n. Taking the one whose diagonal
|
||||||
|
# entry is largest means the multiplier has absolute value at least
|
||||||
|
# 1/sqrt(3), so the column is never degenerate.
|
||||||
|
largest = torch.diagonal(nnT, dim1=-2, dim2=-1).argmax(dim=-1)
|
||||||
|
n = nnT.gather(-1, largest.reshape(-1, 1, 1).expand(-1, 3, 1)).squeeze(-1)
|
||||||
|
n = n / torch.linalg.vector_norm(n, dim=-1, keepdim=True)
|
||||||
|
# nnT determines n only up to sign, which omegas resolves. (At exactly pi
|
||||||
|
# the sign is arbitrary, and there omegas is zero.)
|
||||||
|
n = torch.where((n * near_pi_omegas).sum(-1, keepdim=True) < 0, -n, n)
|
||||||
|
|
||||||
# this derives from: nnT = (R + 1) / 2
|
near_pi_angles = angles.reshape(-1, 1).index_select(0, index)
|
||||||
n = 0.5 * (
|
return torch.index_copy(flat_axis_angles, 0, index, near_pi_angles * n).reshape(
|
||||||
matrix[near_pi][..., 0, :]
|
axis_angles.shape
|
||||||
+ torch.eye(1, 3, dtype=matrix.dtype, device=matrix.device)
|
|
||||||
)
|
)
|
||||||
axis_angles[near_pi] = angles[near_pi] * n / torch.norm(n)
|
|
||||||
|
|
||||||
return axis_angles
|
|
||||||
|
|
||||||
|
|
||||||
def axis_angle_to_quaternion(axis_angle: torch.Tensor) -> torch.Tensor:
|
def axis_angle_to_quaternion(axis_angle: torch.Tensor) -> torch.Tensor:
|
||||||
|
|||||||
@@ -780,6 +780,79 @@ class TestMarchingCubes(TestCaseMixin, unittest.TestCase):
|
|||||||
self.assertClose(faces[0], expected_faces)
|
self.assertClose(faces[0], expected_faces)
|
||||||
self.assertTrue(verts[0].ge(-1).all() and verts[0].le(1).all())
|
self.assertTrue(verts[0].ge(-1).all() and verts[0].le(1).all())
|
||||||
|
|
||||||
|
def test_degenerate_triangle_keeps_later_faces(self):
|
||||||
|
# A cube whose first triangle is degenerate must not suppress the
|
||||||
|
# remaining triangles of the same cube. Here the isolevel coincides
|
||||||
|
# with the value of the outside corners, so every interpolated point
|
||||||
|
# snaps onto a corner and the cube's 4 candidate triangles collapse
|
||||||
|
# onto the 4 outside corners: triangles 1 and 4 become degenerate
|
||||||
|
# while triangles 2 and 3 stay valid.
|
||||||
|
volume_data = torch.ones(1, 2, 2, 2) # (B, W, H, D)
|
||||||
|
volume_data[0, 1, 0, 0] = 0
|
||||||
|
volume_data[0, 1, 0, 1] = 0
|
||||||
|
volume_data[0, 0, 1, 1] = 0
|
||||||
|
volume_data[0, 1, 1, 1] = 0
|
||||||
|
volume_data = volume_data.permute(0, 3, 2, 1) # (B, D, H, W)
|
||||||
|
|
||||||
|
# The four inside corners are separated from the four outside corners
|
||||||
|
# (1, 1, 0), (0, 0, 1), (0, 1, 0) and (0, 0, 0), which the surface
|
||||||
|
# passes exactly through, giving a quad made of two triangles.
|
||||||
|
expected_verts = torch.tensor(
|
||||||
|
[
|
||||||
|
[1.0, 1.0, 0.0],
|
||||||
|
[0.0, 0.0, 1.0],
|
||||||
|
[0.0, 1.0, 0.0],
|
||||||
|
[0.0, 0.0, 0.0],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
expected_faces = torch.tensor([[0, 1, 2], [0, 3, 1]])
|
||||||
|
|
||||||
|
verts, faces = marching_cubes_naive(volume_data, 1, return_local_coords=False)
|
||||||
|
self.assertClose(verts[0], expected_verts)
|
||||||
|
self.assertClose(faces[0], expected_faces)
|
||||||
|
|
||||||
|
verts, faces = marching_cubes(volume_data, 1, return_local_coords=False)
|
||||||
|
self.assertClose(verts[0], expected_verts)
|
||||||
|
self.assertClose(faces[0], expected_faces)
|
||||||
|
|
||||||
|
def test_large_grid_edge_ids(self):
|
||||||
|
# The C++ implementation identifies a vertex by hashing the pair of
|
||||||
|
# grid-point ids of the edge it lies on. Grid-point ids run up to
|
||||||
|
# W * H * D - 1, so once the grid exceeds 2 ** 24 points the ids must
|
||||||
|
# not be computed in float32 or distinct edges collide and their
|
||||||
|
# vertices get merged. Use a long, thin volume to cross that bound
|
||||||
|
# with a volume small enough to allocate (~67MB).
|
||||||
|
W, H, D = 4_200_000, 2, 2
|
||||||
|
# Isolated points on the (y=1, z=1) row, whose ids are x + W + W * H
|
||||||
|
# and therefore the largest in the grid. Spacing 3 keeps the blobs
|
||||||
|
# from sharing a cube while cycling through every id residue.
|
||||||
|
first_x = 2**24 - W * H - W
|
||||||
|
xs = [first_x + 3 * i for i in range(16)]
|
||||||
|
self.assertLess(xs[-1] + 1, W)
|
||||||
|
|
||||||
|
vol = torch.ones(1, D, H, W)
|
||||||
|
for x in xs:
|
||||||
|
vol[0, 1, 1, x] = 0.0
|
||||||
|
verts, faces = marching_cubes(vol, 0.5, return_local_coords=False)
|
||||||
|
|
||||||
|
# Each isolated point cuts the four grid edges leading away from it,
|
||||||
|
# so every blob contributes 4 distinct vertices and 2 faces.
|
||||||
|
expected_verts = set()
|
||||||
|
for x in xs:
|
||||||
|
expected_verts.update(
|
||||||
|
[
|
||||||
|
(x - 0.5, 1.0, 1.0),
|
||||||
|
(x + 0.5, 1.0, 1.0),
|
||||||
|
(float(x), 0.5, 1.0),
|
||||||
|
(float(x), 1.0, 0.5),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
{tuple(v) for v in verts[0].tolist()},
|
||||||
|
expected_verts,
|
||||||
|
)
|
||||||
|
self.assertEqual(faces[0].shape[0], 2 * len(xs))
|
||||||
|
|
||||||
def test_sphere(self):
|
def test_sphere(self):
|
||||||
# (B, W, H, D)
|
# (B, W, H, D)
|
||||||
volume = torch.Tensor(
|
volume = torch.Tensor(
|
||||||
|
|||||||
@@ -228,6 +228,165 @@ class TestRotationConversion(TestCaseMixin, unittest.TestCase):
|
|||||||
self.assertClose(data, axis_angle_to_matrix(euler_angles_fast))
|
self.assertClose(data, axis_angle_to_matrix(euler_angles_fast))
|
||||||
self.assertClose(data, axis_angle_to_matrix(euler_angles, fast=True))
|
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):
|
def test_quaternion_application(self):
|
||||||
"""Applying a quaternion is the same as applying the matrix."""
|
"""Applying a quaternion is the same as applying the matrix."""
|
||||||
quaternions = random_quaternions(3, torch.float64)
|
quaternions = random_quaternions(3, torch.float64)
|
||||||
|
|||||||
Reference in New Issue
Block a user