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

import os
import unittest
5
import warnings
facebook-github-bot's avatar
facebook-github-bot committed
6
7
8
from io import StringIO
from pathlib import Path

9
10
import torch
from common_testing import TestCaseMixin
11
from pytorch3d.io import load_obj, load_objs_as_meshes, save_obj
12
13
14
from pytorch3d.io.mtl_io import (
    _bilinear_interpolation_grid_sample,
    _bilinear_interpolation_vectorized,
15
    _parse_mtl,
16
)
Nikhila Ravi's avatar
Nikhila Ravi committed
17
18
from pytorch3d.renderer import TexturesAtlas, TexturesUV, TexturesVertex
from pytorch3d.structures import Meshes, join_meshes_as_batch
19
from pytorch3d.utils import torus
facebook-github-bot's avatar
facebook-github-bot committed
20

21
22

class TestMeshObjIO(TestCaseMixin, unittest.TestCase):
facebook-github-bot's avatar
facebook-github-bot committed
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    def test_load_obj_simple(self):
        obj_file = "\n".join(
            [
                "# this is a comment",  # Comments should be ignored.
                "v 0.1 0.2 0.3",
                "v 0.2 0.3 0.4",
                "v 0.3 0.4 0.5",
                "v  0.4 0.5 0.6",  # some obj files have multiple spaces after v
                "f 1 2 3",
                "f 1 2 4 3 1",  # Polygons should be split into triangles
            ]
        )
        obj_file = StringIO(obj_file)
        verts, faces, aux = load_obj(obj_file)
        normals = aux.normals
        textures = aux.verts_uvs
        materials = aux.material_colors
        tex_maps = aux.texture_images

        expected_verts = torch.tensor(
43
            [[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5], [0.4, 0.5, 0.6]],
facebook-github-bot's avatar
facebook-github-bot committed
44
45
46
47
48
49
50
51
52
53
54
55
56
            dtype=torch.float32,
        )
        expected_faces = torch.tensor(
            [
                [0, 1, 2],  # First face
                [0, 1, 3],  # Second face (polygon)
                [0, 3, 2],  # Second face (polygon)
                [0, 2, 0],  # Second face (polygon)
            ],
            dtype=torch.int64,
        )
        self.assertTrue(torch.all(verts == expected_verts))
        self.assertTrue(torch.all(faces.verts_idx == expected_faces))
Nikhila Ravi's avatar
Nikhila Ravi committed
57
        padded_vals = -(torch.ones_like(faces.verts_idx))
58
59
        self.assertTrue(torch.all(faces.normals_idx == padded_vals))
        self.assertTrue(torch.all(faces.textures_idx == padded_vals))
facebook-github-bot's avatar
facebook-github-bot committed
60
        self.assertTrue(
Nikhila Ravi's avatar
Nikhila Ravi committed
61
            torch.all(faces.materials_idx == -(torch.ones(len(expected_faces))))
facebook-github-bot's avatar
facebook-github-bot committed
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
        )
        self.assertTrue(normals is None)
        self.assertTrue(textures is None)
        self.assertTrue(materials is None)
        self.assertTrue(tex_maps is None)

    def test_load_obj_complex(self):
        obj_file = "\n".join(
            [
                "# this is a comment",  # Comments should be ignored.
                "v 0.1 0.2 0.3",
                "v 0.2 0.3 0.4",
                "v 0.3 0.4 0.5",
                "v 0.4 0.5 0.6",
                "vn 0.000000 0.000000 -1.000000",
                "vn -1.000000 -0.000000 -0.000000",
                "vn -0.000000 -0.000000 1.000000",  # Normals should not be ignored.
                "v 0.5 0.6 0.7",
                "vt 0.749279 0.501284 0.0",  # Some files add 0.0 - ignore this.
                "vt 0.999110 0.501077",
                "vt 0.999455 0.750380",
                "f 1 2 3",
                "f 1 2 4 3 5",  # Polygons should be split into triangles
                "f 2/1/2 3/1/2 4/2/2",  # Texture/normals are loaded correctly.
                "f -1 -2 1",  # Negative indexing counts from the end.
            ]
        )
        obj_file = StringIO(obj_file)
        verts, faces, aux = load_obj(obj_file)
        normals = aux.normals
        textures = aux.verts_uvs
        materials = aux.material_colors
        tex_maps = aux.texture_images

        expected_verts = torch.tensor(
            [
                [0.1, 0.2, 0.3],
                [0.2, 0.3, 0.4],
                [0.3, 0.4, 0.5],
                [0.4, 0.5, 0.6],
                [0.5, 0.6, 0.7],
            ],
            dtype=torch.float32,
        )
        expected_faces = torch.tensor(
            [
                [0, 1, 2],  # First face
                [0, 1, 3],  # Second face (polygon)
                [0, 3, 2],  # Second face (polygon)
                [0, 2, 4],  # Second face (polygon)
                [1, 2, 3],  # Third face (normals / texture)
                [4, 3, 0],  # Fourth face (negative indices)
            ],
            dtype=torch.int64,
        )
        expected_normals = torch.tensor(
            [
                [0.000000, 0.000000, -1.000000],
                [-1.000000, -0.000000, -0.000000],
                [-0.000000, -0.000000, 1.000000],
            ],
            dtype=torch.float32,
        )
        expected_textures = torch.tensor(
            [[0.749279, 0.501284], [0.999110, 0.501077], [0.999455, 0.750380]],
            dtype=torch.float32,
        )
