Skip to content

Commit 050f650

Browse files
Krzysztof Chalupkafacebook-github-bot
Krzysztof Chalupka
authored andcommitted
Submesh 3/n: Add submeshing functionality
Summary: Copypasting the docstring: ``` Split a mesh into submeshes, defined by face indices of the original Meshes object. Args: face_indices: Let the original mesh have verts_list() of length N. Can be either - List of length N. The n-th element is a list of length num_submeshes_n (empty lists are allowed). Each element of the n-th sublist is a LongTensor of length num_faces. - List of length N. The n-th element is a possibly empty padded LongTensor of shape (num_submeshes_n, max_num_faces). Returns: Meshes object with selected submeshes. The submesh tensors are cloned. Currently submeshing only works with no textures or with the TexturesVertex texture. Example: Take a Meshes object `cubes` with 4 meshes, each a translated cube. Then: * len(cubes) is 4, len(cubes.verts_list()) is 4, len(cubes.faces_list()) is 4, * [cube_verts.size for cube_verts in cubes.verts_list()] is [8, 8, 8, 8], * [cube_faces.size for cube_faces in cubes.faces_list()] if [6, 6, 6, 6], Now let front_facet, top_and_bottom, all_facets be LongTensors of sizes (2), (4), and (12), each picking up a number of facets of a cube by specifying the appropriate triangular faces. Then let `subcubes = cubes.submeshes([[front_facet, top_and_bottom], [], [all_facets], []])`. * len(subcubes) is 3. * subcubes[0] is the front facet of the cube contained in cubes[0]. * subcubes[1] is a mesh containing the (disconnected) top and bottom facets of cubes[0]. * subcubes[2] is a clone of cubes[2]. * There are no submeshes of cubes[1] and cubes[3] in subcubes. * subcubes[0] and subcubes[1] are not watertight. subcubes[2] is. ``` Reviewed By: bottler Differential Revision: D35440657 fbshipit-source-id: 8a6d2d300ce226b5b9eb440688528b5e795195a1
1 parent 8596fca commit 050f650

File tree

2 files changed

+249
-0
lines changed

2 files changed

+249
-0
lines changed

pytorch3d/structures/meshes.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,6 +1556,115 @@ def sample_textures(self, fragments):
15561556
else:
15571557
raise ValueError("Meshes does not have textures")
15581558

