def test_pluggable_load_cube(self): """ This won't work on Windows due to NamedTemporaryFile being reopened. Use the testpath package instead? """ ply_file = "\n".join(CUBE_PLY_LINES) io = IO() with NamedTemporaryFile(mode="w", suffix=".ply") as f: f.write(ply_file) f.flush() mesh = io.load_mesh(f.name) self.assertClose(mesh.verts_padded(), torch.FloatTensor(CUBE_VERTS)[None]) self.assertClose(mesh.faces_padded(), torch.LongTensor(CUBE_FACES)[None]) device = torch.device("cuda:0") with NamedTemporaryFile(mode="w", suffix=".ply") as f2: io.save_mesh(mesh, f2.name) f2.flush() mesh2 = io.load_mesh(f2.name, device=device) self.assertEqual(mesh2.verts_padded().device, device) self.assertClose(mesh2.verts_padded().cpu(), mesh.verts_padded()) self.assertClose(mesh2.faces_padded().cpu(), mesh.faces_padded()) with NamedTemporaryFile(mode="w") as f3: with self.assertRaisesRegex( ValueError, "No mesh interpreter found to write to"): io.save_mesh(mesh, f3.name) with self.assertRaisesRegex(ValueError, "No mesh interpreter found to read "): io.load_mesh(f3.name)
def test_load_vertex_colors(self): # Example with no faces and with integer vertex colors off_file_lines = [ "8 1 12", " 1.0 0.0 1.4142 0 1 0", " 0.0 1.0 1.4142 0 1 0", "-1.0 0.0 1.4142 0 1 0", " 0.0 -1.0 1.4142 0 1 0", " 1.0 0.0 0.0 0 1 0", " 0.0 1.0 0.0 0 1 0", "-1.0 0.0 0.0 0 1 0", " 0.0 -1.0 0.0 0 1 0", "3 0 1 2", ] off_file = "\n".join(off_file_lines) io = IO() with NamedTemporaryFile(mode="w", suffix=".off") as f: f.write(off_file) f.flush() mesh = io.load_mesh(f.name) self.assertEqual(mesh.verts_padded().shape, (1, 8, 3)) verts_lines = (line.split()[:3] for line in off_file_lines[1:9]) verts_data = [[[float(x) for x in line] for line in verts_lines]] self.assertClose(mesh.verts_padded(), torch.tensor(verts_data)) self.assertClose(mesh.faces_padded(), torch.tensor([[[0, 1, 2]]])) self.assertIsInstance(mesh.textures, TexturesVertex) colors = mesh.textures.verts_features_padded() self.assertEqual(colors.shape, (1, 8, 3)) self.assertClose(colors[0, :, [0, 2]], torch.zeros(8, 2)) self.assertClose(colors[0, :, 1], torch.full((8, ), 1.0 / 255))
def test_load_lumpy(self): # Example off file whose faces have different numbers of vertices. off_file_lines = [ "8 3 12", " 1.0 0.0 1.4142", " 0.0 1.0 1.4142", "-1.0 0.0 1.4142", " 0.0 -1.0 1.4142", " 1.0 0.0 0.0", " 0.0 1.0 0.0", "-1.0 0.0 0.0", " 0.0 -1.0 0.0", "3 0 1 2 255 0 0 #red", "4 7 4 0 3 0 255 0 #green", "4 4 5 1 0 0 0 255 #blue", ] off_file = "\n".join(off_file_lines) io = IO() with NamedTemporaryFile(mode="w", suffix=".off") as f: f.write(off_file) f.flush() mesh = io.load_mesh(f.name) self.assertEqual(mesh.verts_padded().shape, (1, 8, 3)) verts_str = " ".join(off_file_lines[1:9]) verts_data = torch.tensor([float(i) for i in verts_str.split()]) self.assertClose(mesh.verts_padded().flatten(), verts_data) self.assertEqual(mesh.faces_padded().shape, (1, 5, 3)) faces_expected = [[0, 1, 2], [7, 4, 0], [7, 0, 3], [4, 5, 1], [4, 1, 0]] self.assertClose(mesh.faces_padded()[0], torch.tensor(faces_expected))
def test_load_face_colors(self): # Example from wikipedia off_file_lines = [ "OFF", "# cube.off", "# A cube", " ", "8 6 12", " 1.0 0.0 1.4142", " 0.0 1.0 1.4142", "-1.0 0.0 1.4142", " 0.0 -1.0 1.4142", " 1.0 0.0 0.0", " 0.0 1.0 0.0", "-1.0 0.0 0.0", " 0.0 -1.0 0.0", "4 0 1 2 3 255 0 0 #red", "4 7 4 0 3 0 255 0 #green", "4 4 5 1 0 0 0 255 #blue", "4 5 6 2 1 0 255 0 ", "4 3 2 6 7 0 0 255", "4 6 5 4 7 255 0 0", ] off_file = "\n".join(off_file_lines) io = IO() with NamedTemporaryFile(mode="w", suffix=".off") as f: f.write(off_file) f.flush() mesh = io.load_mesh(f.name) self.assertEqual(mesh.verts_padded().shape, (1, 8, 3)) verts_str = " ".join(off_file_lines[5:13]) verts_data = torch.tensor([float(i) for i in verts_str.split()]) self.assertClose(mesh.verts_padded().flatten(), verts_data) self.assertClose(mesh.faces_padded(), torch.tensor(CUBE_FACES)[None]) faces_colors_full = mesh.textures.atlas_padded() self.assertEqual(faces_colors_full.shape, (1, 12, 1, 1, 3)) faces_colors = faces_colors_full[0, :, 0, 0] max_color = faces_colors.max() self.assertEqual(max_color, 1) # Every face has one color 1, the rest 0. total_color = faces_colors.sum(dim=1) self.assertEqual(total_color.max(), max_color) self.assertEqual(total_color.min(), max_color)
def test_save_load_meshes(self): verts = torch.tensor([[0, 0, 0], [0, 0, 1], [0, 1, 0], [1, 0, 0]], dtype=torch.float32) faces = torch.tensor([[0, 1, 2], [0, 2, 3]]) normals = torch.tensor([[0, 1, 0], [1, 0, 0], [1, 4, 1], [1, 0, 0]], dtype=torch.float32) vert_colors = torch.rand_like(verts) texture = TexturesVertex(verts_features=[vert_colors]) for do_textures, do_normals in itertools.product([True, False], [True, False]): mesh = Meshes( verts=[verts], faces=[faces], textures=texture if do_textures else None, verts_normals=[normals] if do_normals else None, ) device = torch.device("cuda:0") io = IO() with NamedTemporaryFile(mode="w", suffix=".ply") as f: io.save_mesh(mesh.cuda(), f.name) f.flush() mesh2 = io.load_mesh(f.name, device=device) self.assertEqual(mesh2.device, device) mesh2 = mesh2.cpu() self.assertClose(mesh2.verts_padded(), mesh.verts_padded()) self.assertClose(mesh2.faces_padded(), mesh.faces_padded()) if do_normals: self.assertTrue(mesh.has_verts_normals()) self.assertTrue(mesh2.has_verts_normals()) self.assertClose(mesh2.verts_normals_padded(), mesh.verts_normals_padded()) else: self.assertFalse(mesh.has_verts_normals()) self.assertFalse(mesh2.has_verts_normals()) self.assertFalse( torch.allclose(mesh2.verts_normals_padded(), normals)) if do_textures: self.assertIsInstance(mesh2.textures, TexturesVertex) self.assertClose(mesh2.textures.verts_features_list()[0], vert_colors) else: self.assertIsNone(mesh2.textures)
def test_load_open3d_mesh(self): # Header based on issue #1104 header = "\n".join([ "ply", "format binary_little_endian 1.0", "comment Created by Open3D", "element vertex 3", "property double x", "property double y", "property double z", "property double nx", "property double ny", "property double nz", "property uchar red", "property uchar green", "property uchar blue", "element face 1", "property list uchar uint vertex_indices", "end_header", "", ]).encode("ascii") vert_data = struct.pack("<" + "ddddddBBB" * 3, *range(9 * 3)) face_data = struct.pack("<" + "BIII", 3, 0, 1, 2) io = IO() with NamedTemporaryFile(mode="wb", suffix=".ply") as f: f.write(header) f.write(vert_data) f.write(face_data) f.flush() mesh = io.load_mesh(f.name) self.assertClose(mesh.faces_padded(), torch.arange(3)[None, None]) self.assertClose( mesh.verts_padded(), (torch.arange(3) + 9.0 * torch.arange(3)[:, None])[None], )
def test_save_load_icosphere(self): # Test that saving a mesh as an off file and loading it results in the # same data on the correct device, for all permitted types of textures. # Standard test is for random colors, but also check totally white, # because there's a different in OFF semantics between "1.0" color (=full) # and "1" (= 1/255 color) sphere = ico_sphere(0) io = IO() device = torch.device("cuda:0") atlas_padded = torch.rand(1, sphere.faces_list()[0].shape[0], 1, 1, 3) atlas = TexturesAtlas(atlas_padded) atlas_padded_white = torch.ones(1, sphere.faces_list()[0].shape[0], 1, 1, 3) atlas_white = TexturesAtlas(atlas_padded_white) verts_colors_padded = torch.rand(1, sphere.verts_list()[0].shape[0], 3) vertex_texture = TexturesVertex(verts_colors_padded) verts_colors_padded_white = torch.ones(1, sphere.verts_list()[0].shape[0], 3) vertex_texture_white = TexturesVertex(verts_colors_padded_white) # No colors case with NamedTemporaryFile(mode="w", suffix=".off") as f: io.save_mesh(sphere, f.name) f.flush() mesh1 = io.load_mesh(f.name, device=device) self.assertEqual(mesh1.device, device) mesh1 = mesh1.cpu() self.assertClose(mesh1.verts_padded(), sphere.verts_padded()) self.assertClose(mesh1.faces_padded(), sphere.faces_padded()) self.assertIsNone(mesh1.textures) # Atlas case sphere.textures = atlas with NamedTemporaryFile(mode="w", suffix=".off") as f: io.save_mesh(sphere, f.name) f.flush() mesh2 = io.load_mesh(f.name, device=device) self.assertEqual(mesh2.device, device) mesh2 = mesh2.cpu() self.assertClose(mesh2.verts_padded(), sphere.verts_padded()) self.assertClose(mesh2.faces_padded(), sphere.faces_padded()) self.assertClose(mesh2.textures.atlas_padded(), atlas_padded, atol=1e-4) # White atlas case sphere.textures = atlas_white with NamedTemporaryFile(mode="w", suffix=".off") as f: io.save_mesh(sphere, f.name) f.flush() mesh3 = io.load_mesh(f.name) self.assertClose(mesh3.textures.atlas_padded(), atlas_padded_white, atol=1e-4) # TexturesVertex case sphere.textures = vertex_texture with NamedTemporaryFile(mode="w", suffix=".off") as f: io.save_mesh(sphere, f.name) f.flush() mesh4 = io.load_mesh(f.name, device=device) self.assertEqual(mesh4.device, device) mesh4 = mesh4.cpu() self.assertClose(mesh4.verts_padded(), sphere.verts_padded()) self.assertClose(mesh4.faces_padded(), sphere.faces_padded()) self.assertClose(mesh4.textures.verts_features_padded(), verts_colors_padded, atol=1e-4) # white TexturesVertex case sphere.textures = vertex_texture_white with NamedTemporaryFile(mode="w", suffix=".off") as f: io.save_mesh(sphere, f.name) f.flush() mesh5 = io.load_mesh(f.name) self.assertClose(mesh5.textures.verts_features_padded(), verts_colors_padded_white, atol=1e-4)
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", # 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. ]) 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], # 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, ) 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 _load(path, **kwargs) -> Meshes: io = IO() io.register_meshes_format(MeshGlbFormat()) return io.load_mesh(path, **kwargs)