Nikhila Ravi's avatar
Nikhila Ravi committed
129
130
131
        expected_faces_normals_idx = -(
            torch.ones_like(expected_faces, dtype=torch.int64)
        )
132
        expected_faces_normals_idx[4, :] = torch.tensor([1, 1, 1], dtype=torch.int64)
Nikhila Ravi's avatar
Nikhila Ravi committed
133
134
        expected_faces_textures_idx = -(
            torch.ones_like(expected_faces, dtype=torch.int64)
135
136
        )
        expected_faces_textures_idx[4, :] = torch.tensor([0, 0, 1], dtype=torch.int64)
facebook-github-bot's avatar
facebook-github-bot committed
137
138
139

        self.assertTrue(torch.all(verts == expected_verts))
        self.assertTrue(torch.all(faces.verts_idx == expected_faces))
Jeremy Reizenstein's avatar
Jeremy Reizenstein committed
140
141
142
143
        self.assertClose(normals, expected_normals)
        self.assertClose(textures, expected_textures)
        self.assertClose(faces.normals_idx, expected_faces_normals_idx)
        self.assertClose(faces.textures_idx, expected_faces_textures_idx)
facebook-github-bot's avatar
facebook-github-bot committed
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
        self.assertTrue(materials is None)
        self.assertTrue(tex_maps is None)

    def test_load_obj_normals_only(self):
        obj_file = "\n".join(
            [
                "v 0.1 0.2 0.3",
                "v 0.2 0.3 0.4",
                "v 0.3 0.4 0.5",
                "v 0.4 0.5 0.6",
                "vn 0.000000 0.000000 -1.000000",
                "vn -1.000000 -0.000000 -0.000000",
                "f 2//1 3//1 4//2",
            ]
        )
        obj_file = StringIO(obj_file)
160
        expected_faces_normals_idx = torch.tensor([[0, 0, 1]], dtype=torch.int64)
facebook-github-bot's avatar
facebook-github-bot committed
161
        expected_normals = torch.tensor(
162
            [[0.000000, 0.000000, -1.000000], [-1.000000, -0.000000, -0.000000]],
facebook-github-bot's avatar
facebook-github-bot committed
163
164
165
            dtype=torch.float32,
        )
        expected_verts = torch.tensor(
166
            [[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5], [0.4, 0.5, 0.6]],
facebook-github-bot's avatar
facebook-github-bot committed
167
168
169
170
171
172
173
            dtype=torch.float32,
        )
        verts, faces, aux = load_obj(obj_file)
        normals = aux.normals
        textures = aux.verts_uvs
        materials = aux.material_colors
        tex_maps = aux.texture_images
Jeremy Reizenstein's avatar
Jeremy Reizenstein committed
174
175
176
        self.assertClose(faces.normals_idx, expected_faces_normals_idx)
        self.assertClose(normals, expected_normals)
        self.assertClose(verts, expected_verts)
177
178
        # Textures idx padded  with -1.
        self.assertClose(faces.textures_idx, torch.ones_like(faces.verts_idx) * -1)
facebook-github-bot's avatar
facebook-github-bot committed
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
        self.assertTrue(textures is None)
        self.assertTrue(materials is None)
        self.assertTrue(tex_maps is None)

    def test_load_obj_textures_only(self):
        obj_file = "\n".join(
            [
                "v 0.1 0.2 0.3",
                "v 0.2 0.3 0.4",
                "v 0.3 0.4 0.5",
                "v 0.4 0.5 0.6",
                "vt 0.999110 0.501077",
                "vt 0.999455 0.750380",
                "f 2/1 3/1 4/2",
            ]
        )
        obj_file = StringIO(obj_file)
196
        expected_faces_textures_idx = torch.tensor([[0, 0, 1]], dtype=torch.int64)
facebook-github-bot's avatar
facebook-github-bot committed
197
198
199
200
        expected_textures = torch.tensor(
            [[0.999110, 0.501077], [0.999455, 0.750380]], dtype=torch.float32
        )
        expected_verts = torch.tensor(
201
            [[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5], [0.4, 0.5, 0.6]],
facebook-github-bot's avatar
facebook-github-bot committed
202
203
204
205
206
207
208
209
            dtype=torch.float32,
        )
        verts, faces, aux = load_obj(obj_file)
        normals = aux.normals
        textures = aux.verts_uvs
        materials = aux.material_colors
        tex_maps = aux.texture_images

