|
|
|
|
|
|
|
|
|
|
|
|
|
import unittest |
|
from tempfile import NamedTemporaryFile |
|
|
|
import torch |
|
from pytorch3d.io import IO |
|
from pytorch3d.renderer import TexturesAtlas, TexturesVertex |
|
from pytorch3d.utils import ico_sphere |
|
|
|
from .common_testing import TestCaseMixin |
|
|
|
|
|
CUBE_FACES = [ |
|
[0, 1, 2], |
|
[7, 4, 0], |
|
[4, 5, 1], |
|
[5, 6, 2], |
|
[3, 2, 6], |
|
[6, 5, 4], |
|
[0, 2, 3], |
|
[7, 0, 3], |
|
[4, 1, 0], |
|
[5, 2, 1], |
|
[3, 6, 7], |
|
[6, 4, 7], |
|
] |
|
|
|
|
|
class TestMeshOffIO(TestCaseMixin, unittest.TestCase): |
|
def test_load_face_colors(self): |
|
|
|
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) |
|
|
|
|
|
total_color = faces_colors.sum(dim=1) |
|
self.assertEqual(total_color.max(), max_color) |
|
self.assertEqual(total_color.min(), max_color) |
|
|
|
def test_load_vertex_colors(self): |
|
|
|
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): |
|
|
|
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_save_load_icosphere(self): |
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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 |
|
) |
|
|
|
|
|
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_bad(self): |
|
|
|
io = IO() |
|
|
|
def load(lines): |
|
off_file = "\n".join(lines) |
|
with NamedTemporaryFile(mode="w", suffix=".off") as f: |
|
f.write(off_file) |
|
f.flush() |
|
io.load_mesh(f.name) |
|
|
|
|
|
lines = [ |
|
"4 2 12", |
|
" 1.0 0.0 1.4142", |
|
" 0.0 1.0 1.4142", |
|
" 1.0 0.0 0.4142", |
|
" 0.0 1.0 0.4142", |
|
"3 0 1 2 ", |
|
"3 1 3 0 ", |
|
] |
|
|
|
|
|
load(lines) |
|
|
|
|
|
load(["OFF"] + lines) |
|
|
|
|
|
lines2 = lines.copy() |
|
lines2[0] = "OFF " + lines[0] |
|
load(lines2) |
|
|
|
|
|
lines2 = lines.copy() |
|
lines2[0] = "OFF" + lines[0] |
|
load(lines2) |
|
|
|
with self.assertRaisesRegex(ValueError, "Not enough face data."): |
|
load(lines[:-1]) |
|
|
|
lines2 = lines.copy() |
|
lines2[0] = "4 1 12" |
|
with self.assertRaisesRegex(ValueError, "Extra data at end of file:"): |
|
load(lines2) |
|
|
|
lines2 = lines.copy() |
|
lines2[-1] = "2 1 3" |
|
with self.assertRaisesRegex(ValueError, "Faces must have at least 3 vertices."): |
|
load(lines2) |
|
|
|
lines2 = lines.copy() |
|
lines2[-1] = "4 1 3 0" |
|
with self.assertRaisesRegex( |
|
ValueError, "A line of face data did not have the specified length." |
|
): |
|
load(lines2) |
|
|
|
lines2 = lines.copy() |
|
lines2[0] = "6 2 0" |
|
with self.assertRaisesRegex(ValueError, "Wrong number of columns at line 5"): |
|
load(lines2) |
|
|
|
lines2[0] = "5 1 0" |
|
with self.assertRaisesRegex(ValueError, "Wrong number of columns at line 5"): |
|
load(lines2) |
|
|
|
lines2[0] = "16 2 0" |
|
with self.assertRaisesRegex(ValueError, "Wrong number of columns at line 5"): |
|
load(lines2) |
|
|
|
lines2[0] = "3 3 0" |
|
|
|
with self.assertRaisesRegex(ValueError, "Faces must have at least 3 vertices."): |
|
load(lines2) |
|
|
|
lines2[4] = "7.3 4.2 8.3" |
|
with self.assertRaisesRegex( |
|
ValueError, "A line of face data did not have the specified length." |
|
): |
|
load(lines2) |
|
|
|
|
|
|
|
lines2 = lines.copy() |
|
lines2[2] = "7.3 4.2 8.3 932" |
|
with self.assertRaisesRegex(ValueError, "Wrong number of columns at line 2"): |
|
load(lines2) |
|
|
|
lines2[1] = "7.3 4.2 8.3 932" |
|
lines2[3] = "7.3 4.2 8.3 932" |
|
lines2[4] = "7.3 4.2 8.3 932" |
|
with self.assertRaisesRegex(ValueError, "Bad vertex data."): |
|
load(lines2) |
|
|
|
lines2 = lines.copy() |
|
lines2[5] = "3 0 1 2 0.9" |
|
lines2[6] = "3 0 3 0 0.9" |
|
with self.assertRaisesRegex(ValueError, "Unexpected number of colors."): |
|
load(lines2) |
|
|
|
lines2 = lines.copy() |
|
for i in range(1, 7): |
|
lines2[i] = lines2[i] + " 4 4 4 4" |
|
msg = "Faces colors ignored because vertex colors provided too." |
|
with self.assertWarnsRegex(UserWarning, msg): |
|
load(lines2) |
|
|