|
|
|
|
|
|
|
|
|
|
|
|
|
import os |
|
import unittest |
|
import warnings |
|
from collections import Counter |
|
from io import StringIO |
|
from pathlib import Path |
|
from tempfile import NamedTemporaryFile, TemporaryDirectory |
|
|
|
import torch |
|
from iopath.common.file_io import PathManager |
|
from pytorch3d.io import IO, load_obj, load_objs_as_meshes, save_obj |
|
from pytorch3d.io.mtl_io import ( |
|
_bilinear_interpolation_grid_sample, |
|
_bilinear_interpolation_vectorized, |
|
_parse_mtl, |
|
) |
|
from pytorch3d.renderer import TexturesAtlas, TexturesUV, TexturesVertex |
|
from pytorch3d.structures import join_meshes_as_batch, Meshes |
|
from pytorch3d.utils import torus |
|
|
|
from .common_testing import ( |
|
get_pytorch3d_dir, |
|
get_tests_dir, |
|
load_rgb_image, |
|
TestCaseMixin, |
|
) |
|
|
|
|
|
DATA_DIR = get_tests_dir() / "data" |
|
TUTORIAL_DATA_DIR = get_pytorch3d_dir() / "docs/tutorials/data" |
|
|
|
|
|
class TestMeshObjIO(TestCaseMixin, unittest.TestCase): |
|
def test_load_obj_simple(self): |
|
obj_file = "\n".join( |
|
[ |
|
"# this is a comment", |
|
"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", |
|
"f 1 2 3", |
|
"f 1 2 4 3 1", |
|
] |
|
) |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
|
|
verts, faces, aux = load_obj(Path(f.name)) |
|
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]], |
|
dtype=torch.float32, |
|
) |
|
expected_faces = torch.tensor( |
|
[ |
|
[0, 1, 2], |
|
[0, 1, 3], |
|
[0, 3, 2], |
|
[0, 2, 0], |
|
], |
|
dtype=torch.int64, |
|
) |
|
self.assertTrue(torch.all(verts == expected_verts)) |
|
self.assertTrue(torch.all(faces.verts_idx == expected_faces)) |
|
padded_vals = -(torch.ones_like(faces.verts_idx)) |
|
self.assertTrue(torch.all(faces.normals_idx == padded_vals)) |
|
self.assertTrue(torch.all(faces.textures_idx == padded_vals)) |
|
self.assertTrue( |
|
torch.all(faces.materials_idx == -(torch.ones(len(expected_faces)))) |
|
) |
|
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", |
|
"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", |
|
"v 0.5 0.6 0.7", |
|
"vt 0.749279 0.501284 0.0", |
|
"vt 0.999110 0.501077", |
|
"vt 0.999455 0.750380", |
|
"f 1 2 3", |
|
"f 1 2 4 3 5", |
|
"f 2/1/2 3/1/2 4/2/2", |
|
"f -1 -2 1", |
|
] |
|
) |
|
|
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
|
|
verts, faces, aux = load_obj(Path(f.name)) |
|
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], |
|
[0, 1, 3], |
|
[0, 3, 2], |
|
[0, 2, 4], |
|
[1, 2, 3], |
|
[4, 3, 0], |
|
], |
|
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, |
|
) |
|
expected_faces_normals_idx = -( |
|
torch.ones_like(expected_faces, dtype=torch.int64) |
|
) |
|
expected_faces_normals_idx[4, :] = torch.tensor( |
|
[1, 1, 1], dtype=torch.int64 |
|
) |
|
expected_faces_textures_idx = -( |
|
torch.ones_like(expected_faces, dtype=torch.int64) |
|
) |
|
expected_faces_textures_idx[4, :] = torch.tensor( |
|
[0, 0, 1], dtype=torch.int64 |
|
) |
|
|
|
self.assertTrue(torch.all(verts == expected_verts)) |
|
self.assertTrue(torch.all(faces.verts_idx == expected_faces)) |
|
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) |
|
self.assertTrue(materials is None) |
|
self.assertTrue(tex_maps is None) |
|
|
|
def test_load_obj_complex_pluggable(self): |
|
""" |
|
This won't work on Windows due to the behavior of NamedTemporaryFile |
|
""" |
|
obj_file = "\n".join( |
|
[ |
|
"# this is a comment", |
|
"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", |
|
"v 0.5 0.6 0.7", |
|
"vt 0.749279 0.501284 0.0", |
|
"vt 0.999110 0.501077", |
|
"vt 0.999455 0.750380", |
|
"f 1 2 3", |
|
"f 1 2 4 3 5", |
|
"f 2/1/2 3/1/2 4/2/2", |
|
"f -1 -2 1", |
|
] |
|
) |
|
io = IO() |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
mesh = io.load_mesh(f.name) |
|
mesh_from_path = io.load_mesh(Path(f.name)) |
|
|
|
with NamedTemporaryFile(mode="w", suffix=".ply") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
with self.assertRaisesRegex(ValueError, "Invalid file header."): |
|
io.load_mesh(f.name) |
|
|
|
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], |
|
[0, 1, 3], |
|
[0, 3, 2], |
|
[0, 2, 4], |
|
[1, 2, 3], |
|
[4, 3, 0], |
|
], |
|
dtype=torch.int64, |
|
) |
|
self.assertClose(mesh.verts_padded(), expected_verts[None]) |
|
self.assertClose(mesh.faces_padded(), expected_faces[None]) |
|
self.assertClose(mesh_from_path.verts_padded(), expected_verts[None]) |
|
self.assertClose(mesh_from_path.faces_padded(), expected_faces[None]) |
|
self.assertIsNone(mesh.textures) |
|
|
|
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", |
|
] |
|
) |
|
|
|
expected_faces_normals_idx = torch.tensor([[0, 0, 1]], dtype=torch.int64) |
|
expected_normals = torch.tensor( |
|
[[0.000000, 0.000000, -1.000000], [-1.000000, -0.000000, -0.000000]], |
|
dtype=torch.float32, |
|
) |
|
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]], |
|
dtype=torch.float32, |
|
) |
|
|
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
|
|
verts, faces, aux = load_obj(Path(f.name)) |
|
normals = aux.normals |
|
textures = aux.verts_uvs |
|
materials = aux.material_colors |
|
tex_maps = aux.texture_images |
|
self.assertClose(faces.normals_idx, expected_faces_normals_idx) |
|
self.assertClose(normals, expected_normals) |
|
self.assertClose(verts, expected_verts) |
|
|
|
self.assertClose(faces.textures_idx, torch.ones_like(faces.verts_idx) * -1) |
|
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", |
|
] |
|
) |
|
|
|
expected_faces_textures_idx = torch.tensor([[0, 0, 1]], dtype=torch.int64) |
|
expected_textures = torch.tensor( |
|
[[0.999110, 0.501077], [0.999455, 0.750380]], dtype=torch.float32 |
|
) |
|
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]], |
|
dtype=torch.float32, |
|
) |
|
|
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
|
|
verts, faces, aux = load_obj(Path(f.name)) |
|
normals = aux.normals |
|
textures = aux.verts_uvs |
|
materials = aux.material_colors |
|
tex_maps = aux.texture_images |
|
|
|
self.assertClose(faces.textures_idx, expected_faces_textures_idx) |
|
self.assertClose(expected_textures, textures) |
|
self.assertClose(expected_verts, verts) |
|
self.assertTrue( |
|
torch.all(faces.normals_idx == -(torch.ones_like(faces.textures_idx))) |
|
) |
|
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"]) |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
|
|
with self.assertRaises(ValueError) as err: |
|
load_obj(Path(f.name)) |
|
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"]) |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
|
|
with self.assertRaises(ValueError) as err: |
|
load_obj(Path(f.name)) |
|
self.assertTrue("does not have 3 values" in str(err.exception)) |
|
|
|
def test_load_obj_error_vertices(self): |
|
obj_file = "\n".join(["v 1"]) |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
|
|
with self.assertRaises(ValueError) as err: |
|
load_obj(Path(f.name)) |
|
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"]) |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
|
|
with self.assertRaises(ValueError) as err: |
|
load_obj(Path(f.name)) |
|
self.assertTrue("Vertex properties are inconsistent" in str(err.exception)) |
|
|
|
def test_load_obj_error_too_many_vertex_properties(self): |
|
obj_file = "\n".join(["f 2/1/1/3"]) |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
|
|
with self.assertRaises(ValueError) as err: |
|
load_obj(Path(f.name)) |
|
self.assertTrue( |
|
"Face vertices can only have 3 properties" in str(err.exception) |
|
) |
|
|
|
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"] |
|
) |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
|
|
with self.assertWarnsRegex(UserWarning, "Faces have invalid indices"): |
|
load_obj(Path(f.name)) |
|
|
|
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", |
|
] |
|
) |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
|
|
with self.assertWarnsRegex(UserWarning, "Faces have invalid indices"): |
|
load_obj(Path(f.name)) |
|
|
|
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", |
|
] |
|
) |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
|
|
with self.assertWarnsRegex(UserWarning, "Faces have invalid indices"): |
|
load_obj(Path(f.name)) |
|
|
|
def test_save_obj_invalid_shapes(self): |
|
|
|
verts = torch.FloatTensor([[0.1, 0.2, 0.3, 0.4]]) |
|
faces = torch.LongTensor([[0, 1, 2]]) |
|
with self.assertRaises(ValueError) as error: |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
save_obj(Path(f.name), verts, faces) |
|
expected_message = ( |
|
"Argument 'verts' should either be empty or of shape (num_verts, 3)." |
|
) |
|
self.assertTrue(expected_message, error.exception) |
|
|
|
|
|
verts = torch.FloatTensor([[0.1, 0.2, 0.3]]) |
|
faces = torch.LongTensor([[0, 1, 2, 3]]) |
|
with self.assertRaises(ValueError) as error: |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
save_obj(Path(f.name), verts, faces) |
|
expected_message = ( |
|
"Argument 'faces' should either be empty or of shape (num_faces, 3)." |
|
) |
|
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): |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
save_obj(Path(f.name), verts, faces) |
|
|
|
faces = torch.LongTensor([[-1, 0, 1]]) |
|
with self.assertWarnsRegex(UserWarning, message_regex): |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
save_obj(Path(f.name), verts, faces) |
|
|
|
def _test_save_load(self, verts, faces): |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
file_path = Path(f.name) |
|
save_obj(file_path, verts, faces) |
|
f.flush() |
|
|
|
expected_verts, expected_faces = verts, faces |
|
if not len(expected_verts): |
|
expected_verts = torch.zeros(size=(0, 3), dtype=torch.float32) |
|
if not len(expected_faces): |
|
expected_faces = torch.zeros(size=(0, 3), dtype=torch.int64) |
|
actual_verts, actual_faces, _ = load_obj(file_path) |
|
self.assertClose(expected_verts, actual_verts) |
|
self.assertClose(expected_faces, actual_faces.verts_idx) |
|
|
|
def test_empty_save_load_obj(self): |
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
def test_save_obj(self): |
|
verts = torch.tensor( |
|
[[0.01, 0.2, 0.301], [0.2, 0.03, 0.408], [0.3, 0.4, 0.05], [0.6, 0.7, 0.8]], |
|
dtype=torch.float32, |
|
) |
|
faces = torch.tensor( |
|
[[0, 2, 1], [0, 1, 2], [3, 2, 1], [3, 1, 0]], dtype=torch.int64 |
|
) |
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
save_obj(Path(f.name), 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", |
|
] |
|
) |
|
self.assertEqual(Path(f.name).read_text(), expected_file) |
|
|
|
def test_load_mtl(self): |
|
obj_filename = "cow_mesh/cow.obj" |
|
filename = os.path.join(TUTORIAL_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), |
|
} |
|
} |
|
|
|
|
|
self.assertTrue(aux.texture_atlas is None) |
|
|
|
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))) |
|
) |
|
|
|
|
|
for n1, n2 in zip(materials.keys(), expected_materials.keys()): |
|
self.assertTrue(n1 == n2) |
|
for k1, k2 in zip(materials[n1].keys(), expected_materials[n2].keys()): |
|
self.assertTrue( |
|
torch.allclose(materials[n1][k1], expected_materials[n2][k2]) |
|
) |
|
|
|
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", |
|
"Kd 1.000 1.000 1.000", |
|
"Ks 0.000 0.000 0.000", |
|
"Ns 10.0", |
|
] |
|
) |
|
with NamedTemporaryFile(mode="w", suffix=".mtl") as f: |
|
f.write(mtl_file) |
|
f.flush() |
|
|
|
material_properties, texture_files = _parse_mtl( |
|
Path(f.name), path_manager=PathManager(), 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), |
|
} |
|
} |
|
|
|
self.assertTrue(tuple(texture_files.keys()) == ("material_1",)) |
|
|
|
self.assertTrue(texture_files["material_1"] == "material 1.png") |
|
|
|
|
|
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] |
|
) |
|
) |
|
|
|
def test_load_mtl_texture_atlas_compare_softras(self): |
|
|
|
device = torch.device("cuda:0") |
|
obj_filename = TUTORIAL_DATA_DIR / "cow_mesh/cow.obj" |
|
expected_atlas_fname = DATA_DIR / "cow_texture_atlas_softras.pt" |
|
|
|
|
|
|
|
if not os.path.exists(expected_atlas_fname): |
|
url = ( |
|
"https://dl.fbaipublicfiles.com/pytorch3d/data/" |
|
"tests/cow_texture_atlas_softras.pt" |
|
) |
|
msg = ( |
|
"cow_texture_atlas_softras.pt not found, download from %s, " |
|
"save it at the path %s, and rerun" % (url, expected_atlas_fname) |
|
) |
|
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) |
|
|
|
def test_load_mtl_noload(self): |
|
obj_filename = "cow_mesh/cow.obj" |
|
filename = os.path.join(TUTORIAL_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) |
|
|
|
def test_load_no_usemtl(self): |
|
obj_filename = "missing_usemtl/cow.obj" |
|
|
|
filename = os.path.join(DATA_DIR, obj_filename) |
|
|
|
mesh = IO().load_mesh(filename) |
|
self.assertIsNotNone(mesh.textures) |
|
|
|
verts, faces, aux = load_obj(filename) |
|
self.assertTrue("material_1" in aux.material_colors) |
|
self.assertTrue("material_1" in aux.texture_images) |
|
|
|
def test_load_mtl_fail(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", |
|
"usemtl material_1", |
|
"f 1 2 3", |
|
"f 1 2 4", |
|
] |
|
) |
|
|
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f: |
|
f.write(obj_file) |
|
f.flush() |
|
|
|
with self.assertWarnsRegex(UserWarning, "No mtl file provided"): |
|
verts, faces, aux = load_obj(Path(f.name)) |
|
|
|
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]], |
|
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_mtl_no_image(self): |
|
obj_filename = "obj_mtl_no_image/model.obj" |
|
filename = os.path.join(DATA_DIR, obj_filename) |
|
R = 8 |
|
verts, faces, aux = load_obj( |
|
filename, |
|
load_textures=True, |
|
create_texture_atlas=True, |
|
texture_atlas_size=R, |
|
texture_wrap=None, |
|
) |
|
|
|
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]], |
|
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)) |
|
|
|
|
|
|
|
expected_atlas = torch.tensor([0.5, 0.0, 0.0], dtype=torch.float32) |
|
expected_atlas = expected_atlas[None, None, None, :].expand(2, R, R, -1) |
|
self.assertTrue(torch.allclose(aux.texture_atlas, expected_atlas)) |
|
self.assertEqual(len(aux.material_colors.keys()), 1) |
|
self.assertEqual(list(aux.material_colors.keys()), ["material_1"]) |
|
|
|
def test_load_obj_missing_texture(self): |
|
obj_filename = "missing_files_obj/model.obj" |
|
filename = os.path.join(DATA_DIR, obj_filename) |
|
with self.assertWarnsRegex(UserWarning, "Texture file does not exist"): |
|
verts, faces, aux = load_obj(filename) |
|
|
|
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]], |
|
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)) |
|
|
|
def test_load_obj_missing_texture_noload(self): |
|
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( |
|
[[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5], [0.4, 0.5, 0.6]], |
|
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) |
|
|
|
def test_load_obj_missing_mtl(self): |
|
obj_filename = "missing_files_obj/model2.obj" |
|
filename = os.path.join(DATA_DIR, obj_filename) |
|
with self.assertWarnsRegex(UserWarning, "Mtl file does not exist"): |
|
verts, faces, aux = load_obj(filename) |
|
|
|
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]], |
|
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)) |
|
|
|
def test_load_obj_missing_mtl_noload(self): |
|
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( |
|
[[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5], [0.4, 0.5, 0.6]], |
|
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) |
|
|
|
def test_join_meshes_as_batch(self): |
|
""" |
|
Test that join_meshes_as_batch and load_objs_as_meshes are consistent |
|
with single meshes. |
|
""" |
|
|
|
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()) |
|
|
|
if mesh.textures is not None: |
|
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() |
|
) |
|
|
|
obj_filename = TUTORIAL_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) |
|
self.assertTupleEqual(mesh.textures.maps_padded().shape, (1, 1024, 1024, 3)) |
|
|
|
|
|
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")]) |
|
|
|
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) |
|
|
|
|
|
verts = torch.randn((4, 3), dtype=torch.float32) |
|
faces = torch.tensor([[2, 1, 0], [3, 1, 0]], dtype=torch.int64) |
|
vert_tex = torch.ones_like(verts) |
|
rgb_tex = TexturesVertex(verts_features=[vert_tex]) |
|
mesh_rgb = Meshes(verts=[verts], faces=[faces], textures=rgb_tex) |
|
mesh_rgb3 = join_meshes_as_batch([mesh_rgb, mesh_rgb, mesh_rgb]) |
|
check_triple(mesh_rgb, mesh_rgb3) |
|
nums_rgb = mesh_rgb.textures._num_verts_per_mesh |
|
nums_rgb3 = mesh_rgb3.textures._num_verts_per_mesh |
|
self.assertEqual(type(nums_rgb), list) |
|
self.assertEqual(type(nums_rgb3), list) |
|
self.assertListEqual(nums_rgb * 3, nums_rgb3) |
|
|
|
|
|
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) |
|
|
|
|
|
teapot_obj = TUTORIAL_DATA_DIR / "teapot.obj" |
|
mesh_teapot = load_objs_as_meshes([teapot_obj]) |
|
teapot_verts, teapot_faces = mesh_teapot.get_mesh_verts_faces(0) |
|
mix_mesh = load_objs_as_meshes([obj_filename, teapot_obj], load_textures=False) |
|
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) |
|
|
|
cow3_tea = join_meshes_as_batch([mesh3, mesh_teapot], include_textures=False) |
|
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]) |
|
|
|
|
|
with self.assertRaisesRegex(ValueError, "same type of texture"): |
|
join_meshes_as_batch([mesh_atlas, mesh_rgb, mesh_atlas]) |
|
|
|
def test_save_obj_with_normal(self): |
|
verts = torch.tensor( |
|
[[0.01, 0.2, 0.301], [0.2, 0.03, 0.408], [0.3, 0.4, 0.05], [0.6, 0.7, 0.8]], |
|
dtype=torch.float32, |
|
) |
|
faces = torch.tensor( |
|
[[0, 2, 1], [0, 1, 2], [3, 2, 1], [3, 1, 0]], dtype=torch.int64 |
|
) |
|
normals = torch.tensor( |
|
[ |
|
[0.02, 0.5, 0.73], |
|
[0.3, 0.03, 0.361], |
|
[0.32, 0.12, 0.47], |
|
[0.36, 0.17, 0.9], |
|
[0.40, 0.7, 0.19], |
|
[1.0, 0.00, 0.000], |
|
[0.00, 1.00, 0.00], |
|
[0.00, 0.00, 1.0], |
|
], |
|
dtype=torch.float32, |
|
) |
|
faces_normals_idx = torch.tensor( |
|
[[0, 1, 2], [2, 3, 4], [4, 5, 6], [6, 7, 0]], dtype=torch.int64 |
|
) |
|
|
|
with TemporaryDirectory() as temp_dir: |
|
obj_file = os.path.join(temp_dir, "mesh.obj") |
|
save_obj( |
|
obj_file, |
|
verts, |
|
faces, |
|
decimal_places=2, |
|
normals=normals, |
|
faces_normals_idx=faces_normals_idx, |
|
) |
|
|
|
expected_obj_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", |
|
"vn 0.02 0.50 0.73", |
|
"vn 0.30 0.03 0.36", |
|
"vn 0.32 0.12 0.47", |
|
"vn 0.36 0.17 0.90", |
|
"vn 0.40 0.70 0.19", |
|
"vn 1.00 0.00 0.00", |
|
"vn 0.00 1.00 0.00", |
|
"vn 0.00 0.00 1.00", |
|
"f 1//1 3//2 2//3", |
|
"f 1//3 2//4 3//5", |
|
"f 4//5 3//6 2//7", |
|
"f 4//7 2//8 1//1", |
|
] |
|
) |
|
|
|
|
|
with open(obj_file, "r") as actual_file: |
|
self.assertEqual(actual_file.read(), expected_obj_file) |
|
|
|
def test_save_obj_with_texture(self): |
|
verts = torch.tensor( |
|
[[0.01, 0.2, 0.301], [0.2, 0.03, 0.408], [0.3, 0.4, 0.05], [0.6, 0.7, 0.8]], |
|
dtype=torch.float32, |
|
) |
|
faces = torch.tensor( |
|
[[0, 2, 1], [0, 1, 2], [3, 2, 1], [3, 1, 0]], dtype=torch.int64 |
|
) |
|
verts_uvs = torch.tensor( |
|
[[0.02, 0.5], [0.3, 0.03], [0.32, 0.12], [0.36, 0.17]], |
|
dtype=torch.float32, |
|
) |
|
faces_uvs = faces |
|
texture_map = torch.randint(size=(2, 2, 3), high=255) / 255.0 |
|
|
|
with TemporaryDirectory() as temp_dir: |
|
obj_file = os.path.join(temp_dir, "mesh.obj") |
|
save_obj( |
|
obj_file, |
|
verts, |
|
faces, |
|
decimal_places=2, |
|
verts_uvs=verts_uvs, |
|
faces_uvs=faces_uvs, |
|
texture_map=texture_map, |
|
) |
|
|
|
expected_obj_file = "\n".join( |
|
[ |
|
"", |
|
"mtllib mesh.mtl", |
|
"usemtl mesh", |
|
"", |
|
"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", |
|
"vt 0.02 0.50", |
|
"vt 0.30 0.03", |
|
"vt 0.32 0.12", |
|
"vt 0.36 0.17", |
|
"f 1/1 3/3 2/2", |
|
"f 1/1 2/2 3/3", |
|
"f 4/4 3/3 2/2", |
|
"f 4/4 2/2 1/1", |
|
] |
|
) |
|
expected_mtl_file = "\n".join(["newmtl mesh", "map_Kd mesh.png", ""]) |
|
|
|
|
|
tempfiles = ["mesh.obj", "mesh.png", "mesh.mtl"] |
|
tempfiles_dir = os.listdir(temp_dir) |
|
self.assertEqual(Counter(tempfiles), Counter(tempfiles_dir)) |
|
|
|
|
|
with open(obj_file, "r") as actual_file: |
|
self.assertEqual(actual_file.read(), expected_obj_file) |
|
|
|
|
|
mtl_file_name = os.path.join(temp_dir, "mesh.mtl") |
|
with open(mtl_file_name, "r") as mtl_file: |
|
self.assertEqual(mtl_file.read(), expected_mtl_file) |
|
|
|
|
|
texture_image = load_rgb_image("mesh.png", temp_dir) |
|
self.assertClose(texture_image, texture_map) |
|
|
|
def test_save_obj_with_normal_and_texture(self): |
|
verts = torch.tensor( |
|
[[0.01, 0.2, 0.301], [0.2, 0.03, 0.408], [0.3, 0.4, 0.05], [0.6, 0.7, 0.8]], |
|
dtype=torch.float32, |
|
) |
|
faces = torch.tensor( |
|
[[0, 2, 1], [0, 1, 2], [3, 2, 1], [3, 1, 0]], dtype=torch.int64 |
|
) |
|
normals = torch.tensor( |
|
[ |
|
[0.02, 0.5, 0.73], |
|
[0.3, 0.03, 0.361], |
|
[0.32, 0.12, 0.47], |
|
[0.36, 0.17, 0.9], |
|
], |
|
dtype=torch.float32, |
|
) |
|
faces_normals_idx = faces |
|
verts_uvs = torch.tensor( |
|
[[0.02, 0.5], [0.3, 0.03], [0.32, 0.12], [0.36, 0.17]], |
|
dtype=torch.float32, |
|
) |
|
faces_uvs = faces |
|
texture_map = torch.randint(size=(2, 2, 3), high=255) / 255.0 |
|
|
|
with TemporaryDirectory() as temp_dir: |
|
obj_file = os.path.join(temp_dir, "mesh.obj") |
|
save_obj( |
|
obj_file, |
|
verts, |
|
faces, |
|
decimal_places=2, |
|
normals=normals, |
|
faces_normals_idx=faces_normals_idx, |
|
verts_uvs=verts_uvs, |
|
faces_uvs=faces_uvs, |
|
texture_map=texture_map, |
|
) |
|
|
|
expected_obj_file = "\n".join( |
|
[ |
|
"", |
|
"mtllib mesh.mtl", |
|
"usemtl mesh", |
|
"", |
|
"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", |
|
"vn 0.02 0.50 0.73", |
|
"vn 0.30 0.03 0.36", |
|
"vn 0.32 0.12 0.47", |
|
"vn 0.36 0.17 0.90", |
|
"vt 0.02 0.50", |
|
"vt 0.30 0.03", |
|
"vt 0.32 0.12", |
|
"vt 0.36 0.17", |
|
"f 1/1/1 3/3/3 2/2/2", |
|
"f 1/1/1 2/2/2 3/3/3", |
|
"f 4/4/4 3/3/3 2/2/2", |
|
"f 4/4/4 2/2/2 1/1/1", |
|
] |
|
) |
|
expected_mtl_file = "\n".join(["newmtl mesh", "map_Kd mesh.png", ""]) |
|
|
|
|
|
tempfiles = ["mesh.obj", "mesh.png", "mesh.mtl"] |
|
tempfiles_dir = os.listdir(temp_dir) |
|
self.assertEqual(Counter(tempfiles), Counter(tempfiles_dir)) |
|
|
|
|
|
with open(obj_file, "r") as actual_file: |
|
self.assertEqual(actual_file.read(), expected_obj_file) |
|
|
|
|
|
mtl_file_name = os.path.join(temp_dir, "mesh.mtl") |
|
with open(mtl_file_name, "r") as mtl_file: |
|
self.assertEqual(mtl_file.read(), expected_mtl_file) |
|
|
|
|
|
texture_image = load_rgb_image("mesh.png", temp_dir) |
|
self.assertClose(texture_image, texture_map) |
|
|
|
def test_save_obj_with_texture_errors(self): |
|
verts = torch.tensor( |
|
[[0.01, 0.2, 0.301], [0.2, 0.03, 0.408], [0.3, 0.4, 0.05], [0.6, 0.7, 0.8]], |
|
dtype=torch.float32, |
|
) |
|
faces = torch.tensor( |
|
[[0, 2, 1], [0, 1, 2], [3, 2, 1], [3, 1, 0]], dtype=torch.int64 |
|
) |
|
verts_uvs = torch.tensor( |
|
[[0.02, 0.5], [0.3, 0.03], [0.32, 0.12], [0.36, 0.17]], |
|
dtype=torch.float32, |
|
) |
|
faces_uvs = faces |
|
texture_map = torch.randint(size=(2, 2, 3), high=255) |
|
|
|
expected_obj_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", |
|
] |
|
) |
|
with TemporaryDirectory() as temp_dir: |
|
obj_file = os.path.join(temp_dir, "mesh.obj") |
|
|
|
|
|
|
|
for arg in [ |
|
{"verts_uvs": verts_uvs}, |
|
{"faces_uvs": faces_uvs}, |
|
{"texture_map": texture_map}, |
|
]: |
|
save_obj( |
|
obj_file, |
|
verts, |
|
faces, |
|
decimal_places=2, |
|
**arg, |
|
) |
|
|
|
|
|
tempfiles = ["mesh.obj"] |
|
tempfiles_dir = os.listdir(temp_dir) |
|
self.assertEqual(tempfiles, tempfiles_dir) |
|
|
|
|
|
with open(obj_file, "r") as actual_file: |
|
self.assertEqual(actual_file.read(), expected_obj_file) |
|
|
|
obj_file = StringIO() |
|
with self.assertRaises(ValueError): |
|
save_obj( |
|
obj_file, |
|
verts, |
|
faces, |
|
decimal_places=2, |
|
verts_uvs=verts_uvs, |
|
faces_uvs=faces_uvs[..., 2], |
|
texture_map=texture_map, |
|
) |
|
|
|
with self.assertRaises(ValueError): |
|
save_obj( |
|
obj_file, |
|
verts, |
|
faces, |
|
decimal_places=2, |
|
verts_uvs=verts_uvs[..., 0], |
|
faces_uvs=faces_uvs, |
|
texture_map=texture_map, |
|
) |
|
|
|
with self.assertRaises(ValueError): |
|
save_obj( |
|
obj_file, |
|
verts, |
|
faces, |
|
decimal_places=2, |
|
verts_uvs=verts_uvs, |
|
faces_uvs=faces_uvs, |
|
texture_map=texture_map[..., 1], |
|
) |
|
|
|
def test_save_obj_with_texture_IO(self): |
|
verts = torch.tensor( |
|
[[0.01, 0.2, 0.301], [0.2, 0.03, 0.408], [0.3, 0.4, 0.05], [0.6, 0.7, 0.8]], |
|
dtype=torch.float32, |
|
) |
|
faces = torch.tensor( |
|
[[0, 2, 1], [0, 1, 2], [3, 2, 1], [3, 1, 0]], dtype=torch.int64 |
|
) |
|
verts_uvs = torch.tensor( |
|
[[0.02, 0.5], [0.3, 0.03], [0.32, 0.12], [0.36, 0.17]], |
|
dtype=torch.float32, |
|
) |
|
faces_uvs = faces |
|
texture_map = torch.randint(size=(2, 2, 3), high=255) / 255.0 |
|
|
|
with TemporaryDirectory() as temp_dir: |
|
obj_file = os.path.join(temp_dir, "mesh.obj") |
|
textures_uv = TexturesUV([texture_map], [faces_uvs], [verts_uvs]) |
|
test_mesh = Meshes(verts=[verts], faces=[faces], textures=textures_uv) |
|
|
|
IO().save_mesh(data=test_mesh, path=obj_file, decimal_places=2) |
|
|
|
expected_obj_file = "\n".join( |
|
[ |
|
"", |
|
"mtllib mesh.mtl", |
|
"usemtl mesh", |
|
"", |
|
"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", |
|
"vt 0.02 0.50", |
|
"vt 0.30 0.03", |
|
"vt 0.32 0.12", |
|
"vt 0.36 0.17", |
|
"f 1/1 3/3 2/2", |
|
"f 1/1 2/2 3/3", |
|
"f 4/4 3/3 2/2", |
|
"f 4/4 2/2 1/1", |
|
] |
|
) |
|
expected_mtl_file = "\n".join(["newmtl mesh", "map_Kd mesh.png", ""]) |
|
|
|
|
|
tempfiles = ["mesh.obj", "mesh.png", "mesh.mtl"] |
|
tempfiles_dir = os.listdir(temp_dir) |
|
self.assertEqual(Counter(tempfiles), Counter(tempfiles_dir)) |
|
|
|
|
|
with open(obj_file, "r") as actual_file: |
|
self.assertEqual(actual_file.read(), expected_obj_file) |
|
|
|
|
|
mtl_file_name = os.path.join(temp_dir, "mesh.mtl") |
|
with open(mtl_file_name, "r") as mtl_file: |
|
self.assertEqual(mtl_file.read(), expected_mtl_file) |
|
|
|
|
|
texture_image = load_rgb_image("mesh.png", temp_dir) |
|
self.assertClose(texture_image, texture_map) |
|
|
|
@staticmethod |
|
def _bm_save_obj(verts: torch.Tensor, faces: torch.Tensor, decimal_places: int): |
|
return lambda: save_obj(StringIO(), verts, faces, decimal_places) |
|
|
|
@staticmethod |
|
def _bm_load_obj(verts: torch.Tensor, faces: torch.Tensor, decimal_places: int): |
|
f = StringIO() |
|
save_obj(f, verts, faces, decimal_places) |
|
s = f.getvalue() |
|
|
|
return lambda: load_obj(StringIO(s)) |
|
|
|
@staticmethod |
|
def bm_save_simple_obj_with_init(V: int, F: int): |
|
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) |
|
|
|
@staticmethod |
|
def bm_load_simple_obj_with_init(V: int, F: int): |
|
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) |
|
|
|
@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 |
|
|