4
$\begingroup$

I know a couple of ways to get images in the node_tree, the simplest is:

image_list=[]
for n in bpy.data.materials['My Mat'].node_tree.nodes:
    if n.type == 'TEX_IMAGE':
        if n.image:
            if n.image not in image_list:
                image_list.append(n.image)

Clearly, it is the fastest way if group type nodes are not used. Alternatively you can build a function that iterates over groups, subgroups etc. It's not a problem. technically I can get to all the images in any sub-group

So basically I can currently get to all the data images. but I was wondering, is there a python api that allows you to see directly which images bpy.data.images are related to bpy.data.materials['My Mat'] ?

Note: The main purpose is, to keep track in real time in the panel, of how many images there are in that node tree. So that method (which I proposed in the example) is wasteful of resources if the materials are very complex. It would run too many times when the cursor is touched on the panel.

$\endgroup$
10
  • $\begingroup$ Since all materials are node based, I guess there is no other way to determine whether an image is in use, also I can not find anything related in the docs. $\endgroup$ – brockmann Dec 9 '20 at 12:36
  • $\begingroup$ So... if the answer is not in Python code... the answer is just "no"? $\endgroup$ – lemon Dec 9 '20 at 12:37
  • $\begingroup$ Not sure it will be quicker. Could play around with users. ie make a list of all users [im.users for im in D.images] copy a material, recreate list. Any that have incremented user count are in use by the material. Then remove dupe. Similarly with materials using node groups using images. $\endgroup$ – batFINGER Dec 9 '20 at 12:45
  • $\begingroup$ @batFINGER but im.users is just user count I think and is a simple integer. are there any other ways to get users? $\endgroup$ – MohammadHossein Jamshidi Dec 11 '20 at 15:11
  • 1
    $\begingroup$ @batFINGER, thanks. Tested it on 1k times on 'Logic Node Libary' for around 0.8s here. $\endgroup$ – lemon Dec 12 '20 at 16:28
8
+50
$\begingroup$

Recurse the tree.

In question code, the major rate determining step will be checking the list for dupe to append, often quicker to add them all and use a set if only unique members are of interest.

Suggest can use list comprehension, or recursively walk the tree.

Here is an example, pass a node tree to a method and walk its nodes, if it has an image property yield it, if it has a node tree yield from it.

Personally I like the simplicity of yield syntax usage for traversing a tree, and the low memory hogging nature of generators, however they need a little tweaking to optimize for speed, see below

import bpy

def images_in_tree(node_tree):
    for node in node_tree.nodes:
        if hasattr(node, "image"):
            yield node.image
        if hasattr(node, "node_tree"):
            yield from images_in_tree(node.node_tree)
        
for mat in bpy.data.materials:
    if mat.use_nodes:
        print(set(filter(None, images_in_tree(mat.node_tree))))

Note could yield above getattr(node, "image", None) to avoid testing and filter out the Nones, however it may be a better idea to yield the node_tree, and the node. The ID data of the node_tree node_tree.id_data will be a material or a node group.

Just the number of unique images in tree.

In answer to

Keep track of how many images are in use in a Material (Or node_tree) in real time, printed on the UI, without having to press buttons or the like

Instead of yielding the images returns only a count.

Used a boolean default dictionary to tag items. Any item is given the default value false

In response to

fyi, you can't use tag here. Or any optimization like caching.

Have tagged both the nodegroups and images with one dict to make the method directly usable in a draw method, instead of using say a getter of a material property,

bpy.types.Material.image_users = IntProperty(get=num_images)

In which case could lay it out with

layout.prop(mat, "image_users")

and using the collection.tag(False) would be Ok in this context, and removes the need for the default dictionary.

As noted yielding and recursion can be slow in python, however using functools.lru_cache seriously fixes this see similar gain here https://blender.stackexchange.com/a/199075/15543 as well as other optimizations used. See lru_cache link re expiring or clearing cache on some interval

Basically it is caching the result of a call to memory.

import bpy
from collections import defaultdict
from functools import lru_cache

@lru_cache(16) # play with this
def num_images(mat):
    tag = defaultdict(bool)
    
    def unique(node):
        img = getattr(node, "image", None) 
        if tag[img]:
            return False
        tag[img] = True
        return img is not None

     
    def images_in_tree(node_trees):
        for node_tree in node_trees:
            if tag[node_tree]:
                continue
            tag[node_tree] = True
            yield sum(unique(n) for n in node_tree.nodes)
            yield from images_in_tree((n.node_tree  for n in node_tree.nodes if hasattr(n, "node_tree")))
    return sum(images_in_tree((mat.node_tree,)))