Jeremy Reizenstein's avatar
Jeremy Reizenstein committed
210
211
212
        self.assertClose(faces.textures_idx, expected_faces_textures_idx)
        self.assertClose(expected_textures, textures)
        self.assertClose(expected_verts, verts)
213
        self.assertTrue(
Nikhila Ravi's avatar
Nikhila Ravi committed
214
            torch.all(faces.normals_idx == -(torch.ones_like(faces.textures_idx)))
215
        )
facebook-github-bot's avatar
facebook-github-bot committed
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
        self.assertTrue(normals is None)
        self.assertTrue(materials is None)
        self.assertTrue(tex_maps is None)

    def test_load_obj_error_textures(self):
        obj_file = "\n".join(["vt 0.1"])
        obj_file = StringIO(obj_file)

        with self.assertRaises(ValueError) as err:
            load_obj(obj_file)
        self.assertTrue("does not have 2 values" in str(err.exception))

    def test_load_obj_error_normals(self):
        obj_file = "\n".join(["vn 0.1"])
        obj_file = StringIO(obj_file)

        with self.assertRaises(ValueError) as err:
            load_obj(obj_file)
        self.assertTrue("does not have 3 values" in str(err.exception))

    def test_load_obj_error_vertices(self):
        obj_file = "\n".join(["v 1"])
        obj_file = StringIO(obj_file)

        with self.assertRaises(ValueError) as err:
            load_obj(obj_file)
        self.assertTrue("does not have 3 values" in str(err.exception))

    def test_load_obj_error_inconsistent_triplets(self):
        obj_file = "\n".join(["f 2//1 3/1 4/1/2"])
        obj_file = StringIO(obj_file)

        with self.assertRaises(ValueError) as err:
            load_obj(obj_file)
250
        self.assertTrue("Vertex properties are inconsistent" in str(err.exception))
facebook-github-bot's avatar
facebook-github-bot committed
251
252
253
254
255
256
257

    def test_load_obj_error_too_many_vertex_properties(self):
        obj_file = "\n".join(["f 2/1/1/3"])
        obj_file = StringIO(obj_file)

        with self.assertRaises(ValueError) as err:
            load_obj(obj_file)
258
        self.assertTrue("Face vertices can ony have 3 properties" in str(err.exception))
facebook-github-bot's avatar
facebook-github-bot committed
259
260
261
262
263
264
265

    def test_load_obj_error_invalid_vertex_indices(self):
        obj_file = "\n".join(
            ["v 0.1 0.2 0.3", "v 0.1 0.2 0.3", "v 0.1 0.2 0.3", "f -2 5 1"]
        )
        obj_file = StringIO(obj_file)

266
        with self.assertWarnsRegex(UserWarning, "Faces have invalid indices"):
facebook-github-bot's avatar
facebook-github-bot committed
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
            load_obj(obj_file)

    def test_load_obj_error_invalid_normal_indices(self):
        obj_file = "\n".join(
            [
                "v 0.1 0.2 0.3",
                "v 0.1 0.2 0.3",
                "v 0.1 0.2 0.3",
                "vn 0.1 0.2 0.3",
                "vn 0.1 0.2 0.3",
                "vn 0.1 0.2 0.3",
                "f -2/2 2/4 1/1",
            ]
        )
        obj_file = StringIO(obj_file)

283
        with self.assertWarnsRegex(UserWarning, "Faces have invalid indices"):
facebook-github-bot's avatar
facebook-github-bot committed
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
            load_obj(obj_file)

    def test_load_obj_error_invalid_texture_indices(self):
        obj_file = "\n".join(
            [
                "v 0.1 0.2 0.3",
                "v 0.1 0.2 0.3",
                "v 0.1 0.2 0.3",
                "vt 0.1 0.2",
                "vt 0.1 0.2",
                "vt 0.1 0.2",
                "f -2//2 2//6 1//1",
            ]
        )
        obj_file = StringIO(obj_file)

300
        with self.assertWarnsRegex(UserWarning, "Faces have invalid indices"):
facebook-github-bot's avatar
facebook-github-bot committed
301
            load_obj(obj_file)
302
303
304
305
306
307
308

    def test_save_obj_invalid_shapes(self):
        # Invalid vertices shape
        with self.assertRaises(ValueError) as error:
            verts = torch.FloatTensor([[0.1, 0.2, 0.3, 0.4]])  # (V, 4)
            faces = torch.LongTensor([[0, 1, 2]])
            save_obj(StringIO(), verts, faces)
309
310
311
        expected_message = (
            "Argument 'verts' should either be empty or of shape (num_verts, 3)."
        )
312
313
314
315
316
317
318
        self.assertTrue(expected_message, error.exception)

        # Invalid faces shape
        with self.assertRaises(ValueError) as error:
            verts = torch.FloatTensor([[0.1, 0.2, 0.3]])
            faces = torch.LongTensor([[0, 1, 2, 3]])  # (F, 4)
            save_obj(StringIO(), verts, faces)
