2
$\begingroup$

Hi everyone I am making an addon for my degree thesis, I have a problem with the following: I have to count the number of faces of each vertex group present in my scene and i use this function

           for x in list(bpy.data.objects): 
               list_vertex = [] 
               for vertex in vs:
                    for g in vertex.groups:
                        if (g.group == i):
                            list_vertex.append(vertex.index)
                            break 
               for face in list(bpy.data.objects[x.name].data.polygons):
                    isFace = False
                    count = 0
                    for vertex in face.vertices:
                        if vertex in list_vertex:
                            isFace = True
                            count+=1
                        else: isFace = False
                        if isFace == False:
                            break
                    if count == len(face.vertices):
                        count_face += 1

If I try to run the addon on a mesh with few vertices then everything works, if instead I use a bigger scene blender stops working immediately so I think my problem is related to the performance of my code, I'm new to addon programming, could anyone tell me if there is a more optimal method to do this?

$\endgroup$
4
  • $\begingroup$ What is vs? What is i? They're defined outside of the quoted code. $\endgroup$ Jun 13 at 19:33
  • $\begingroup$ IIUC you count a poly as being in a vertex group if all of its verts are in the group (no matter their weight) and you want the total number of polys in each group? $\endgroup$
    – scurest
    Jun 13 at 19:35
  • $\begingroup$ Please clarify your specific problem or provide additional details to highlight exactly what you need. As it's currently written, it's hard to tell exactly what you're asking. $\endgroup$
    – Community Bot
    Jun 13 at 19:39
  • $\begingroup$ You can probably accelerate it a lot by just using a set() instead of a list for list_vertices. Doing vertex in list_vertex will be very slow. Also don't use list() on the data.polygons. $\endgroup$
    – scurest
    Jun 13 at 19:57

3 Answers 3

4
$\begingroup$

You are right, there is room for improvement here. Quite a lot of unnecessary loops, where you loop over groups, then over object, vertices, and then over groups again. All these nested loops multiply, so the overall runtime explodes pretty quickly. Also you are creating unnecessary intermediate lists, which can be very inefficient when you add a lot of vertices one by one.

The basic idea is sound: count for each face how many of its vertices are in a group, then check if that matches the total vertex count of the face. That way you only need a single loop over groups and vertices. So lets do that:

import bpy

# No need to convert this into a list(...)
for obj in bpy.data.objects:
    # We get errors if we try to do the code below for anything other than meshes
    if obj.type != 'MESH':
        continue
    
    num_groups = len(obj.vertex_groups)

    # [x] * n is a more efficient way to create a large list all at once,
    # instead of adding one element at a time.
    
    # Counts number of faces in each group
    group_faces = [0] * len(obj.vertex_groups)

    for face in obj.data.polygons:
        # Counts vertices of this face in each group
        group_counts = [0] * num_groups

        for vertex_idx in face.vertices:
            vertex = obj.data.vertices[vertex_idx]
            for vgroup in vertex.groups:
                group_counts[vgroup.group] += 1
        
        # Count up faces where all vertices are in a group
        # enumerate is a utility to loop with an index and element together
        for group_idx, count in enumerate(group_counts):
            if count == len(face.vertices):
                group_faces[group_idx] += 1

    print(obj.name + ": " + ", ".join(group.name + "=" + str(group_faces[group_idx]) for group_idx, group in enumerate(obj.vertex_groups)))
$\endgroup$
2
  • $\begingroup$ Sorry for the long delay, your solution helped me a lot, however I still have some problems in terms of efficiency, I tested your solution on a mesh with 250,000 vertices and 500,000 faces, the script takes about 8 seconds to run, Isn't there an even faster solution? $\endgroup$
    – luke_mcp
    Jul 16 at 15:35
  • $\begingroup$ There are a few things to try if you really need that kind of performance: 1. Use bmesh instead of the bpy.Mesh class. This may involve conversion to the BMesh type, but it has nicer iterators over things like face corners, faces adjacent to a vertex, etc. docs.blender.org/api/3.2/bmesh.html 2. Profile the code to find actual bottlenecks docs.python.org/3/library/profile.html 3. Use numpy to store large arrays and parallelize computations. Numpy is shipped with Blender's builtin python. Copying mesh arrays to numpy and then computing with that might be faster overall. $\endgroup$
    – lukas_t
    Jul 17 at 16:31
2
$\begingroup$

