How to get indices of the faces to which a material has been applied, using Python?

I have many objects similar to this and I import them into the scene using bpy.ops.import_scene.obj(filepath='model.obj', split_mode="OFF") to do some renderings using Blender's internal rendering engine. I want to loop over all of the materials and get the indices of the faces/polygons to which each of the materials have been applied to. However, I do not know how to do that. I initially tried to get the indices of the faces that use the same material_index as shown below:

matDict = {}
polys = bpy.context.scene.objects.active.data.polygons
mats = bpy.context.scene.objects.active.data.materials
for idx, poly in enumerate(polys):
if mats[poly.material_index].name not in matDict:
matDict[mats[poly.material_index].name] = []
matDict[mats[poly.material_index].name].append(idx)
else:
matDict[mats[poly.material_index].name].append(idx)


However this method does not give me back the list of all materials and misses most of the materials in mats and therefore, the number of keys in matDict is not equal to the number of materials. I'm not entirely sure why, but it could be because some of the materials are applied to the same faces and material_index returns only one of them. I wonder if someone can offer a better solution?

2 Answers

This script iterates over the polys and adds their indices to the appropriate key (material name) if the material is applied to each of the current faces:

import bpy

o = bpy.context.object # Active object

# Initialize dictionary of all materials applied to object with empty lists
# which will contain indices of faces on which these materials are applied
materialPolys = { ms.material.name : [] for ms in o.material_slots }

for i, p in enumerate( o.data.polygons ):
materialPolys[ o.material_slots[ p.material_index ].name ].append( i )

print( materialPolys )


For this object, an icosphere which has 4 materials (3 of which are actually applied to faces), this is the result:

{
'Material.002': [7, 12, 41, 42, 53, 54, 55],
'Material.004': [],
'Material.003': [10, 19, 34, 35, 47, 48, 49, 62],
'Material.001': [0, 1, 2, 3, 4, 5, 6, 8, 9, 11, 13, 14, 15, 16, 17, 18, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 36, 37, 38, 39, 40, 43, 44, 45, 46, 50, 51, 52, 56, 57, 58, 59, 60, 61, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79]
}

• Thank you. I forgot to mention (will edit my question later) but is there a way to get the face indices for material covering the surface as well? Do you think that's a separate question or can you modify your answer easily for that? – Amir Mar 17 '18 at 11:28
• @Amir, not sure I understand what you mean by "the material covering the surface". The solution above shows all the materials and the faces each of them is assigned to. – TLousky Mar 17 '18 at 11:34
• I want to chanhe the color of the materials, but changing the color of the materials that are not visible (because they are being covered by some other materials) will not help me with. So I need to eventually identify the material that is on top of others. If you download the obj file I uploaded you can get a better sense of what I am saying. I eventually want to change the color of that car from blue to red – Amir Mar 17 '18 at 11:43
• Will fail on empty material slot. if ms.material is not None You could use geattr(mat, "name", "Not Assigned") in comp, or the material itself as a dic key, and have a None item which will give you the non assigned material faces. (but not the index that is assigned) In that case could use (mat, mat_index) as the key. @Amir if you want to change blue to red you simply replace the material in the slots, eg where there's a blue material change it to red. you don't have to look at the faces at all. – batFINGER Mar 17 '18 at 12:14
• @batFINGER Ye that's true but I need the face indices to do something else with them later on but I mostly need the face indices of the material that I'm going to modify (e.g. change color) – Amir Mar 17 '18 at 12:19

Use bpy.context.active_object in stead of bpy.context.scene.objects.active but rest of the code is working as expected.

Only materials will be added to the matDict if they are assigned to a face on the edit mesh.

A better solution (but not different then yours) would be to use the defaultdict.

from collections import defaultdict
import bpy
matDict = defaultdict(set)

polys = bpy.context.active_object.data.polygons
mats = bpy.context.active_object.data.materials
for idx, poly in enumerate(polys):
matDict[mats[poly.material_index].name].add(idx)


Using a cube with 3 materials and assigned to the mesh gives me correct result as long as materials are assigned. Are you sure that all materials are assigned?

• Thank you. Yes I am sure the materials are assigned cause when I remove them the surface colors change. Did you try with the object I uploaded? Also, I forgot to mention (will edit my question later) but is there a way to get the face indices for material covering the surface as well? Do you think that's a separate question or can you modify your answer easily for that? – Amir Mar 17 '18 at 11:28
• Could give strange results when materials are not data linked. – batFINGER Mar 17 '18 at 12:04