mirror of
https://github.com/facebookresearch/pytorch3d.git
synced 2025-12-19 22:00:35 +08:00
add existing mesh formats to pluggable
Summary: We already have code for obj and ply formats. Here we actually make it available in `IO.load_mesh` and `IO.save_mesh`. Reviewed By: theschnitz, nikhilaravi Differential Revision: D25400650 fbshipit-source-id: f26d6d7fc46c48634a948eea4d255afad13b807b
This commit is contained in:
committed by
Facebook GitHub Bot
parent
b183dcb6e8
commit
89532a876e
@@ -5,11 +5,12 @@ import unittest
|
||||
import warnings
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import torch
|
||||
from common_testing import TestCaseMixin
|
||||
from iopath.common.file_io import PathManager
|
||||
from pytorch3d.io import load_obj, load_objs_as_meshes, save_obj
|
||||
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,
|
||||
@@ -145,6 +146,70 @@ class TestMeshObjIO(TestCaseMixin, unittest.TestCase):
|
||||
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", # 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 test_load_obj_normals_only(self):
|
||||
obj_file = "\n".join(
|
||||
[
|
||||
@@ -588,8 +653,8 @@ class TestMeshObjIO(TestCaseMixin, unittest.TestCase):
|
||||
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.assertEquals(len(aux.material_colors.keys()), 1)
|
||||
self.assertEquals(list(aux.material_colors.keys()), ["material_1"])
|
||||
self.assertEqual(len(aux.material_colors.keys()), 1)
|
||||
self.assertEqual(list(aux.material_colors.keys()), ["material_1"])
|
||||
|
||||
def test_load_obj_missing_texture(self):
|
||||
DATA_DIR = Path(__file__).resolve().parent / "data"
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import struct
|
||||
import unittest
|
||||
from io import BytesIO, StringIO
|
||||
from tempfile import TemporaryFile
|
||||
from tempfile import NamedTemporaryFile, TemporaryFile
|
||||
|
||||
import pytorch3d.io.ply_io
|
||||
import torch
|
||||
from common_testing import TestCaseMixin
|
||||
from iopath.common.file_io import PathManager
|
||||
from pytorch3d.io import IO
|
||||
from pytorch3d.io.ply_io import load_ply, save_ply
|
||||
from pytorch3d.utils import torus
|
||||
|
||||
@@ -20,6 +21,60 @@ def _load_ply_raw(stream):
|
||||
return pytorch3d.io.ply_io._load_ply_raw(stream, global_path_manager)
|
||||
|
||||
|
||||
CUBE_PLY_LINES = [
|
||||
"ply",
|
||||
"format ascii 1.0",
|
||||
"comment made by Greg Turk",
|
||||
"comment this file is a cube",
|
||||
"element vertex 8",
|
||||
"property float x",
|
||||
"property float y",
|
||||
"property float z",
|
||||
"element face 6",
|
||||
"property list uchar int vertex_index",
|
||||
"end_header",
|
||||
"0 0 0",
|
||||
"0 0 1",
|
||||
"0 1 1",
|
||||
"0 1 0",
|
||||
"1 0 0",
|
||||
"1 0 1",
|
||||
"1 1 1",
|
||||
"1 1 0",
|
||||
"4 0 1 2 3",
|
||||
"4 7 6 5 4",
|
||||
"4 0 4 5 1",
|
||||
"4 1 5 6 2",
|
||||
"4 2 6 7 3",
|
||||
"4 3 7 4 0",
|
||||
]
|
||||
|
||||
CUBE_VERTS = [
|
||||
[0, 0, 0],
|
||||
[0, 0, 1],
|
||||
[0, 1, 1],
|
||||
[0, 1, 0],
|
||||
[1, 0, 0],
|
||||
[1, 0, 1],
|
||||
[1, 1, 1],
|
||||
[1, 1, 0],
|
||||
]
|
||||
CUBE_FACES = [
|
||||
[0, 1, 2],
|
||||
[7, 6, 5],
|
||||
[0, 4, 5],
|
||||
[1, 5, 6],
|
||||
[2, 6, 7],
|
||||
[3, 7, 4],
|
||||
[0, 2, 3],
|
||||
[7, 5, 4],
|
||||
[0, 5, 1],
|
||||
[1, 6, 2],
|
||||
[2, 7, 3],
|
||||
[3, 4, 0],
|
||||
]
|
||||
|
||||
|
||||
class TestMeshPlyIO(TestCaseMixin, unittest.TestCase):
|
||||
def test_raw_load_simple_ascii(self):
|
||||
ply_file = "\n".join(
|
||||
@@ -82,35 +137,7 @@ class TestMeshPlyIO(TestCaseMixin, unittest.TestCase):
|
||||
self.assertClose(x, [4, 5, 1])
|
||||
|
||||
def test_load_simple_ascii(self):
|
||||
ply_file = "\n".join(
|
||||
[
|
||||
"ply",
|
||||
"format ascii 1.0",
|
||||
"comment made by Greg Turk",
|
||||
"comment this file is a cube",
|
||||
"element vertex 8",
|
||||
"property float x",
|
||||
"property float y",
|
||||
"property float z",
|
||||
"element face 6",
|
||||
"property list uchar int vertex_index",
|
||||
"end_header",
|
||||
"0 0 0",
|
||||
"0 0 1",
|
||||
"0 1 1",
|
||||
"0 1 0",
|
||||
"1 0 0",
|
||||
"1 0 1",
|
||||
"1 1 1",
|
||||
"1 1 0",
|
||||
"4 0 1 2 3",
|
||||
"4 7 6 5 4",
|
||||
"4 0 4 5 1",
|
||||
"4 1 5 6 2",
|
||||
"4 2 6 7 3",
|
||||
"4 3 7 4 0",
|
||||
]
|
||||
)
|
||||
ply_file = "\n".join(CUBE_PLY_LINES)
|
||||
for line_ending in [None, "\n", "\r\n"]:
|
||||
if line_ending is None:
|
||||
stream = StringIO(ply_file)
|
||||
@@ -122,32 +149,41 @@ class TestMeshPlyIO(TestCaseMixin, unittest.TestCase):
|
||||
verts, faces = load_ply(stream)
|
||||
self.assertEqual(verts.shape, (8, 3))
|
||||
self.assertEqual(faces.shape, (12, 3))
|
||||
verts_expected = [
|
||||
[0, 0, 0],
|
||||
[0, 0, 1],
|
||||
[0, 1, 1],
|
||||
[0, 1, 0],
|
||||
[1, 0, 0],
|
||||
[1, 0, 1],
|
||||
[1, 1, 1],
|
||||
[1, 1, 0],
|
||||
]
|
||||
self.assertClose(verts, torch.FloatTensor(verts_expected))
|
||||
faces_expected = [
|
||||
[0, 1, 2],
|
||||
[7, 6, 5],
|
||||
[0, 4, 5],
|
||||
[1, 5, 6],
|
||||
[2, 6, 7],
|
||||
[3, 7, 4],
|
||||
[0, 2, 3],
|
||||
[7, 5, 4],
|
||||
[0, 5, 1],
|
||||
[1, 6, 2],
|
||||
[2, 7, 3],
|
||||
[3, 4, 0],
|
||||
]
|
||||
self.assertClose(faces, torch.LongTensor(faces_expected))
|
||||
self.assertClose(verts, torch.FloatTensor(CUBE_VERTS))
|
||||
self.assertClose(faces, torch.LongTensor(CUBE_FACES))
|
||||
|
||||
def test_pluggable_load_cube(self):
|
||||
"""
|
||||
This won't work on Windows due to NamedTemporaryFile being reopened.
|
||||
"""
|
||||
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_save_ply_invalid_shapes(self):
|
||||
# Invalid vertices shape
|
||||
|
||||
Reference in New Issue
Block a user