Here's the fastest I found. It first collects a set of vertex indices for all groups, then iterates over all polys using issuperset to detect if the poly's verts are contained in a group.

import bpy

ob = bpy.data.objects["Your Object Here"]
mesh = ob.data
ngroups = len(ob.vertex_groups)

# set of all vertex indices that belong to each group
group_verts = [set() for _ in range(ngroups)]

for vi, v in enumerate(mesh.vertices):
    for g in v.groups:
        group_verts[g.group].add(vi)

# number of polys in each group
# (poly is in group if all its verts are)
poly_counts = [0] * ngroups

for poly in mesh.polygons:
    poly_verts = list(poly.vertices)

    # check whether poly belongs to each group
    for gi in range(ngroups):
        if group_verts[gi].issuperset(poly_verts):
            poly_counts[gi] += 1

print(poly_counts)

This was a bit faster than lukas_t's answer, but uses a lot more memory, so I don't necessarily recommend it.

Test mesh was 125k verts, 125k quads, 32 groups randomly assigned, ~3k polys in each group.

$\endgroup$
2
$\begingroup$

Any Corner

Consider a face belonging to a vertex group if it's affected by it - at least one of its corners belongs to the vertex group:

import bpy
from bpy import data as D
from collections import defaultdict

for ob in D.objects:
    if ob.type != 'MESH':
        continue
    print(f"--\n{ob.name}")
    me = ob.data
    g2f = defaultdict(set)  # group to face mapping
    v2f = defaultdict(set)  # vertex to face mapping
    for f in me.polygons:
        for v_index in f.vertices:
            v2f[v_index].add(f.index)
    for v in me.vertices:
        for g in v.groups:
            g2f[g.group].update(v2f[v.index])
    for g_index, faces_set in g2f.items():
        g_name = ob.vertex_groups[g_index].name
        print(f"{g_name}: {len(faces_set)} faces")

All Corners

A face belongs to a vert group only if it's entirely built from vertices belonging to the vertex group:

import bpy
from bpy import data as D
from collections import Counter

for ob in D.objects:
    if ob.type != 'MESH':
        continue
    print(f"--\n{ob.name}")
    me = ob.data
    group_counter = Counter()
    for f in me.polygons:
        size = len(f.vertices)
        verts = (me.vertices[i] for i in f.vertices)
        cnt = Counter(g.group for v in verts for g in v.groups)
        group_counter.update(i for i, c in cnt.items() if c == size)
    for g_index, num_faces in group_counter.items():
        g_name = ob.vertex_groups[g_index].name
        print(f"{g_name}: {num_faces} faces")

Bmesh - Any Corner

This will display info also for unused meshes and won't display info for the same mesh multiple times. Also I don't know how to access a vertex name without an object, though you could create a temporary object just for that...

import bpy, bmesh
from bpy import data as D
from collections import Counter

bm = bmesh.new()
for me in D.meshes:
    print(f"--\n{me.name}")
    bm.from_mesh(me, face_normals=False, vertex_normals=False)
    layer = bm.verts.layers.deform.active
    if not layer:
        continue
    group_counter = Counter()
    for f in bm.faces:
        group_counter.update({g for v in f.verts for g in v[layer].keys()})
    for g_index, num_faces in group_counter.items():
        print(f"Group #{g_index}: {num_faces} faces")
    bm.clear()

Bmesh - All Corners

import bpy, bmesh
from bpy import data as D
from collections import Counter

bm = bmesh.new()
for me in D.meshes:
    print(f"--\n{me.name}")
    bm.from_mesh(me, face_normals=False, vertex_normals=False)
    layer = bm.verts.layers.deform.active
    if not layer:
        continue
    group_counter = Counter()
    for f in bm.faces:
        groups = (v[layer].keys() for v in f.verts)
        groups_set = set(next(groups))
        for g in groups:
            groups_set.intersection_update(g)
        group_counter.update(groups_set)
    for g_index, num_faces in group_counter.items():
        print(f"Group #{g_index}: {num_faces} faces")
    bm.clear()
$\endgroup$
2
  • $\begingroup$ Doesn't this count a poly as belonging to a group if at least one of its verts belongs to it (vs all of them belonging to it)? $\endgroup$
    – scurest
    Jun 14 at 3:22
  • $\begingroup$ @scurest yes, I thought that's the idea. $\endgroup$ Jun 14 at 7:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.