File size: 5,196 Bytes
7088d16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# 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 unittest

import torch
from pytorch3d.ops import mesh_face_areas_normals
from pytorch3d.structures.meshes import Meshes

from .common_testing import get_random_cuda_device, TestCaseMixin


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(
                (num_verts, 3), dtype=torch.float32, device=device, requires_grad=True
            )
            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
    def face_areas_normals_python(verts, faces):
        """
        Pytorch implementation for face areas & normals.
        """
        # TODO(gkioxari) Change cast to floats once we add support for doubles.
        verts = verts.float()
        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
        face_normals = torch.nn.functional.normalize(normals, p=2, dim=1, eps=1e-6)
        return face_areas, face_normals

    def _test_face_areas_normals_helper(self, device, dtype=torch.float32):
        """
        Check the results from face_areas cuda/cpp and PyTorch implementation are
        the same.
        """
        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()
        (areas_torch, normals_torch) = TestFaceAreasNormals.face_areas_normals_python(
            verts_torch, faces_torch
        )
        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)
        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)

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

    def test_face_areas_normals_cuda(self):
        device = get_random_cuda_device()
        self._test_face_areas_normals_helper(device)

    def test_nonfloats_cpu(self):
        self._test_face_areas_normals_helper("cpu", dtype=torch.double)

    def test_nonfloats_cuda(self):
        device = get_random_cuda_device()
        self._test_face_areas_normals_helper(device, dtype=torch.double)

    @staticmethod
    def face_areas_normals_with_init(
        num_meshes: int, num_verts: int, num_faces: int, device: str = "cpu"
    ):
        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():
            mesh_face_areas_normals(verts, faces)
            torch.cuda.synchronize()

        return face_areas_normals

    @staticmethod
    def face_areas_normals_with_init_torch(
        num_meshes: int, num_verts: int, num_faces: int, device: str = "cpu"
    ):
        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():
            TestFaceAreasNormals.face_areas_normals_python(verts, faces)
            torch.cuda.synchronize()

        return face_areas_normals