319
320
321
        expected_message = (
            "Argument 'faces' should either be empty or of shape (num_faces, 3)."
        )
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
        self.assertTrue(expected_message, error.exception)

    def test_save_obj_invalid_indices(self):
        message_regex = "Faces have invalid indices"
        verts = torch.FloatTensor([[0.1, 0.2, 0.3]])
        faces = torch.LongTensor([[0, 1, 2]])
        with self.assertWarnsRegex(UserWarning, message_regex):
            save_obj(StringIO(), verts, faces)

        faces = torch.LongTensor([[-1, 0, 1]])
        with self.assertWarnsRegex(UserWarning, message_regex):
            save_obj(StringIO(), verts, faces)

    def _test_save_load(self, verts, faces):
        f = StringIO()
        save_obj(f, verts, faces)
        f.seek(0)
        expected_verts, expected_faces = verts, faces
        if not len(expected_verts):  # Always compare with a (V, 3) tensor
            expected_verts = torch.zeros(size=(0, 3), dtype=torch.float32)
        if not len(expected_faces):  # Always compare with an (F, 3) tensor
            expected_faces = torch.zeros(size=(0, 3), dtype=torch.int64)
        actual_verts, actual_faces, _ = load_obj(f)
        self.assertClose(expected_verts, actual_verts)
        self.assertClose(expected_faces, actual_faces.verts_idx)

    def test_empty_save_load_obj(self):
        # Vertices + empty faces
        verts = torch.FloatTensor([[0.1, 0.2, 0.3]])
        faces = torch.LongTensor([])
        self._test_save_load(verts, faces)

        faces = torch.zeros(size=(0, 3), dtype=torch.int64)
        self._test_save_load(verts, faces)

        # Faces + empty vertices
        message_regex = "Faces have invalid indices"
        verts = torch.FloatTensor([])
        faces = torch.LongTensor([[0, 1, 2]])
        with self.assertWarnsRegex(UserWarning, message_regex):
            self._test_save_load(verts, faces)

        verts = torch.zeros(size=(0, 3), dtype=torch.float32)
        with self.assertWarnsRegex(UserWarning, message_regex):
            self._test_save_load(verts, faces)

        # Empty vertices + empty faces
        message_regex = "Empty 'verts' and 'faces' arguments provided"
        verts0 = torch.FloatTensor([])
        faces0 = torch.LongTensor([])
        with self.assertWarnsRegex(UserWarning, message_regex):
            self._test_save_load(verts0, faces0)

        faces3 = torch.zeros(size=(0, 3), dtype=torch.int64)
        with self.assertWarnsRegex(UserWarning, message_regex):
            self._test_save_load(verts0, faces3)

        verts3 = torch.zeros(size=(0, 3), dtype=torch.float32)
        with self.assertWarnsRegex(UserWarning, message_regex):
            self._test_save_load(verts3, faces0)

        with self.assertWarnsRegex(UserWarning, message_regex):
            self._test_save_load(verts3, faces3)
facebook-github-bot's avatar
facebook-github-bot committed
385
386
387

    def test_save_obj(self):
        verts = torch.tensor(
388
            [[0.01, 0.2, 0.301], [0.2, 0.03, 0.408], [0.3, 0.4, 0.05], [0.6, 0.7, 0.8]],
facebook-github-bot's avatar
facebook-github-bot committed
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
            dtype=torch.float32,
        )
        faces = torch.tensor(
            [[0, 2, 1], [0, 1, 2], [3, 2, 1], [3, 1, 0]], dtype=torch.int64
        )
        obj_file = StringIO()
        save_obj(obj_file, verts, faces, decimal_places=2)
        expected_file = "\n".join(
            [
                "v 0.01 0.20 0.30",
                "v 0.20 0.03 0.41",
                "v 0.30 0.40 0.05",
                "v 0.60 0.70 0.80",
                "f 1 3 2",
                "f 1 2 3",
                "f 4 3 2",
                "f 4 2 1",
            ]
        )
        actual_file = obj_file.getvalue()
        self.assertEqual(actual_file, expected_file)

    def test_load_mtl(self):
412
        DATA_DIR = Path(__file__).resolve().parent.parent / "docs/tutorials/data"
facebook-github-bot's avatar
facebook-github-bot committed
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
        obj_filename = "cow_mesh/cow.obj"
        filename = os.path.join(DATA_DIR, obj_filename)
        verts, faces, aux = load_obj(filename)
        materials = aux.material_colors
        tex_maps = aux.texture_images

        dtype = torch.float32
        expected_materials = {
            "material_1": {
                "ambient_color": torch.tensor([1.0, 1.0, 1.0], dtype=dtype),
                "diffuse_color": torch.tensor([1.0, 1.0, 1.0], dtype=dtype),
                "specular_color": torch.tensor([0.0, 0.0, 0.0], dtype=dtype),
                "shininess": torch.tensor([10.0], dtype=dtype),
            }
        }
428
429
430
        # Texture atlas is not created as `create_texture_atlas=True` was
        # not set in the load_obj args
        self.assertTrue(aux.texture_atlas is None)