# make an int material property 
bpy.types.Material.image_users = bpy.props.IntProperty(get=num_images.__wrapped__)
# tack a draw method to text editor footer to test
from random import random
def draw(self, context):
    mat = context.object.active_material
    if mat:
        self.layout.prop(mat, "image_users")
        if random() <= 0.5:
            num_images.cache_clear()
        self.layout.label(text=f"{num_images(mat)}")
        
bpy.types.TEXT_HT_footer.prepend(draw)

The material property uses an uncached version of the method above, num_images.__wrapped__ the label uses the cached method. The cache is cleared each time a random is below a threshold. The expectation above is that it is only calculated approximately every 2nd time.

Other methods to consider would be using a counter on the UI class, clearing in a draw, poll or __init__ method of classes in different regions, a draw call back.. (Or as mentioned prior expire the cache) or simply not use it at all.

enter image description here Extra image tacked into test file, showing 8 images in material, removed shows 7, undo back to 8

$\endgroup$
5
  • $\begingroup$ It looks good, I'll have to test it! Let's see how it behaves with 800+ Nodes. (I'm not kidding XD) $\endgroup$ – Noob Cat Dec 12 '20 at 16:11
  • $\begingroup$ Cool. Even it slows things down, might be useful to check whether an image node is connected at all. $\endgroup$ – brockmann Dec 12 '20 at 17:12
  • $\begingroup$ Would be tricky. Know I often leave unconnected "thinking about using" nodes littered around. Testing for nodes with valid paths to output could be something to look at. $\endgroup$ – batFINGER Dec 12 '20 at 17:18
  • 1
    $\begingroup$ If you group.tag before the loop it won't count images per node tree. $\endgroup$ – lemon Dec 13 '20 at 6:37
  • $\begingroup$ fyi, you can't use tag here. Or any optimization like caching. $\endgroup$ – lemon Dec 15 '20 at 7:47
6
$\begingroup$

So... it seems quite confirmed that there is no api to do that. And even if this api exist it would be called as often in the context.

You can either recurse into, or iterate over, subnodes that are groups.

But in both case there is no need to go through the same groups several times (as we may find the same images and their users property won't change for that).

To collect the images without duplicates or to test if a group is already parsed, we can use a set().

Recursive way

def search_images_recursive4(material):
    node_trees = set()
    if material.use_nodes:
        def search_trees(trees):
            for tree in trees:
                if not tree in node_trees:
                    node_trees.add(tree)
                    search_trees((n.node_tree for n in tree.nodes if hasattr(n, "node_tree")))
        search_trees((material.node_tree,))
        
    return set(n.image for t in node_trees for n in t.nodes if hasattr(n,"image") and n.image)

Iterative way

def search_images_iterative3(material):
    result = set()
    if material.use_nodes:
        encountered = set()
        trees = set((material.node_tree,))
        while trees:
            encountered.update(trees)
            result.update(n.image for t in trees for n in t.nodes if hasattr(n, "image"))
            trees = set(n.node_tree for t in trees for n in t.nodes if hasattr(n, "node_tree")).difference(encountered)
            
    return result

Test case: the file from this question.

The test is a 1000 times loop call each function over all the materials of the file.

Run it 10 times and took the average.

import time

n = 1000
start = time.time()
for i in range(n):
    for mat in [m for m in bpy.data.materials if m.use_nodes]:
        pass #call the function to test here instead of pass
end = time.time()
print(end-start)

Results:

New @batFINGER solution num_images: 4.4 seconds.

search_images_recursive4: 3.17 seconds

search_images_iterative3: 3.26 seconds


Note

What I've finally discovered (and learned) is using

hasattr(n, "node_tree") 

is the key point as it is much faster than

n.type == 'GROUP'
$\endgroup$
9
  • $\begingroup$ For your information, @batFINGER, with tags, quite similar in runtime. $\endgroup$ – lemon Dec 12 '20 at 16:55
  • $\begingroup$ would still avoid if x in y if possible. $\endgroup$ – batFINGER Dec 12 '20 at 16:58
  • $\begingroup$ in counterpart, yield is just very slow... @batFINGER $\endgroup$ – lemon Dec 13 '20 at 6:48
  • $\begingroup$ Here it is really difficult, to assign the Bounty, guys, I ask you, in your opinion which of the 2 answers is the best. Really hard for me to decide. $\endgroup$ – Noob Cat Dec 14 '20 at 18:35
  • 2
    $\begingroup$ @NoobCat. I think the best way for you to choose is to test and compare. This is your question and so your responsability in some way : ) $\endgroup$ – lemon Dec 15 '20 at 8:10

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.