test_face_areas_normals.py 5.07 KB
Newer Older
1
# Copyright (c) Meta Platforms, Inc. and affiliates.
Patrick Labatut's avatar
Patrick Labatut committed
2
3
4
5
# 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.
6
7
8
9


import unittest

10
import torch
11
from common_testing import get_random_cuda_device, TestCaseMixin
Georgia Gkioxari's avatar
Georgia Gkioxari committed
12
from pytorch3d.ops import mesh_face_areas_normals
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pytorch3d.structures.meshes import Meshes


class TestFaceAreasNormals(TestCaseMixin, unittest.TestCase):
    def setUp(self) -> None:
        super().setUp()
        torch.manual_seed(1)

    @staticmethod
    def init_meshes(
        num_meshes: int = 10,
        num_verts: int = 1000,
        num_faces: int = 3000,
        device: str = "cpu",
    ):
        device = torch.device(device)
        verts_list = []
        faces_list = []
        for _ in range(num_meshes):
            verts = torch.rand(
33
                (num_verts, 3), dtype=torch.float32, device=device, requires_grad=True
34
35
36
37
38
39
40
41
42
43
44
            )
            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)

        return meshes

    @staticmethod
Georgia Gkioxari's avatar
Georgia Gkioxari committed
45
    def face_areas_normals_python(verts, faces):
46
47
48
        """
        Pytorch implementation for face areas & normals.
        """
Georgia Gkioxari's avatar
Georgia Gkioxari committed
49
50
        # TODO(gkioxari) Change cast to floats once we add support for doubles.
        verts = verts.float()
51
52
53
54
55
56
57
        vertices_faces = verts[faces]  # (F, 3, 3)
        # vector pointing from v0 to v1
        v01 = vertices_faces[:, 1] - vertices_faces[:, 0]
        # vector pointing from v0 to v2
        v02 = vertices_faces[:, 2] - vertices_faces[:, 0]
        normals = torch.cross(v01, v02, dim=1)  # (F, 3)
        face_areas = normals.norm(dim=-1) / 2
58
        face_normals = torch.nn.functional.normalize(normals, p=2, dim=1, eps=1e-6)
59
60
        return face_areas, face_normals

Georgia Gkioxari's avatar
Georgia Gkioxari committed
61
    def _test_face_areas_normals_helper(self, device, dtype=torch.float32):
62
63
64
65
        """
        Check the results from face_areas cuda/cpp and PyTorch implementation are
        the same.
        """
Georgia Gkioxari's avatar
Georgia Gkioxari committed
66
67
68
69
70
71
72
73
74
75
76
        meshes = self.init_meshes(10, 200, 400, device=device)
        # make them leaf nodes
        verts = meshes.verts_packed().detach().clone().to(dtype)
        verts.requires_grad = True
        faces = meshes.faces_packed().detach().clone()

        # forward
        areas, normals = mesh_face_areas_normals(verts, faces)
        verts_torch = verts.detach().clone().to(dtype)
        verts_torch.requires_grad = True
        faces_torch = faces.detach().clone()
77
        (areas_torch, normals_torch) = TestFaceAreasNormals.face_areas_normals_python(
Georgia Gkioxari's avatar
Georgia Gkioxari committed
78
79
            verts_torch, faces_torch
        )
80
81
82
83
84
85
        self.assertClose(areas_torch, areas, atol=1e-7)
        # normals get normalized by area thus sensitivity increases as areas
        # in our tests can be arbitrarily small. Thus we compare normals after
        # multiplying with areas
        unnormals = normals * areas.view(-1, 1)
        unnormals_torch = normals_torch * areas_torch.view(-1, 1)
Georgia Gkioxari's avatar
Georgia Gkioxari committed
86
87
88
89
90
91
92
93
94
95
        self.assertClose(unnormals_torch, unnormals, atol=1e-6)

        # backward
        grad_areas = torch.rand(areas.shape, device=device, dtype=dtype)
        grad_normals = torch.rand(normals.shape, device=device, dtype=dtype)
        areas.backward((grad_areas, grad_normals))
        grad_verts = verts.grad
        areas_torch.backward((grad_areas, grad_normals))
        grad_verts_torch = verts_torch.grad
        self.assertClose(grad_verts_torch, grad_verts, atol=1e-6)
96
97
98
99
100

    def test_face_areas_normals_cpu(self):
        self._test_face_areas_normals_helper("cpu")

    def test_face_areas_normals_cuda(self):
Nikhila Ravi's avatar
Nikhila Ravi committed
101
102
        device = get_random_cuda_device()
        self._test_face_areas_normals_helper(device)
103

Georgia Gkioxari's avatar
Georgia Gkioxari committed
104
105
106
107
    def test_nonfloats_cpu(self):
        self._test_face_areas_normals_helper("cpu", dtype=torch.double)

    def test_nonfloats_cuda(self):
Nikhila Ravi's avatar
Nikhila Ravi committed
108
109
        device = get_random_cuda_device()
        self._test_face_areas_normals_helper(device, dtype=torch.double)
Georgia Gkioxari's avatar
Georgia Gkioxari committed
110

111
112
    @staticmethod
    def face_areas_normals_with_init(
Georgia Gkioxari's avatar
Georgia Gkioxari committed
113
        num_meshes: int, num_verts: int, num_faces: int, device: str = "cpu"
114
115
116
117
118
119
120
121
122
    ):
        meshes = TestFaceAreasNormals.init_meshes(
            num_meshes, num_verts, num_faces, device
        )
        verts = meshes.verts_packed()
        faces = meshes.faces_packed()
        torch.cuda.synchronize()

        def face_areas_normals():
Georgia Gkioxari's avatar
Georgia Gkioxari committed
123
            mesh_face_areas_normals(verts, faces)
124
125
126
127
128
129
            torch.cuda.synchronize()

        return face_areas_normals

    @staticmethod
    def face_areas_normals_with_init_torch(
Georgia Gkioxari's avatar
Georgia Gkioxari committed
130
        num_meshes: int, num_verts: int, num_faces: int, device: str = "cpu"
131
132
133
134
135
136
137
138
139
    ):
        meshes = TestFaceAreasNormals.init_meshes(
            num_meshes, num_verts, num_faces, device
        )
        verts = meshes.verts_packed()
        faces = meshes.faces_packed()
        torch.cuda.synchronize()

        def face_areas_normals():
Georgia Gkioxari's avatar
Georgia Gkioxari committed
140
            TestFaceAreasNormals.face_areas_normals_python(verts, faces)
141
142
143
            torch.cuda.synchronize()

        return face_areas_normals