facebook-github-bot's avatar
facebook-github-bot committed
431
432
433
434
435
436
437
438
439
440
        # Check that there is an image with material name material_1.
        self.assertTrue(tuple(tex_maps.keys()) == ("material_1",))
        self.assertTrue(torch.is_tensor(tuple(tex_maps.values())[0]))
        self.assertTrue(
            torch.all(faces.materials_idx == torch.zeros(len(faces.verts_idx)))
        )

        # Check all keys and values in dictionary are the same.
        for n1, n2 in zip(materials.keys(), expected_materials.keys()):
            self.assertTrue(n1 == n2)
441
            for k1, k2 in zip(materials[n1].keys(), expected_materials[n2].keys()):
facebook-github-bot's avatar
facebook-github-bot committed
442
                self.assertTrue(
443
                    torch.allclose(materials[n1][k1], expected_materials[n2][k2])
facebook-github-bot's avatar
facebook-github-bot committed
444
445
                )

446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
    def test_load_mtl_with_spaces_in_resource_filename(self):
        """
        Check that the texture image for materials in mtl files
        is loaded correctly even if there is a space in the file name
        e.g. material 1.png
        """
        mtl_file = "\n".join(
            [
                "newmtl material_1",
                "map_Kd material 1.png",
                "Ka 1.000 1.000 1.000",  # white
                "Kd 1.000 1.000 1.000",  # white
                "Ks 0.000 0.000 0.000",  # black
                "Ns 10.0",
            ]
        )
        mtl_file = StringIO(mtl_file)
        material_properties, texture_files = _parse_mtl(mtl_file, device="cpu")

        dtype = torch.float32
        expected_materials = {
            "material_1": {
                "ambient_color": torch.tensor([1.0, 1.0, 1.0], dtype=dtype),
                "diffuse_color": torch.tensor([1.0, 1.0, 1.0], dtype=dtype),
                "specular_color": torch.tensor([0.0, 0.0, 0.0], dtype=dtype),
                "shininess": torch.tensor([10.0], dtype=dtype),
            }
        }
        # Check that there is a material with name material_1
        self.assertTrue(tuple(texture_files.keys()) == ("material_1",))
        # Check that there is an image with name material 1.png
        self.assertTrue(texture_files["material_1"] == "material 1.png")

        # Check all keys and values in dictionary are the same.
        for n1, n2 in zip(material_properties.keys(), expected_materials.keys()):
            self.assertTrue(n1 == n2)
            for k1, k2 in zip(
                material_properties[n1].keys(), expected_materials[n2].keys()
            ):
                self.assertTrue(
                    torch.allclose(
                        material_properties[n1][k1], expected_materials[n2][k2]
                    )
                )

491
492
493
494
495
496
497
498
499
500
    def test_load_mtl_texture_atlas_compare_softras(self):
        # Load saved texture atlas created with SoftRas.
        device = torch.device("cuda:0")
        DATA_DIR = Path(__file__).resolve().parent.parent
        obj_filename = DATA_DIR / "docs/tutorials/data/cow_mesh/cow.obj"
        expected_atlas_fname = DATA_DIR / "tests/data/cow_texture_atlas_softras.pt"

        # Note, the reference texture atlas generated using SoftRas load_obj function
        # is too large to check in to the repo. Download the file to run the test locally.
        if not os.path.exists(expected_atlas_fname):
Patrick Labatut's avatar
Patrick Labatut committed
501
502
503
504
            url = (
                "https://dl.fbaipublicfiles.com/pytorch3d/data/"
                "tests/cow_texture_atlas_softras.pt"
            )
505
            msg = (
Patrick Labatut's avatar
Patrick Labatut committed
506
507
                "cow_texture_atlas_softras.pt not found, download from %s, "
                "save it at the path %s, and rerun" % (url, expected_atlas_fname)
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
            )
            warnings.warn(msg)
            return True

        expected_atlas = torch.load(expected_atlas_fname)
        _, _, aux = load_obj(
            obj_filename,
            load_textures=True,
            device=device,
            create_texture_atlas=True,
            texture_atlas_size=15,
            texture_wrap="repeat",
        )

        self.assertClose(expected_atlas, aux.texture_atlas, atol=5e-5)

Georgia Gkioxari's avatar
Georgia Gkioxari committed
524
    def test_load_mtl_noload(self):
525
        DATA_DIR = Path(__file__).resolve().parent.parent / "docs/tutorials/data"
Georgia Gkioxari's avatar
Georgia Gkioxari committed
526
527
528
529
530
531
532
        obj_filename = "cow_mesh/cow.obj"
        filename = os.path.join(DATA_DIR, obj_filename)
        verts, faces, aux = load_obj(filename, load_textures=False)

        self.assertTrue(aux.material_colors is None)
        self.assertTrue(aux.texture_images is None)

