| |
| |
| |
| |
| |
|
|
|
|
| import unittest |
|
|
| import torch |
| from pytorch3d.loss.mesh_laplacian_smoothing import mesh_laplacian_smoothing |
| from pytorch3d.structures.meshes import Meshes |
|
|
|
|
| class TestLaplacianSmoothing(unittest.TestCase): |
| @staticmethod |
| def laplacian_smoothing_naive_uniform(meshes): |
| """ |
| Naive implementation of laplacian smoothing with uniform weights. |
| """ |
| verts_packed = meshes.verts_packed() |
| faces_packed = meshes.faces_packed() |
| V = verts_packed.shape[0] |
|
|
| L = torch.zeros((V, V), dtype=torch.float32, device=meshes.device) |
|
|
| |
| for f in faces_packed: |
| L[f[0], f[1]] = 1 |
| L[f[0], f[2]] = 1 |
| L[f[1], f[2]] = 1 |
| |
| L[f[1], f[0]] = 1 |
| L[f[2], f[0]] = 1 |
| L[f[2], f[1]] = 1 |
|
|
| norm_w = L.sum(dim=1, keepdims=True) |
| idx = norm_w > 0 |
| norm_w[idx] = 1.0 / norm_w[idx] |
|
|
| loss = (L.mm(verts_packed) * norm_w - verts_packed).norm(dim=1) |
|
|
| weights = torch.zeros(V, dtype=torch.float32, device=meshes.device) |
| for v in range(V): |
| weights[v] = meshes.num_verts_per_mesh()[ |
| meshes.verts_packed_to_mesh_idx()[v] |
| ] |
| weights = 1.0 / weights |
| loss = loss * weights |
|
|
| return loss.sum() / len(meshes) |
|
|
| @staticmethod |
| def laplacian_smoothing_naive_cot(meshes, method: str = "cot"): |
| """ |
| Naive implementation of laplacian smoothing wit cotangent weights. |
| """ |
| verts_packed = meshes.verts_packed() |
| faces_packed = meshes.faces_packed() |
| V = verts_packed.shape[0] |
|
|
| L = torch.zeros((V, V), dtype=torch.float32, device=meshes.device) |
| inv_areas = torch.zeros((V, 1), dtype=torch.float32, device=meshes.device) |
|
|
| for f in faces_packed: |
| v0 = verts_packed[f[0], :] |
| v1 = verts_packed[f[1], :] |
| v2 = verts_packed[f[2], :] |
| A = (v1 - v2).norm() |
| B = (v0 - v2).norm() |
| C = (v0 - v1).norm() |
| s = 0.5 * (A + B + C) |
|
|
| face_area = (s * (s - A) * (s - B) * (s - C)).clamp_(min=1e-12).sqrt() |
| inv_areas[f[0]] += face_area |
| inv_areas[f[1]] += face_area |
| inv_areas[f[2]] += face_area |
|
|
| A2, B2, C2 = A * A, B * B, C * C |
| cota = (B2 + C2 - A2) / face_area / 4.0 |
| cotb = (A2 + C2 - B2) / face_area / 4.0 |
| cotc = (A2 + B2 - C2) / face_area / 4.0 |
|
|
| L[f[1], f[2]] += cota |
| L[f[2], f[0]] += cotb |
| L[f[0], f[1]] += cotc |
| |
| L[f[2], f[1]] += cota |
| L[f[0], f[2]] += cotb |
| L[f[1], f[0]] += cotc |
|
|
| idx = inv_areas > 0 |
| inv_areas[idx] = 1.0 / inv_areas[idx] |
|
|
| norm_w = L.sum(dim=1, keepdims=True) |
| L_sum = norm_w.clone() |
| idx = norm_w > 0 |
| norm_w[idx] = 1.0 / norm_w[idx] |
|
|
| if method == "cotcurv": |
| loss = (L.mm(verts_packed) - L_sum * verts_packed) * inv_areas * 0.25 |
| loss = loss.norm(dim=1) |
| else: |
| loss = L.mm(verts_packed) * norm_w - verts_packed |
| loss = loss.norm(dim=1) |
|
|
| weights = torch.zeros(V, dtype=torch.float32, device=meshes.device) |
| for v in range(V): |
| weights[v] = meshes.num_verts_per_mesh()[ |
| meshes.verts_packed_to_mesh_idx()[v] |
| ] |
| weights = 1.0 / weights |
| loss = loss * weights |
|
|
| return loss.sum() / len(meshes) |
|
|
| @staticmethod |
| def init_meshes(num_meshes: int = 10, num_verts: int = 1000, num_faces: int = 3000): |
| device = torch.device("cuda:0") |
| verts_list = [] |
| faces_list = [] |
| for _ in range(num_meshes): |
| verts = ( |
| torch.rand((num_verts, 3), dtype=torch.float32, device=device) * 2.0 |
| - 1.0 |
| ) |
| faces = torch.stack( |
| [ |
| torch.randperm(num_verts, device=device)[:3] |
| for _ in range(num_faces) |
| ], |
| dim=0, |
| ) |
| |
| verts_list.append(verts) |
| faces_list.append(faces) |
| meshes = Meshes(verts_list, faces_list) |
|
|
| return meshes |
|
|
| def test_laplacian_smoothing_uniform(self): |
| """ |
| Test Laplacian Smoothing with uniform weights. |
| """ |
| meshes = TestLaplacianSmoothing.init_meshes(10, 100, 300) |
|
|
| |
| out = mesh_laplacian_smoothing(meshes, method="uniform") |
| naive_out = TestLaplacianSmoothing.laplacian_smoothing_naive_uniform(meshes) |
|
|
| self.assertTrue(torch.allclose(out, naive_out)) |
|
|
| def test_laplacian_smoothing_cot(self): |
| """ |
| Test Laplacian Smoothing with cot weights. |
| """ |
| meshes = TestLaplacianSmoothing.init_meshes(10, 100, 300) |
|
|
| |
| out = mesh_laplacian_smoothing(meshes, method="cot") |
| naive_out = TestLaplacianSmoothing.laplacian_smoothing_naive_cot( |
| meshes, method="cot" |
| ) |
|
|
| self.assertTrue(torch.allclose(out, naive_out)) |
|
|
| def test_laplacian_smoothing_cotcurv(self): |
| """ |
| Test Laplacian Smoothing with cotcurv weights. |
| """ |
| meshes = TestLaplacianSmoothing.init_meshes(10, 100, 300) |
|
|
| |
| out = mesh_laplacian_smoothing(meshes, method="cotcurv") |
| naive_out = TestLaplacianSmoothing.laplacian_smoothing_naive_cot( |
| meshes, method="cotcurv" |
| ) |
|
|
| self.assertTrue(torch.allclose(out, naive_out)) |
|
|
| @staticmethod |
| def laplacian_smoothing_with_init( |
| num_meshes: int, num_verts: int, num_faces: int, device: str = "cpu" |
| ): |
| device = torch.device(device) |
| verts_list = [] |
| faces_list = [] |
| for _ in range(num_meshes): |
| verts = torch.rand((num_verts, 3), dtype=torch.float32, device=device) |
| faces = torch.randint( |
| num_verts, size=(num_faces, 3), dtype=torch.int64, device=device |
| ) |
| verts_list.append(verts) |
| faces_list.append(faces) |
| meshes = Meshes(verts_list, faces_list) |
| torch.cuda.synchronize() |
|
|
| def smooth(): |
| mesh_laplacian_smoothing(meshes, method="cotcurv") |
| torch.cuda.synchronize() |
|
|
| return smooth |
|
|