test_mesh_laplacian_smoothing.py 6.51 KB
Newer Older
Jeremy Reizenstein's avatar
Jeremy Reizenstein committed
1
# Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
facebook-github-bot's avatar
facebook-github-bot committed
2
3
4
5


import unittest

6
import torch
facebook-github-bot's avatar
facebook-github-bot committed
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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()  # (sum(V_n), 3)
        faces_packed = meshes.faces_packed()  # (sum(F_n), 3)
        V = verts_packed.shape[0]

        L = torch.zeros((V, V), dtype=torch.float32, device=meshes.device)

        # filling L with the face pairs should be the same as edge pairs
        for f in faces_packed:
            L[f[0], f[1]] = 1
            L[f[0], f[2]] = 1
            L[f[1], f[2]] = 1
            # symetric
            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()  # (sum(V_n), 3)
        faces_packed = meshes.faces_packed()  # (sum(F_n), 3)
        V = verts_packed.shape[0]

        L = torch.zeros((V, V), dtype=torch.float32, device=meshes.device)
59
        inv_areas = torch.zeros((V, 1), dtype=torch.float32, device=meshes.device)
facebook-github-bot's avatar
facebook-github-bot committed
60
61
62
63
64
65
66
67
68
69

        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)

70
            face_area = (s * (s - A) * (s - B) * (s - C)).clamp_(min=1e-12).sqrt()
facebook-github-bot's avatar
facebook-github-bot committed
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
            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
            # symetric
            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)
92
        L_sum = norm_w.clone()
facebook-github-bot's avatar
facebook-github-bot committed
93
94
95
96
        idx = norm_w > 0
        norm_w[idx] = 1.0 / norm_w[idx]

        if method == "cotcurv":
97
            loss = (L.mm(verts_packed) - L_sum * verts_packed) * inv_areas * 0.25
facebook-github-bot's avatar
facebook-github-bot committed
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
            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
114
    def init_meshes(num_meshes: int = 10, num_verts: int = 1000, num_faces: int = 3000):
facebook-github-bot's avatar
facebook-github-bot committed
115
116
117
118
119
        device = torch.device("cuda:0")
        verts_list = []
        faces_list = []
        for _ in range(num_meshes):
            verts = (
120
                torch.rand((num_verts, 3), dtype=torch.float32, device=device) * 2.0
facebook-github-bot's avatar
facebook-github-bot committed
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
                - 1.0
            )  # verts in the space of [-1, 1]
            faces = torch.stack(
                [
                    torch.randperm(num_verts, device=device)[:3]
                    for _ in range(num_faces)
                ],
                dim=0,
            )
            # avoids duplicate vertices in a face
            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)

        # feats in list
        out = mesh_laplacian_smoothing(meshes, method="uniform")
145
        naive_out = TestLaplacianSmoothing.laplacian_smoothing_naive_uniform(meshes)
facebook-github-bot's avatar
facebook-github-bot committed
146
147
148
149
150

        self.assertTrue(torch.allclose(out, naive_out))

    def test_laplacian_smoothing_cot(self):
        """
151
        Test Laplacian Smoothing with cot weights.
facebook-github-bot's avatar
facebook-github-bot committed
152
153
154
155
156
157
158
159
160
161
162
163
164
        """
        meshes = TestLaplacianSmoothing.init_meshes(10, 100, 300)

        # feats in list
        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):
        """
165
        Test Laplacian Smoothing with cotcurv weights.
facebook-github-bot's avatar
facebook-github-bot committed
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
        """
        meshes = TestLaplacianSmoothing.init_meshes(10, 100, 300)

        # feats in list
        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):
185
            verts = torch.rand((num_verts, 3), dtype=torch.float32, device=device)
facebook-github-bot's avatar
facebook-github-bot committed
186
187
188
189
190
191
192
193
194
195
196
197
198
            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