facebook-github-bot's avatar
facebook-github-bot committed
533
534
535
536
537
538
539
540
541
542
543
544
545
546
    def test_load_mtl_fail(self):
        # Faces have a material
        obj_file = "\n".join(
            [
                "v 0.1 0.2 0.3",
                "v 0.2 0.3 0.4",
                "v 0.3 0.4 0.5",
                "v 0.4 0.5 0.6",
                "usemtl material_1",
                "f 1 2 3",
                "f 1 2 4",
            ]
        )
        obj_file = StringIO(obj_file)
547
        with self.assertWarnsRegex(UserWarning, "No mtl file provided"):
facebook-github-bot's avatar
facebook-github-bot committed
548
549
550
            verts, faces, aux = load_obj(obj_file)

        expected_verts = torch.tensor(
551
            [[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5], [0.4, 0.5, 0.6]],
facebook-github-bot's avatar
facebook-github-bot committed
552
553
554
555
556
557
558
559
560
561
562
563
564
565
            dtype=torch.float32,
        )
        expected_faces = torch.tensor([[0, 1, 2], [0, 1, 3]], dtype=torch.int64)
        self.assertTrue(torch.allclose(verts, expected_verts))
        self.assertTrue(torch.allclose(faces.verts_idx, expected_faces))
        self.assertTrue(aux.material_colors is None)
        self.assertTrue(aux.texture_images is None)
        self.assertTrue(aux.normals is None)
        self.assertTrue(aux.verts_uvs is None)

    def test_load_obj_missing_texture(self):
        DATA_DIR = Path(__file__).resolve().parent / "data"
        obj_filename = "missing_files_obj/model.obj"
        filename = os.path.join(DATA_DIR, obj_filename)
566
        with self.assertWarnsRegex(UserWarning, "Texture file does not exist"):
facebook-github-bot's avatar
facebook-github-bot committed
567
568
569
            verts, faces, aux = load_obj(filename)

        expected_verts = torch.tensor(
570
            [[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5], [0.4, 0.5, 0.6]],
facebook-github-bot's avatar
facebook-github-bot committed
571
572
573
574
575
576
            dtype=torch.float32,
        )
        expected_faces = torch.tensor([[0, 1, 2], [0, 1, 3]], dtype=torch.int64)
        self.assertTrue(torch.allclose(verts, expected_verts))
        self.assertTrue(torch.allclose(faces.verts_idx, expected_faces))

Georgia Gkioxari's avatar
Georgia Gkioxari committed
577
578
579
580
581
582
583
    def test_load_obj_missing_texture_noload(self):
        DATA_DIR = Path(__file__).resolve().parent / "data"
        obj_filename = "missing_files_obj/model.obj"
        filename = os.path.join(DATA_DIR, obj_filename)
        verts, faces, aux = load_obj(filename, load_textures=False)

        expected_verts = torch.tensor(
584
            [[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5], [0.4, 0.5, 0.6]],
Georgia Gkioxari's avatar
Georgia Gkioxari committed
585
586
587
588
589
590
591
592
            dtype=torch.float32,
        )
        expected_faces = torch.tensor([[0, 1, 2], [0, 1, 3]], dtype=torch.int64)
        self.assertTrue(torch.allclose(verts, expected_verts))
        self.assertTrue(torch.allclose(faces.verts_idx, expected_faces))
        self.assertTrue(aux.material_colors is None)
        self.assertTrue(aux.texture_images is None)

facebook-github-bot's avatar
facebook-github-bot committed
593
594
595
596
    def test_load_obj_missing_mtl(self):
        DATA_DIR = Path(__file__).resolve().parent / "data"
        obj_filename = "missing_files_obj/model2.obj"
        filename = os.path.join(DATA_DIR, obj_filename)
597
        with self.assertWarnsRegex(UserWarning, "Mtl file does not exist"):
facebook-github-bot's avatar
facebook-github-bot committed
598
599
600
            verts, faces, aux = load_obj(filename)

        expected_verts = torch.tensor(
601
            [[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5], [0.4, 0.5, 0.6]],
facebook-github-bot's avatar
facebook-github-bot committed
602
603
604
605
606
607
            dtype=torch.float32,
        )
        expected_faces = torch.tensor([[0, 1, 2], [0, 1, 3]], dtype=torch.int64)
        self.assertTrue(torch.allclose(verts, expected_verts))
        self.assertTrue(torch.allclose(faces.verts_idx, expected_faces))

Georgia Gkioxari's avatar
Georgia Gkioxari committed
608
609
610
611
612
613
614
    def test_load_obj_missing_mtl_noload(self):
        DATA_DIR = Path(__file__).resolve().parent / "data"
        obj_filename = "missing_files_obj/model2.obj"
        filename = os.path.join(DATA_DIR, obj_filename)
        verts, faces, aux = load_obj(filename, load_textures=False)

        expected_verts = torch.tensor(
615
            [[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5], [0.4, 0.5, 0.6]],
Georgia Gkioxari's avatar
Georgia Gkioxari committed
616
617
618
619
620
621
622
623
            dtype=torch.float32,
        )
        expected_faces = torch.tensor([[0, 1, 2], [0, 1, 3]], dtype=torch.int64)
        self.assertTrue(torch.allclose(verts, expected_verts))
        self.assertTrue(torch.allclose(faces.verts_idx, expected_faces))
        self.assertTrue(aux.material_colors is None)
        self.assertTrue(aux.texture_images is None)