1559+
def submeshes(
1560+
self,
1561+
face_indices: Union[
1562+
List[List[torch.LongTensor]], List[torch.LongTensor], torch.LongTensor
1563+
],
1564+
) -> "Meshes":
1565+
"""
1566+
Split a batch of meshes into a batch of submeshes.
1567+
1568+
The return value is a Meshes object representing
1569+
[mesh restricted to only faces indexed by selected_faces
1570+
for mesh, selected_faces_list in zip(self, face_indices)
1571+
for faces in selected_faces_list]
1572+
1573+
Args:
1574+
face_indices:
1575+
Let the original mesh have verts_list() of length N.
1576+
Can be either
1577+
- List of lists of LongTensors. The n-th element is a list of length
1578+
num_submeshes_n (empty lists are allowed). The k-th element of the n-th
1579+
sublist is a LongTensor of length num_faces_submesh_n_k.
1580+
- List of LongTensors. The n-th element is a (possibly empty) LongTensor
1581+
of shape (num_submeshes_n, num_faces_n).
1582+
- A LongTensor of shape (N, num_submeshes_per_mesh, num_faces_per_submesh)
1583+
where all meshes in the batch will have the same number of submeshes.
1584+
This will result in an output Meshes object with batch size equal to
1585+
N * num_submeshes_per_mesh.
1586+
1587+
Returns:
1588+
Meshes object of length `sum(len(ids) for ids in face_indices)`.
1589+
1590+
Submeshing only works with no textures or with the TexturesVertex texture.
1591+
1592+
Example 1:
1593+
1594+
If `meshes` has batch size 1, and `face_indices` is a 1D LongTensor,
1595+
then `meshes.submeshes([[face_indices]]) and
1596+
`meshes.submeshes(face_indices[None, None])` both produce a Meshes of length 1,
1597+
containing a single submesh with a subset of `meshes`' faces, whose indices are
1598+
specified by `face_indices`.
1599+
1600+
Example 2:
1601+
1602+
Take a Meshes object `cubes` with 4 meshes, each a translated cube. Then:
1603+
* len(cubes) is 4, len(cubes.verts_list()) is 4, len(cubes.faces_list()) 4,
1604+
* [cube_verts.size for cube_verts in cubes.verts_list()] is [8, 8, 8, 8],
1605+
* [cube_faces.size for cube_faces in cubes.faces_list()] if [6, 6, 6, 6],
1606+
1607+
Now let front_facet, top_and_bottom, all_facets be LongTensors of
1608+
sizes (2), (4), and (12), each picking up a number of facets of a cube by
1609+
specifying the appropriate triangular faces.
1610+
1611+
Then let `subcubes = cubes.submeshes([[front_facet, top_and_bottom], [],
1612+
[all_facets], []])`.
1613+
* len(subcubes) is 3.
1614+
* subcubes[0] is the front facet of the cube contained in cubes[0].
1615+
* subcubes[1] is a mesh containing the (disconnected) top and bottom facets
1616+
of cubes[0].
1617+
* subcubes[2] is cubes[2].
1618+
* There are no submeshes of cubes[1] and cubes[3] in subcubes.
1619+
* subcubes[0] and subcubes[1] are not watertight. subcubes[2] is.
1620+
"""
1621+
if not (
1622+
self.textures is None or type(self.textures).__name__ == "TexturesVertex"
1623+
):
1624+
raise ValueError(
1625+
"Submesh extraction only works with no textures or TexturesVertex."
1626+
)
1627+
1628+
if len(face_indices) != len(self):
1629+
raise ValueError(
1630+
"You must specify exactly one set of submeshes"
1631+
" for each mesh in this Meshes object."
1632+
)
1633+
1634+
sub_verts = []
1635+
sub_faces = []
1636+
1637+
for face_ids_per_mesh, faces, verts in zip(
1638+
face_indices, self.faces_list(), self.verts_list()
1639+
):
1640+
for submesh_face_ids in face_ids_per_mesh:
1641+
faces_to_keep = faces[submesh_face_ids]
1642+
1643+
# Say we are keeping two faces from a mesh with six vertices:
1644+
# faces_to_keep = [[0, 6, 4],
1645+
# [0, 2, 6]]
1646+
# Then we want verts_to_keep to contain only vertices [0, 2, 4, 6]:
1647+
vertex_ids_to_keep = torch.unique(faces_to_keep, sorted=True)
1648+
sub_verts.append(verts[vertex_ids_to_keep])
1649+
1650+
# Now, convert faces_to_keep to use the new vertex ids.
1651+
# In our example, instead of
1652+
# [[0, 6, 4],
1653+
# [0, 2, 6]]
1654+
# we want faces_to_keep to be
1655+
# [[0, 3, 2],
1656+
# [0, 1, 3]],
1657+
# as each point id got reduced to its sort rank.
1658+
_, ids_of_unique_ids_in_sorted = torch.unique(
1659+
faces_to_keep, return_inverse=True
1660+
)
1661+
sub_faces.append(ids_of_unique_ids_in_sorted)
1662+
1663+
return self.__class__(
1664+
verts=sub_verts,
1665+
faces=sub_faces,
1666+
)
1667+
15591668

