Skip to content

subset

Re-imlementation of trimesh.submesh that is faster for our use case.

Notably we: - ignore normals (possibly needed) and visuals (definitely not needed) - allow only one set of faces to be passed - return vertices and faces instead of a new mesh - make as few copies as possible - allow passing vertex indices instead of faces

This function is 5-10x faster than trimesh.submesh for our use case. Note that the speed of this function was never the bottleneck though, it's about the memory footprint. See https://github.com/navis-org/navis/issues/154.

PARAMETER DESCRIPTION
mesh
        Mesh to submesh.

TYPE: trimesh.Trimesh

faces_index
        Indices of faces to keep.

TYPE: array-like DEFAULT: None

vertex_index
        Indices of vertices to keep.

TYPE: array-like DEFAULT: None

RETURNS DESCRIPTION
vertices

Vertices of submesh.

TYPE: np.ndarray

faces

Faces of submesh.

TYPE: np.ndarray

Source code in navis/morpho/subset.py
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
def submesh(mesh, *, faces_index=None, vertex_index=None):
    """Re-imlementation of trimesh.submesh that is faster for our use case.

    Notably we:
     - ignore normals (possibly needed) and visuals (definitely not needed)
     - allow only one set of faces to be passed
     - return vertices and faces instead of a new mesh
     - make as few copies as possible
     - allow passing vertex indices instead of faces

    This function is 5-10x faster than trimesh.submesh for our use case.
    Note that the speed of this function was never the bottleneck though,
    it's about the memory footprint.
    See https://github.com/navis-org/navis/issues/154.

    Parameters
    ----------
    mesh :          trimesh.Trimesh
                    Mesh to submesh.
    faces_index :   array-like
                    Indices of faces to keep.
    vertex_index :  array-like
                    Indices of vertices to keep.

    Returns
    -------
    vertices :  np.ndarray
                Vertices of submesh.
    faces :     np.ndarray
                Faces of submesh.

    """
    if faces_index is None and vertex_index is None:
        raise ValueError("Either `faces_index` or `vertex_index` must be provided.")
    elif faces_index is not None and vertex_index is not None:
        raise ValueError("Only one of `faces_index` or `vertex_index` can be provided.")

    # First check if we can return either an empty mesh or the original mesh right away
    if faces_index is not None:
        if len(faces_index) == 0:
            return np.array([]), np.array([])
        elif len(faces_index) == len(mesh.faces):
            if len(np.unique(faces_index)) == len(mesh.faces):
                return mesh.vertices.copy(), mesh.faces.copy()
    else:
        if len(vertex_index) == 0:
            return np.array([]), np.array([])
        elif len(vertex_index) == len(mesh.vertices):
            if len(np.unique(vertex_index)) == len(mesh.vertices):
                return mesh.vertices.copy(), mesh.faces.copy()

    # Use a view of the original data
    original_faces = mesh.faces.view(np.ndarray)
    original_vertices = mesh.vertices.view(np.ndarray)

    # If we're starting with vertices, find faces that contain at least one of our vertices
    # This way we will also make sure to drop unreferenced vertices
    if vertex_index is not None:
        faces_index = np.arange(len(original_faces))[
            np.isin(original_faces, vertex_index).all(axis=1)
        ]

    # Get unique vertices in the to-be-kept faces
    faces = original_faces[faces_index]
    unique = np.unique(faces.reshape(-1))

    # Generate a mask for the vertices
    # (using int32 here since we're unlikey to have more than 2B vertices)
    mask = np.arange(len(original_vertices), dtype=np.int32)

    # Remap the vertices to the new indices
    mask[unique] = np.arange(len(unique))

    # Grab the vertices in the order they are referenced
    vertices = original_vertices[unique].copy()

    # Remap the faces to the new vertex indices
    # (making a copy to allow `mask` to be garbage collected)
    faces = mask[faces].copy()

    return vertices, faces