Skip to content

Commit 909dc83

Browse files
bottlerfacebook-github-bot
authored andcommitted
amalgamate meshes with texture into a single scene
Summary: Add a join_scene method to all the textures to allow the join_mesh function to include textures. Rename the join_mesh function to join_meshes_as_scene. For TexturesAtlas, we now interpolate if the user attempts to have the resolution vary across the batch. This doesn't look great if the resolution is already very low. For TexturesUV, a rectangle packing function is required, this does something simple. Reviewed By: gkioxari Differential Revision: D23188773 fbshipit-source-id: c013db061a04076e13e90ccc168a7913e933a9c5
1 parent e25ccab commit 909dc83

14 files changed

+741
-23
lines changed

pytorch3d/renderer/mesh/textures.py

Lines changed: 167 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
import itertools
44
import warnings
5-
from typing import Dict, List, Optional, Tuple, Union
5+
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
66

77
import torch
88
import torch.nn.functional as F
99
from pytorch3d.ops import interpolate_face_attributes
1010
from pytorch3d.structures.utils import list_to_packed, list_to_padded, padded_to_list
1111
from torch.nn.functional import interpolate
1212

13+
from .utils import pack_rectangles
14+
1315

1416
# This file contains classes and helper functions for texturing.
1517
# There are three types of textures: TexturesVertex, TexturesAtlas
@@ -329,18 +331,23 @@ def __init__(self, atlas: Union[torch.Tensor, List, None]):
329331
330332
[1] Liu et al, 'Soft Rasterizer: A Differentiable Renderer for Image-based
331333
3D Reasoning', ICCV 2019
334+
See also https://github.com/ShichenLiu/SoftRas/issues/21
332335
"""
333336
if isinstance(atlas, (list, tuple)):
334337
correct_format = all(
335338
(
336339
torch.is_tensor(elem)
337340
and elem.ndim == 4
338341
and elem.shape[1] == elem.shape[2]
342+
and elem.shape[1] == atlas[0].shape[1]
339343
)
340344
for elem in atlas
341345
)
342346
if not correct_format:
343-
msg = "Expected atlas to be a list of tensors of shape (F, R, R, D)"
347+
msg = (
348+
"Expected atlas to be a list of tensors of shape (F, R, R, D) "
349+
"with the same value of R."
350+
)
344351
raise ValueError(msg)
345352
self._atlas_list = atlas
346353
self._atlas_padded = None
@@ -529,6 +536,12 @@ def join_batch(self, textures: List["TexturesAtlas"]) -> "TexturesAtlas":
529536
new_tex._num_faces_per_mesh = num_faces_per_mesh
530537
return new_tex
531538

539+
def join_scene(self) -> "TexturesAtlas":
540+
"""
541+
Return a new TexturesAtlas amalgamating the batch.
542+
"""
543+
return self.__class__(atlas=[torch.cat(self.atlas_list())])
544+
532545

533546
class TexturesUV(TexturesBase):
534547
def __init__(
@@ -560,7 +573,7 @@ def __init__(
560573
the two align_corners options at
561574
https://discuss.pytorch.org/t/22663/9 .
562575
563-
An example of how the indexing into the maps, with align_corners=True
576+
An example of how the indexing into the maps, with align_corners=True,
564577
works is as follows.
565578
If maps[i] has shape [101, 1001] and the value of verts_uvs[i][j]
566579
is [0.4, 0.3], then a value of j in faces_uvs[i] means a vertex
@@ -574,10 +587,11 @@ def __init__(
574587
If maps[i] has shape [100, 1000] and the value of verts_uvs[i][j]
575588
is [0.405, 0.2995], then a value of j in faces_uvs[i] means a vertex
576589
whose color is given by maps[i][700, 40].
577-
In this case, padding_mode even matters for values in verts_uvs
578-
slightly above 0 or slightly below 1. In this case, it matters if the
579-
first value is outside the interval [0.0005, 0.9995] or if the second
580-
is outside the interval [0.005, 0.995].
590+
When align_corners=False, padding_mode even matters for values in
591+
verts_uvs slightly above 0 or slightly below 1. In this case, the
592+
padding_mode matters if the first value is outside the interval
593+
[0.0005, 0.9995] or if the second is outside the interval
594+
[0.005, 0.995].
581595
"""
582596
super().__init__()
583597
self.padding_mode = padding_mode
@@ -805,12 +819,9 @@ def verts_uvs_list(self) -> List[torch.Tensor]:
805819
def maps_padded(self) -> torch.Tensor:
806820
return self._maps_padded
807821

