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:
Jeremy Reizenstein
2021-01-07 15:38:49 -08:00
committed by Facebook GitHub Bot
parent b183dcb6e8
commit 89532a876e
5 changed files with 271 additions and 65 deletions

View File

@@ -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"

View File

@@ -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