15601669
def join_meshes_as_batch(meshes: List[Meshes], include_textures: bool = True) -> Meshes:
15611670
"""

tests/test_meshes.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,46 @@ def to_sorted(mesh: Meshes) -> "Meshes":
233233
return other
234234

235235

236+
def init_cube_meshes(device: str = "cpu"):
237+
# Make Meshes with four cubes translated from the origin by varying amounts.
238+
verts = torch.FloatTensor(
239+
[
240+
[0, 0, 0],
241+
[1, 0, 0], # 1->0
242+
[1, 1, 0], # 2->1
243+
[0, 1, 0], # 3->2
244+
[0, 1, 1], # 3
245+
[1, 1, 1], # 4
246+
[1, 0, 1], # 5
247+
[0, 0, 1],
248+
],
249+
device=device,
250+
)
251+
252+
faces = torch.FloatTensor(
253+
[
254+
[0, 2, 1],
255+
[0, 3, 2],
256+
[2, 3, 4], # 1,2, 3
257+
[2, 4, 5], #
258+
[1, 2, 5], #
259+
[1, 5, 6], #
260+
[0, 7, 4],
261+
[0, 4, 3],
262+
[5, 4, 7],
263+
[5, 7, 6],
264+
[0, 6, 7],
265+
[0, 1, 6],
266+
],
267+
device=device,
268+
)
269+
270+
return Meshes(
271+
verts=[verts, verts + 1, verts + 2, verts + 3],
272+
faces=[faces, faces, faces, faces],
273+
)
274+
275+
236276
class TestMeshes(TestCaseMixin, unittest.TestCase):
237277
def setUp(self) -> None:
238278
np.random.seed(42)
@@ -1257,6 +1297,106 @@ def test_assigned_normals(self):
12571297
yes_normals.offset_verts_(torch.FloatTensor([1, 2, 3]).expand(12, 3))
12581298
self.assertFalse(torch.allclose(yes_normals.verts_normals_padded(), verts))
12591299

1300+
def test_submeshes(self):
1301+
empty_mesh = Meshes([], [])
1302+
# Four cubes with offsets [0, 1, 2, 3].
1303+
cubes = init_cube_meshes()
1304+
1305+
# Extracting an empty submesh from an empty mesh is allowed, but extracting
1306+
# a nonempty submesh from an empty mesh should result in a value error.
1307+
self.assertTrue(mesh_structures_equal(empty_mesh.submeshes([]), empty_mesh))
1308+
self.assertTrue(
1309+
mesh_structures_equal(cubes.submeshes([[], [], [], []]), empty_mesh)
1310+
)
1311+
1312+
with self.assertRaisesRegex(
1313+
ValueError, "You must specify exactly one set of submeshes"
1314+
):
1315+
empty_mesh.submeshes([torch.LongTensor([0])])
1316+
1317+
# Check that we can chop the cube up into its facets.
1318+
subcubes = to_sorted(
1319+
cubes.submeshes(
1320+
[ # Do not submesh cube#1.
1321+
[],
1322+
# Submesh the front face and the top-and-bottom of cube#2.
1323+
[
1324+
torch.LongTensor([0, 1]),
1325+
torch.LongTensor([2, 3, 4, 5]),
1326+
],
1327+
# Do not submesh cube#3.
1328+
[],
1329+
# Submesh the whole cube#4 (clone it).
1330+
[torch.LongTensor(list(range(12)))],
1331+
]
1332+
)
1333+
)
1334+
1335+
# The cube should've been chopped into three submeshes.
1336+
self.assertEquals(len(subcubes), 3)
1337+
1338+
# The first submesh should be a single facet of cube#2.
1339+
front_facet = to_sorted(
1340+
Meshes(
1341+
verts=torch.FloatTensor([[[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]])
1342+
+ 1,
1343+
faces=torch.LongTensor([[[0, 2, 1], [0, 3, 2]]]),
1344+
)
1345+
)
1346+
self.assertTrue(mesh_structures_equal(front_facet, subcubes[0]))
1347+
1348+
# The second submesh should be the top and bottom facets of cube#2.
1349+
top_and_bottom = Meshes(
1350+
verts=torch.FloatTensor(
1351+
[[[1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 1, 1], [1, 1, 1], [1, 0, 1]]]
1352+
)
1353+
+ 1,
1354+
faces=torch.LongTensor([[[1, 2, 3], [1, 3, 4], [0, 1, 4], [0, 4, 5]]]),
1355+
)
1356+
self.assertTrue(mesh_structures_equal(to_sorted(top_and_bottom), subcubes[1]))
1357+
1358+
# The last submesh should be all of cube#3.
1359+
self.assertTrue(mesh_structures_equal(to_sorted(cubes[3]), subcubes[2]))
1360+
1361+
# Test alternative input parameterization: list of LongTensors.
1362+
two_facets = torch.LongTensor([[0, 1], [4, 5]])
1363+
subcubes = to_sorted(cubes.submeshes([two_facets, [], two_facets, []]))
1364+
expected_verts = torch.FloatTensor(
1365+
[
1366+
[[0, 0, 0], [0, 1, 0], [1, 0, 0], [1, 1, 0]],
1367+
[[1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]],
1368+
[[2, 2, 2], [2, 3, 2], [3, 2, 2], [3, 3, 2]],
1369+
[[3, 2, 2], [3, 2, 3], [3, 3, 2], [3, 3, 3]],
1370+
]
1371+
)
1372+
expected_faces = torch.LongTensor(
1373+
[
1374+
[[0, 3, 2], [0, 1, 3]],
1375+
[[0, 2, 3], [0, 3, 1]],
1376+
[[0, 3, 2], [0, 1, 3]],
1377+
[[0, 2, 3], [0, 3, 1]],
1378+
]
1379+
)
1380+
expected_meshes = Meshes(verts=expected_verts, faces=expected_faces)
1381+
self.assertTrue(mesh_structures_equal(subcubes, expected_meshes))
1382+
1383+
# Test alternative input parameterization: a single LongTensor.
1384+
triangle_per_mesh = torch.LongTensor([[[0]], [[1]], [[4]], [[5]]])
1385+
subcubes = to_sorted(cubes.submeshes(triangle_per_mesh))
1386+
expected_verts = torch.FloatTensor(
1387+
[
1388+
[[0, 0, 0], [1, 0, 0], [1, 1, 0]],
1389+
[[1, 1, 1], [1, 2, 1], [2, 2, 1]],
1390+
[[3, 2, 2], [3, 3, 2], [3, 3, 3]],
1391+
[[4, 3, 3], [4, 3, 4], [4, 4, 4]],
1392+
]
1393+
)
1394+
expected_faces = torch.LongTensor(
1395+
[[[0, 2, 1]], [[0, 1, 2]], [[0, 1, 2]], [[0, 2, 1]]]
1396+
)
1397+
expected_meshes = Meshes(verts=expected_verts, faces=expected_faces)
1398+
self.assertTrue(mesh_structures_equal(subcubes, expected_meshes))
1399+
12601400
def test_compute_faces_areas_cpu_cuda(self):
12611401
num_meshes = 10
12621402
max_v = 100

0 commit comments

Comments
 (0)