808-
def maps_list(self) -> torch.Tensor:
809-
# maps_list is not used anywhere currently - maps
810-
# are padded to ensure the (H, W) of all maps is the
811-
# same across the batch and we don't store the
812-
# unpadded sizes of the maps. Therefore just return
813-
# the unbinded padded tensor.
822+
def maps_list(self) -> List[torch.Tensor]:
823+
if self._maps_list is not None:
824+
return self._maps_list
814825
return self._maps_padded.unbind(0)
815826

816827
def extend(self, N: int) -> "TexturesUV":
@@ -965,6 +976,143 @@ def join_batch(self, textures: List["TexturesUV"]) -> "TexturesUV":
965976
new_tex._num_faces_per_mesh = num_faces_per_mesh
966977
return new_tex
967978

979+
def _place_map_into_single_map(
980+
self,
981+
single_map: torch.Tensor,
982+
map_: torch.Tensor,
983+
location: Tuple[int, int, bool], # (x,y) and whether flipped
984+
) -> None:
985+
"""
986+
Copy map into a larger tensor single_map at the destination specified by location.
987+
If align_corners is False, we add the needed border around the destination.
988+
989+
Used by join_scene.
990+
991+
Args:
992+
single_map: (total_H, total_W, 3)
993+
map_: (H, W, 3) source data
994+
location: where to place map
995+
"""
996+
do_flip = location[2]
997+
source = map_.transpose(0, 1) if do_flip else map_
998+
border_width = 0 if self.align_corners else 1
999+
lower_u = location[0] + border_width
1000+
lower_v = location[1] + border_width
1001+
upper_u = lower_u + source.shape[0]
1002+
upper_v = lower_v + source.shape[1]
1003+
single_map[lower_u:upper_u, lower_v:upper_v] = source
1004+
1005+
if self.padding_mode != "zeros" and not self.align_corners:
1006+
single_map[lower_u - 1, lower_v:upper_v] = single_map[
1007+
lower_u, lower_v:upper_v
1008+
]
1009+
single_map[upper_u, lower_v:upper_v] = single_map[
1010+
upper_u - 1, lower_v:upper_v
1011+
]
1012+
single_map[lower_u:upper_u, lower_v - 1] = single_map[
1013+
lower_u:upper_u, lower_v
1014+
]
1015+
single_map[lower_u:upper_u, upper_v] = single_map[
1016+
lower_u:upper_u, upper_v - 1
1017+
]
1018+
single_map[lower_u - 1, lower_v - 1] = single_map[lower_u, lower_v]
1019+
single_map[lower_u - 1, upper_v] = single_map[lower_u, upper_v - 1]
1020+
single_map[upper_u, lower_v - 1] = single_map[upper_u - 1, lower_v]
1021+
single_map[upper_u, upper_v] = single_map[upper_u - 1, upper_v - 1]
1022+
1023+
def join_scene(self) -> "TexturesUV":
1024+
"""
1025+
Return a new TexturesUV amalgamating the batch.
1026+
1027+
We calculate a large single map which contains the original maps,
1028+
and find verts_uvs to point into it. This will not replicate
1029+
behavior of padding for verts_uvs values outside [0,1].
1030+
1031+
If align_corners=False, we need to add an artificial border around
1032+
every map.
1033+
1034+
We use the function `pack_rectangles` to provide a layout for the
1035+
single map. _place_map_into_single_map is used to copy the maps
1036+
into the single map. The merging of verts_uvs and faces_uvs are
1037+
handled locally in this function.
1038+
"""
1039+
maps = self.maps_list()
1040+
heights_and_widths = []
1041+
extra_border = 0 if self.align_corners else 2
1042+
for map_ in maps:
1043+
heights_and_widths.append(
1044+
(map_.shape[0] + extra_border, map_.shape[1] + extra_border)
1045+
)
1046+
merging_plan = pack_rectangles(heights_and_widths)
1047+
# pyre-fixme[16]: `Tensor` has no attribute `new_zeros`.
1048+
single_map = maps[0].new_zeros((*merging_plan.total_size, 3))
1049+
verts_uvs = self.verts_uvs_list()
1050+
verts_uvs_merged = []
1051+
1052+
for map_, loc, uvs in zip(maps, merging_plan.locations, verts_uvs):
1053+
new_uvs = uvs.clone()
1054+
self._place_map_into_single_map(single_map, map_, loc)
1055+
do_flip = loc[2]
1056+
x_shape = map_.shape[1] if do_flip else map_.shape[0]
1057+
y_shape = map_.shape[0] if do_flip else map_.shape[1]
1058+
1059+
if do_flip:
1060+
# Here we have flipped / transposed the map.
1061+
# In uvs, the y values are decreasing from 1 to 0 and the x
1062+
# values increase from 0 to 1. We subtract all values from 1
1063+
# as the x's become y's and the y's become x's.
1064+
new_uvs = 1.0 - new_uvs[:, [1, 0]]
1065+
if TYPE_CHECKING:
1066+
new_uvs = torch.Tensor(new_uvs)
1067+
1068+
# If align_corners is True, then an index of x (where x is in
1069+
# the range 0 .. map_.shape[]-1) in one of the input maps
1070+
# was hit by a u of x/(map_.shape[]-1).
1071+
# That x is located at the index loc[] + x in the single_map, and
1072+
# to hit that we need u to equal (loc[] + x) / (total_size[]-1)
1073+
# so the old u should be mapped to
1074+
# { u*(map_.shape[]-1) + loc[] } / (total_size[]-1)
1075+
1076+
# If align_corners is False, then an index of x (where x is in
1077+
# the range 1 .. map_.shape[]-2) in one of the input maps
1078+
# was hit by a u of (x+0.5)/(map_.shape[]).
1079+
# That x is located at the index loc[] + 1 + x in the single_map,
1080+
# (where the 1 is for the border)
1081+
# and to hit that we need u to equal (loc[] + 1 + x + 0.5) / (total_size[])
1082+
# so the old u should be mapped to
1083+
# { loc[] + 1 + u*map_.shape[]-0.5 + 0.5 } / (total_size[])
1084+
# = { loc[] + 1 + u*map_.shape[] } / (total_size[])
1085+
1086+
# We change the y's in new_uvs for the scaling of height,
1087+
# and the x's for the scaling of width.
1088+
# That is why the 1's and 0's are mismatched in these lines.
1089+
one_if_align = 1 if self.align_corners else 0
1090+
one_if_not_align = 1 - one_if_align
1091+
denom_x = merging_plan.total_size[0] - one_if_align
1092+
scale_x = x_shape - one_if_align
1093+
denom_y = merging_plan.total_size[1] - one_if_align
1094+
scale_y = y_shape - one_if_align
1095+
new_uvs[:, 1] *= scale_x / denom_x
1096+
new_uvs[:, 1] += (loc[0] + one_if_not_align) / denom_x
1097+
new_uvs[:, 0] *= scale_y / denom_y
1098+
new_uvs[:, 0] += (loc[1] + one_if_not_align) / denom_y
1099+
1100+
verts_uvs_merged.append(new_uvs)
1101+
1102+
faces_uvs_merged = []
1103+
offset = 0
1104+
for faces_uvs_, verts_uvs_ in zip(self.faces_uvs_list(), verts_uvs):
1105+
faces_uvs_merged.append(offset + faces_uvs_)
1106+
offset += verts_uvs_.shape[0]
1107+
1108+
return self.__class__(
1109+
maps=[single_map],
1110+
verts_uvs=[torch.cat(verts_uvs_merged)],
1111+
faces_uvs=[torch.cat(faces_uvs_merged)],
1112+
align_corners=self.align_corners,
1113+
padding_mode=self.padding_mode,
1114+
)
1115+
9681116

9691117
class TexturesVertex(TexturesBase):
9701118
def __init__(
@@ -1156,3 +1304,9 @@ def join_batch(self, textures: List["TexturesVertex"]) -> "TexturesVertex":
11561304
new_tex = self.__class__(verts_features=verts_features_list)
11571305
new_tex._num_verts_per_mesh = num_faces_per_mesh
11581306
return new_tex
1307+
1308+
def join_scene(self) -> "TexturesVertex":
1309+
"""
1310+
Return a new TexturesVertex amalgamating the batch.
1311+
"""
1312+
return self.__class__(verts_features=[torch.cat(self.verts_features_list())])

0 commit comments

Comments
 (0)