Skip to content

Commit f5383a7

Browse files
nikhilaravifacebook-github-bot
authored andcommitted
Fix texture atlas for objs which only have material properties
Summary: Fix for GitHub issue #381. The example mesh provided in the issue only had material properties but no texture image. The current implementation of texture atlassing generated an atlas using both the material properties and the texture image but only worked if there was a texture image and associated vertex uv coordinates. I have now modified the texture atlas creation so that it doesn't require an image and can work with materials which only have material properties. Reviewed By: gkioxari Differential Revision: D24153068 fbshipit-source-id: 63e9d325db09a84b336b83369d5342ce588a9932
1 parent 5d65a0c commit f5383a7

File tree

5 files changed

+87
-37
lines changed

5 files changed

+87
-37
lines changed

pytorch3d/io/mtl_io.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ def make_mesh_texture_atlas(
1515
material_properties: Dict,
1616
texture_images: Dict,
1717
face_material_names,
18-
faces_verts_uvs: torch.Tensor,
18+
faces_uvs: torch.Tensor,
19+
verts_uvs: torch.Tensor,
1920
texture_size: int,
2021
texture_wrap: Optional[str],
2122
) -> torch.Tensor:
@@ -31,8 +32,9 @@ def make_mesh_texture_atlas(
3132
face_material_names: numpy array of the material name corresponding to each
3233
face. Faces which don't have an associated material will be an empty string.
3334
For these faces, a uniform white texture is assigned.
34-
faces_verts_uvs: LongTensor of shape (F, 3, 2) giving the uv coordinates for each
35-
vertex in the face.
35+
faces_uvs: LongTensor of shape (F, 3,) giving the index into the verts_uvs for
36+
each face in the mesh.
37+
verts_uvs: FloatTensor of shape (V, 2) giving the uv coordinates for each vertex.
3638
texture_size: the resolution of the per face texture map returned by this function.
3739
Each face will have a texture map of shape (texture_size, texture_size, 3).
3840
texture_wrap: string, one of ["repeat", "clamp", None]
@@ -47,50 +49,55 @@ def make_mesh_texture_atlas(
4749
"""
4850
# Create an R x R texture map per face in the mesh
4951
R = texture_size
50-
F = faces_verts_uvs.shape[0]
52+
F = faces_uvs.shape[0]
5153

5254
# Initialize the per face texture map to a white color.
5355
# TODO: allow customization of this base color?
54-
# pyre-fixme[16]: `Tensor` has no attribute `new_ones`.
55-
atlas = faces_verts_uvs.new_ones(size=(F, R, R, 3))
56+
atlas = torch.ones(size=(F, R, R, 3), dtype=torch.float32, device=faces_uvs.device)
5657

5758
# Check for empty materials.
5859
if not material_properties and not texture_images:
5960
return atlas
6061

62+
# Iterate through the material properties - not
63+
# all materials have texture images so this is
64+
# done first separately to the texture interpolation.
65+
for material_name, props in material_properties.items():
66+
# Bool to indicate which faces use this texture map.
67+
faces_material_ind = torch.from_numpy(face_material_names == material_name).to(
68+
faces_uvs.device
69+
)
70+
if faces_material_ind.sum() > 0:
71+
# For these faces, update the base color to the
72+
# diffuse material color.
73+
if "diffuse_color" not in props:
74+
continue
75+
atlas[faces_material_ind, ...] = props["diffuse_color"][None, :]
76+
77+
# If there are vertex texture coordinates, create an (F, 3, 2)
78+
# tensor of the vertex textures per face.
79+
faces_verts_uvs = verts_uvs[faces_uvs] if len(verts_uvs) > 0 else None
80+
81+
# Some meshes only have material properties and no texture image.
82+
# In this case, return the atlas here.
83+
if faces_verts_uvs is None:
84+
return atlas
85+
6186
if texture_wrap == "repeat":
6287
# If texture uv coordinates are outside the range [0, 1] follow
6388
# the convention GL_REPEAT in OpenGL i.e the integer part of the coordinate
6489
# will be ignored and a repeating pattern is formed.
6590
# Shapenet data uses this format see:
6691
# https://shapenet.org/qaforum/index.php?qa=15&qa_1=why-is-the-texture-coordinate-in-the-obj-file-not-in-the-range # noqa: B950
67-
# pyre-fixme[16]: `ByteTensor` has no attribute `any`.
6892
if (faces_verts_uvs > 1).any() or (faces_verts_uvs < 0).any():
6993
msg = "Texture UV coordinates outside the range [0, 1]. \
7094
The integer part will be ignored to form a repeating pattern."
7195
warnings.warn(msg)
72-
# pyre-fixme[9]: faces_verts_uvs has type `Tensor`; used as `int`.
73-
# pyre-fixme[58]: `%` is not supported for operand types `Tensor` and `int`.
7496
faces_verts_uvs = faces_verts_uvs % 1
7597
elif texture_wrap == "clamp":
7698
# Clamp uv coordinates to the [0, 1] range.
7799
faces_verts_uvs = faces_verts_uvs.clamp(0.0, 1.0)
78100

79-
# Iterate through the material properties - not
80-
# all materials have texture images so this has to be
81-
# done separately to the texture interpolation.
82-
for material_name, props in material_properties.items():
83-
# Bool to indicate which faces use this texture map.
84-
faces_material_ind = torch.from_numpy(face_material_names == material_name).to(
85-
faces_verts_uvs.device
86-
)
87-
if faces_material_ind.sum() > 0:
88-
# For these faces, update the base color to the
89-
# diffuse material color.
90-
if "diffuse_color" not in props:
91-
continue
92-
atlas[faces_material_ind, ...] = props["diffuse_color"][None, :]
93-
94101
# Iterate through the materials used in this mesh. Update the
95102
# texture atlas for the faces which use this material.
96103
# Faces without texture are white.

pytorch3d/io/obj_io.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -533,19 +533,16 @@ def _load_obj(
533533
face_material_names = np.array(material_names)[idx] # (F,)
534534
face_material_names[idx == -1] = ""
535535

536-
if len(verts_uvs) > 0:
537-
# Get the uv coords for each vert in each face
538-
faces_verts_uvs = verts_uvs[faces_textures_idx] # (F, 3, 2)
539-
540-
# Construct the atlas.
541-
texture_atlas = make_mesh_texture_atlas(
542-
material_colors,
543-
texture_images,
544-
face_material_names,
545-
faces_verts_uvs,
546-
texture_atlas_size,
547-
texture_wrap,
548-
)
536+
# Construct the atlas.
537+
texture_atlas = make_mesh_texture_atlas(
538+
material_colors,
539+
texture_images,
540+
face_material_names,
541+
faces_textures_idx,
542+
verts_uvs,
543+
texture_atlas_size,
544+
texture_wrap,
545+
)
549546

550547
faces = _Faces(
551548
verts_idx=faces_verts_idx,

tests/data/obj_mtl_no_image/model.mtl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Material Count: 1
2+
3+
newmtl material_1
4+
Ns 96.078431
5+
Ka 0.000000 0.000000 0.000000
6+
Kd 0.500000 0.000000 0.000000
7+
Ks 0.500000 0.500000 0.500000

tests/data/obj_mtl_no_image/model.obj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
mtllib model.mtl
3+
4+
v 0.1 0.2 0.3
5+
v 0.2 0.3 0.4
6+
v 0.3 0.4 0.5
7+
v 0.4 0.5 0.6
8+
usemtl material_1
9+
f 1 2 3
10+
f 1 2 4

tests/test_obj_io.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,35 @@ def test_load_mtl_fail(self):
559559
self.assertTrue(aux.normals is None)
560560
self.assertTrue(aux.verts_uvs is None)
561561

562+
def test_load_obj_mlt_no_image(self):
563+
DATA_DIR = Path(__file__).resolve().parent / "data"
564+
obj_filename = "obj_mtl_no_image/model.obj"
565+
filename = os.path.join(DATA_DIR, obj_filename)
566+
R = 8
567+
verts, faces, aux = load_obj(
568+
filename,
569+
load_textures=True,
570+
create_texture_atlas=True,
571+
texture_atlas_size=R,
572+
texture_wrap=None,
573+
)
574+
575+
expected_verts = torch.tensor(
576+
[[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5], [0.4, 0.5, 0.6]],
577+
dtype=torch.float32,
578+
)
579+
expected_faces = torch.tensor([[0, 1, 2], [0, 1, 3]], dtype=torch.int64)
580+
self.assertTrue(torch.allclose(verts, expected_verts))
581+
self.assertTrue(torch.allclose(faces.verts_idx, expected_faces))
582+
583+
# Check that the material diffuse color has been assigned to all the
584+
# values in the texture atlas.
585+
expected_atlas = torch.tensor([0.5, 0.0, 0.0], dtype=torch.float32)
586+
expected_atlas = expected_atlas[None, None, None, :].expand(2, R, R, -1)
587+
self.assertTrue(torch.allclose(aux.texture_atlas, expected_atlas))
588+
self.assertEquals(len(aux.material_colors.keys()), 1)
589+
self.assertEquals(list(aux.material_colors.keys()), ["material_1"])
590+
562591
def test_load_obj_missing_texture(self):
563592
DATA_DIR = Path(__file__).resolve().parent / "data"
564593
obj_filename = "missing_files_obj/model.obj"

0 commit comments

Comments
 (0)