Jeremy Reizenstein's avatar
Jeremy Reizenstein committed
624
    def test_join_meshes_as_batch(self):
625
        """
Jeremy Reizenstein's avatar
Jeremy Reizenstein committed
626
627
        Test that join_meshes_as_batch and load_objs_as_meshes are consistent
        with single meshes.
628
629
630
631
632
633
634
635
636
637
638
639
640
641
        """

        def check_triple(mesh, mesh3):
            """
            Verify that mesh3 is three copies of mesh.
            """

            def check_item(x, y):
                self.assertEqual(x is None, y is None)
                if x is not None:
                    self.assertClose(torch.cat([x, x, x]), y)

            check_item(mesh.verts_padded(), mesh3.verts_padded())
            check_item(mesh.faces_padded(), mesh3.faces_padded())
Nikhila Ravi's avatar
Nikhila Ravi committed
642

643
            if mesh.textures is not None:
Nikhila Ravi's avatar
Nikhila Ravi committed
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
                if isinstance(mesh.textures, TexturesUV):
                    check_item(
                        mesh.textures.faces_uvs_padded(),
                        mesh3.textures.faces_uvs_padded(),
                    )
                    check_item(
                        mesh.textures.verts_uvs_padded(),
                        mesh3.textures.verts_uvs_padded(),
                    )
                    check_item(
                        mesh.textures.maps_padded(), mesh3.textures.maps_padded()
                    )
                elif isinstance(mesh.textures, TexturesVertex):
                    check_item(
                        mesh.textures.verts_features_padded(),
                        mesh3.textures.verts_features_padded(),
                    )
                elif isinstance(mesh.textures, TexturesAtlas):
                    check_item(
                        mesh.textures.atlas_padded(), mesh3.textures.atlas_padded()
                    )
665

666
        DATA_DIR = Path(__file__).resolve().parent.parent / "docs/tutorials/data"
667
668
669
670
671
        obj_filename = DATA_DIR / "cow_mesh/cow.obj"

        mesh = load_objs_as_meshes([obj_filename])
        mesh3 = load_objs_as_meshes([obj_filename, obj_filename, obj_filename])
        check_triple(mesh, mesh3)
672
        self.assertTupleEqual(mesh.textures.maps_padded().shape, (1, 1024, 1024, 3))
673

674
675
676
677
678
679
        # Try mismatched texture map sizes, which needs a call to interpolate()
        mesh2048 = mesh.clone()
        maps = mesh.textures.maps_padded()
        mesh2048.textures._maps_padded = torch.cat([maps, maps], dim=1)
        join_meshes_as_batch([mesh.to("cuda:0"), mesh2048.to("cuda:0")])

680
681
682
683
684
685
686
        mesh_notex = load_objs_as_meshes([obj_filename], load_textures=False)
        mesh3_notex = load_objs_as_meshes(
            [obj_filename, obj_filename, obj_filename], load_textures=False
        )
        check_triple(mesh_notex, mesh3_notex)
        self.assertIsNone(mesh_notex.textures)

Nikhila Ravi's avatar
Nikhila Ravi committed
687
        # meshes with vertex texture, join into a batch.
688
689
        verts = torch.randn((4, 3), dtype=torch.float32)
        faces = torch.tensor([[2, 1, 0], [3, 1, 0]], dtype=torch.int64)
Nikhila Ravi's avatar
Nikhila Ravi committed
690
691
692
        vert_tex = torch.ones_like(verts)
        rgb_tex = TexturesVertex(verts_features=[vert_tex])
        mesh_rgb = Meshes(verts=[verts], faces=[faces], textures=rgb_tex)
Jeremy Reizenstein's avatar
Jeremy Reizenstein committed
693
        mesh_rgb3 = join_meshes_as_batch([mesh_rgb, mesh_rgb, mesh_rgb])
694
695
        check_triple(mesh_rgb, mesh_rgb3)

Nikhila Ravi's avatar
Nikhila Ravi committed
696
697
698
699
700
701
702
703
704
        # meshes with texture atlas, join into a batch.
        device = "cuda:0"
        atlas = torch.rand((2, 4, 4, 3), dtype=torch.float32, device=device)
        atlas_tex = TexturesAtlas(atlas=[atlas])
        mesh_atlas = Meshes(verts=[verts], faces=[faces], textures=atlas_tex)
        mesh_atlas3 = join_meshes_as_batch([mesh_atlas, mesh_atlas, mesh_atlas])
        check_triple(mesh_atlas, mesh_atlas3)

        # Test load multiple meshes with textures into a batch.
705
706
707
        teapot_obj = DATA_DIR / "teapot.obj"
        mesh_teapot = load_objs_as_meshes([teapot_obj])
        teapot_verts, teapot_faces = mesh_teapot.get_mesh_verts_faces(0)
708
        mix_mesh = load_objs_as_meshes([obj_filename, teapot_obj], load_textures=False)
709
710
711
712
713
714
        self.assertEqual(len(mix_mesh), 2)
        self.assertClose(mix_mesh.verts_list()[0], mesh.verts_list()[0])
        self.assertClose(mix_mesh.faces_list()[0], mesh.faces_list()[0])
        self.assertClose(mix_mesh.verts_list()[1], teapot_verts)
        self.assertClose(mix_mesh.faces_list()[1], teapot_faces)

Jeremy Reizenstein's avatar
Jeremy Reizenstein committed
715
        cow3_tea = join_meshes_as_batch([mesh3, mesh_teapot], include_textures=False)
716
717
718
719
720
        self.assertEqual(len(cow3_tea), 4)
        check_triple(mesh_notex, cow3_tea[:3])
        self.assertClose(cow3_tea.verts_list()[3], mesh_teapot.verts_list()[0])
        self.assertClose(cow3_tea.faces_list()[3], mesh_teapot.faces_list()[0])

Nikhila Ravi's avatar
Nikhila Ravi committed
721
722
723
        # Check error raised if all meshes in the batch don't have the same texture type
        with self.assertRaisesRegex(ValueError, "same type of texture"):
            join_meshes_as_batch([mesh_atlas, mesh_rgb, mesh_atlas])
724

725
    @staticmethod
726
    def _bm_save_obj(verts: torch.Tensor, faces: torch.Tensor, decimal_places: int):
727
728
729
        return lambda: save_obj(StringIO(), verts, faces, decimal_places)

    @staticmethod
730
    def _bm_load_obj(verts: torch.Tensor, faces: torch.Tensor, decimal_places: int):
731
732
733
734
735
736
        f = StringIO()
        save_obj(f, verts, faces, decimal_places)
        s = f.getvalue()
        # Recreate stream so it's unaffected by how it was created.
        return lambda: load_obj(StringIO(s))

facebook-github-bot's avatar
facebook-github-bot committed
737
    @staticmethod
738
    def bm_save_simple_obj_with_init(V: int, F: int):
739
740
741
        verts = torch.tensor(V * [[0.11, 0.22, 0.33]]).view(-1, 3)
        faces = torch.tensor(F * [[1, 2, 3]]).view(-1, 3)
        return TestMeshObjIO._bm_save_obj(verts, faces, decimal_places=2)
facebook-github-bot's avatar
facebook-github-bot committed
742
743

    @staticmethod
744
    def bm_load_simple_obj_with_init(V: int, F: int):
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
        verts = torch.tensor(V * [[0.1, 0.2, 0.3]]).view(-1, 3)
        faces = torch.tensor(F * [[1, 2, 3]]).view(-1, 3)
        return TestMeshObjIO._bm_load_obj(verts, faces, decimal_places=2)

    @staticmethod
    def bm_save_complex_obj(N: int):
        meshes = torus(r=0.25, R=1.0, sides=N, rings=2 * N)
        [verts], [faces] = meshes.verts_list(), meshes.faces_list()
        return TestMeshObjIO._bm_save_obj(verts, faces, decimal_places=5)

    @staticmethod
    def bm_load_complex_obj(N: int):
        meshes = torus(r=0.25, R=1.0, sides=N, rings=2 * N)
        [verts], [faces] = meshes.verts_list(), meshes.faces_list()
        return TestMeshObjIO._bm_load_obj(verts, faces, decimal_places=5)
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807

    @staticmethod
    def bm_load_texture_atlas(R: int):
        device = torch.device("cuda:0")
        torch.cuda.set_device(device)
        DATA_DIR = "/data/users/nikhilar/fbsource/fbcode/vision/fair/pytorch3d/docs/"
        obj_filename = os.path.join(DATA_DIR, "tutorials/data/cow_mesh/cow.obj")
        torch.cuda.synchronize()

        def load():
            load_obj(
                obj_filename,
                load_textures=True,
                device=device,
                create_texture_atlas=True,
                texture_atlas_size=R,
            )
            torch.cuda.synchronize()

        return load

    @staticmethod
    def bm_bilinear_sampling_vectorized(S: int, F: int, R: int):
        device = torch.device("cuda:0")
        torch.cuda.set_device(device)
        image = torch.rand((S, S, 3))
        grid = torch.rand((F, R, R, 2))
        torch.cuda.synchronize()

        def load():
            _bilinear_interpolation_vectorized(image, grid)
            torch.cuda.synchronize()

        return load

    @staticmethod
    def bm_bilinear_sampling_grid_sample(S: int, F: int, R: int):
        device = torch.device("cuda:0")
        torch.cuda.set_device(device)
        image = torch.rand((S, S, 3))
        grid = torch.rand((F, R, R, 2))
        torch.cuda.synchronize()

        def load():
            _bilinear_interpolation_grid_sample(image, grid)
            torch.cuda.synchronize()

        return load