Looking to improve the speed of one of my PyMEL functions

I have a PyMEL-function for converting a uv selection into a list containing each individual uv shell; it is a general-purpose function that I use for all sorts of things.
I have noticed that it’s not very fast though, so when there are too many uv shells around this function will stall - making other functions (further down the line) having to wait for it. Im hoping that maybe someone here on the forums could provide a little bit of feedback - maybe even a solution to this problem.

import pymel.core as pm

# Returns list with each shell in it
def convertToShell():
    
    listFinal = []
    
    # Store coords and check count
    selCoords = pm.filterExpand(selectionMask=35) # Poly UV's
    uvRemain = uvCount = len(selCoords)
    
    # Progress window
    pm.progressWindow(
        isInterruptable=True,  
        maxValue=uvCount, 
        progress=0, 
        status="Pre-processing UV shells", 
        title="Pre-processing UV shells" 
    )
    
    # Do for every shell
    while uvRemain != 0:
        
        # Break if cancelled by user
        if pm.progressWindow(query=True, isCancelled=True) == True:
            pm.warning("Interupted by user")
            break
        
        # Edit the progress window
        pm.progressWindow(
            edit=True, 
                progress=(uvCount - uvRemain),
                status=("Pre-processing shells.
%s UVs left")%uvRemain
        )
    
        # Select the first and current coord of selCoords
        pm.select(selCoords[0], replace=True)
        
        # Expand selection to entire shell and store
        pm.polySelectConstraint(type=0x0010, shell=True, border=False, mode=2)
        pm.polySelectConstraint(shell=False, border=False, mode=0)
        selShell = pm.ls(selection=True, flatten=True)
        
        # Go back to original single-UV selection (replace)
        pm.select(selCoords, replace=True)
        
        # Reduce that selection by deselecting previous shell
        pm.select(selShell, deselect=True)
        selCoords = pm.ls(selection=True, flatten=True)
        
        # Recalculate number of UVs left
        uvRemain = len(selCoords)
        
        # Add shell to list
        listFinal.append(selShell)
        pm.select(clear=True)
        
    # Close progress window
    pm.progressWindow(endProgress=True)
    
    # Return shell list
    return listFinal

I know what is taking so long: selections. I’ve been told by more experienced programmers that the select-command is slow, and that it’s always better to work on the raw data instead of working with selections and reading data on the fly.
But I know of no other way for collecting UV shells than this. There’s probably some super-speedy Maya API way of getting all the UV shells but C++ isn’t in my skills-toolbox :frowning:

Any advice?


import pymel.all as pm

def get_selected_uv_shells():
    sel = pm.selected(o = True)[0]
    selected_uvs = set(uv for uv in pm.selected() if isinstance(uv, pm.MeshUV))
    # getUvShellsIds returns a list of shell_ids, and the number of shells
    uv_shell_ids, num_shells = sel.getUvShellsIds()
    # You still need to actually get the MeshUVs yourself    
    # Using sets instead of sublists because checking if an item is in a set is much faster
    all_shells = [set() for i in xrange(num_shells)]
    for uv_index, uv_shell_id in enumerate(uv_shell_ids):
        all_shells[uv_shell_id].add(sel.uvs[uv_index])
    # If you've not selected any UVs, I'm assuming you want them all.
    # Converting your selection to uvs before getting selected_uvs would probably be more accurate
    if not selected_uvs:
        return all_shells
    selected_shells = []
    # This goes through and finds shells that contain one of the selected uvs
    while selected_uvs:
        test_uv = selected_uvs.pop()
        for shell in all_shells:
            if test_uv in shell:
                break
        selected_shells.append(shell)
        # No need to check against that shell again
        all_shells.remove(shell)
        # Also we can get rid of all the uvs from that shell
        # This way we don't end up iterating through all the UVs
        selected_uvs.difference_update(shell)
    return selected_shells
get_selected_uv_shells()


Hopefully this makes sense, but basically I’m speeding the whole thing up by relying on the getUvShellIds method, that returns a list of shell ids for every uv.
Because it is called from the mesh though, you still need to go through and filter out the uvs that you don’t currently have selected.

This can still be slow with dense meshes, just due to the Pymels speed issues with creating a whole lot of component objects.
At which point it might be faster to move everything down to API calls.

Here a function I wrote to retrieve UVs ordered by UV Shell, maybe it can help you get started with working with the API.
The best performance outside of learning C++ is to skip PyMel and use OpenMaya directly.


def getUVShells(shape, uvSet=None):
    
    uvSets = cmds.polyUVSet(shape, q=True, allUVSets=True)
    if not uvSet or uvSet not in uvSets:
        uvSet = cmds.polyUVSet(shape, q=True, cuv=True)[0]

    selectionList = om.MSelectionList()
    selectionList.add(shape)

    mDag = om.MDagPath()
    selectionList.getDagPath(0, mDag)
    
    meshFn = om.MFnMesh(mDag)
    
    uvShellArray = om.MIntArray()
    shells = om.MScriptUtil()
    shellsPtr = shells.asUintPtr()
    
    meshFn.getUvShellsIds(uvShellArray, shellsPtr, uvSet)
    
    uvShells = defaultdict(list)
    for i, shellId in enumerate(uvShellArray):
        uvShells[shellId].append('{0}.map[{1}]'.format(shape, i))
    
    return uvShells.values()

Thanks for your replies guys, I appreciate it!
I’ll digest that code later tomorrow :slight_smile:

rgkovach123:
If you have any good resources/guides on getting started with OpenMaya it would be greatly appreciated. I did have a look at some kind of intro to the Maya API -coding in the book “Maya Python for Games and Films” but found the texts there to be really lacking (really poor descriptions of what was going on. Those chapters felt like they were written by a totally different author (and not a very pedagogic one).

the toughest part of the C++ api is understanding when, and how, to use MScriptUtil. I found the Maya Python for Games and Film to be helpful, especially the chapter on how to translate C++ syntax.

Maya comes will a lot of examples, both C++ and Python. Maya’s own documentation is handy as well.

http://docs.autodesk.com/MAYAUL/2014/ENU/Maya-API-Documentation/index.html?url=files/Maya_Python_API_Using_the_Maya_Python_API.htm,topicNumber=d30e34315

[QUOTE=rgkovach123;25314]the toughest part of the C++ api is understanding when, and how, to use MScriptUtil. I found the Maya Python for Games and Film to be helpful, especially the chapter on how to translate C++ syntax.

Maya comes will a lot of examples, both C++ and Python. Maya’s own documentation is handy as well.

http://docs.autodesk.com/MAYAUL/2014/ENU/Maya-API-Documentation/index.html?url=files/Maya_Python_API_Using_the_Maya_Python_API.htm,topicNumber=d30e34315[/QUOTE]
From my point of view, the big hurdle is knowing how to deconstruct a command (or set of actions) so that I can start looking at reverse-engineering it with openMaya. I can read what’s going on in your code but I do not fully understand/grasp the structure of the objects that you present: It’s like looking at a lego construction, being unable to identify the shape, form and color of the individual lego pieces - and why certain pieces have to be together with certain other pieces.

Also, the code doesn’t run: I get a NameError on the defaultdict -object (which is undefined).

Wrapping your head around the c++ classes is pretty daunting, but once you get over the initial hurdle, it’s not too bad.

I forgot to include an import statement in my example. Try adding:

from collections import defaultdict

I’ll try to explain what is happening. Anyone who understands C++ or the API will probably be horrified by my explanation.

First we need to take a string, our object name, and use to create a Class of MDagObject. This is one of the most basic classes. Once we have a MDagObject, we can use it to create a MfnMesh Class, which is a class that has a lot of polygon specific functions.

The MfnMesh class has a function that will return the UV Shell ID of a UV coordinate. For example, the first and second polygons of a mesh are different shells, it would look like this:

0, 0, 0, 1, 1, 1

This is telling us that .map[0], .map[1], .map[2] define a UV Shell and .map[3], .map[4], .map[5] define a second shell.
If our mesh only had two triangles, this would point back to:

.vtx[0], .vtx[1], vtx[2] belong to a UV Shell and .vtx[0], .vtx[2], vtx[3] define a second UV Shell.

The Shell ID itself is not that useful to use, but it is useful as a grouping mechanism, since every UV coordinate in a shell will have the same ID. We can use this later in combination with the defaultdict.

So to get this list of shell IDs we have to deal with the API. The getUvShellIDs function doesn’t return a value, it needs to be passed a variable to dump the results. If you have written shaders in HLSL or GLSL, its sort of like using inout.

We have to create ahead of time a variable to hold the results, but we have to create a very specific type of variable, to do that we use MScriptUtils. (This class is a crutch to cobble together python and API, and it still gives me headaches).

Now that we have the prerequisites, we can ask for the Shell IDs, this is an ordered list of the UV coordinates and which shell they belong to. If I use the Shell ID as a dictionary key, then I can iterate through the list of IDs and sort them into lists. This is where the defaultdict comes in handy. It eliminates the need to test if a key already exists buy assuming it does, as long as you tell the defaultdict what the default value is if the key doesn’t already exist.

Since I know my mesh name and I know the value of i maps to a UV coordinate, I can use string formatting to setup my return value.

rgkovach123:

I’ve tried getting this to work today but the code doesn’t execute fully.


import maya.api.OpenMaya as om
import maya.cmds as cmds
from collections import defaultdict

def getUVShells(shape, uvSet=None):
    
    uvSets = cmds.polyUVSet(shape, q=True, allUVSets=True)
    if not uvSet or uvSet not in uvSets:
        uvSet = cmds.polyUVSet(shape, q=True, cuv=True)[0]

    selectionList = om.MSelectionList()
    selectionList.add(shape)

    mDag = om.MDagPath()
    selectionList.getDagPath(0, mDag)
    
    meshFn = om.MFnMesh(mDag)
    
    uvShellArray = om.MIntArray()
    shells = om.MScriptUtil()
    shellsPtr = shells.asUintPtr()
    
    meshFn.getUvShellsIds(uvShellArray, shellsPtr, uvSet)
    
    uvShells = defaultdict(list)
    for i, shellId in enumerate(uvShellArray):
        uvShells[shellId].append('{0}.map[{1}]'.format(shape, i))
    
    return uvShells.values()
    
test = getUVShells("pSphere1")
print test

TypeError: getDagPath() takes exactly one argument (2 given)

Also I need to run it on a UV selection so the shape has to be retrieved from the components. Went with this:

shapes = pm.listRelatives(fullPath=True, parent=True, type="mesh")[0]

Not sure though if that’s the proper way though as I can’t really test the code without it halting :frowning:

I tried removing the name from getDagPath, and while the code does execute a few rows more, I get another error 3 rows below:

AttributeError: ‘module’ object has no attribute ‘MScriptUtil’

maya.OpenMaya and maya.api.OpenMaya are not the same thing. Try using the maya.OpenMaya instead.

MScriptUtil is obsolete in API 2.0.

Maya Python API 2.0 Reference
http://help.autodesk.com/cloudhelp/2016/ENU/Maya-SDK/py_ref/index.html

getting uv shells for only the selected components is do-able with a few slight tweaks. instead of adding the shape onto the MSelectionList, we can add the components.

 # API 1.0 example, may be slightly different in API 2.0.
    selectionList = om.MSelectionList()
    for face in selectedFaces:
        selectionList.add(face)

Here’s a quick rewrite to Maya api 2.0 plus some extra notes/comments and additions.
Maybe that helps?

import maya.api.OpenMaya as om2
import maya.cmds as cmds
from collections import defaultdict

def getUVShells(mesh, uvSet=None):
    """ Return UVs per shell ID for a given mesh.
    
    :return: list(list)
    :rtype: Return a list of UVs per shell.
    """
    
    # define which uvSet to process
    uvSets = cmds.polyUVSet(mesh, q=True, allUVSets=True)
    if not uvSet or uvSet not in uvSets:
        uvSet = cmds.polyUVSet(mesh, q=True, cuv=True)[0]

    # get the api dag path
    sel = om2.MSelectionList()
    sel.add(mesh)
    dagPath = sel.getDagPath(0)
    dagPath.extendToShape()
    
    # Note:
    # Maya returns the .map[id] on the transform instead of shape if you select it.
    # For convenience for now let's do the same (whether you want this is up to you)
    mesh = cmds.ls(dagPath.fullPathName(), long=True, o=True)[0]
    mesh = mesh.rsplit('|', 1)[0] # get the parent transform
    
    # get shell uv ids
    fnMesh = om2.MFnMesh(dagPath)
    uvCount, uvShellArray = fnMesh.getUvShellsIds(uvSet)
    
    # convert to a format we like it (per shell)
    uvShells = defaultdict(list)
    for i, shellId in enumerate(uvShellArray):
        uv = '{0}.map[{1}]'.format(mesh, i)
        uvShells[shellId].append(uv)
    
    # return a list per shell
    return uvShells.values()
    
if __name__ == '__main__':
    
    # create test
    sphere = cmds.polySphere()[0]
    cmds.polyAutoProjection()
    cmds.select('{0}.map[1]'.format(sphere), r=1)
    sel = cmds.ls(sl=1)
    shells = getUVShells(sel[0])
    
    # test: print each shell
    for shell in shells:
        print shell
    
    # test: convert each selected uv component to uv shell
    sel = cmds.ls(sl=1, flatten=True, long=True)
    selected_shells = []  
    for shell in shells:
        for component in sel:
            if component in shell:
                selected_shells.extend(shell)
                
    cmds.select(selected_shells, r=1)

Also for retrieving the shape from the selection can be done in multiple ways.
For one we know that the part in front of the dot is the node, after it are the components.
The tricky thing though is that Maya will return selected components as if its on the parent transform node; though I’ve found this to not be the case for 100% of the scenarios!
So relying on it is prone to break at a point.
Anyway, here are some more examples. You could also use the objects argument on the ls command.

sel = cmds.ls(sl=1)[0]
print sel
print cmds.listRelatives(sel, fullPath=True, parent=True, type="mesh")[0]
print cmds.listRelatives(fullPath=True, parent=True, type="mesh")[0]
print cmds.ls(sel,  o=True)[0]
print cmds.ls(sl=1, o=True)[0]

# this won't work since it returns the transform node in most cases
print sel.rsplit(".", 1)[0]

Sorry for the exceptionally late answer. I do appreciate your help even though I haven´t looked at this further for a couple months.

I only have two questions:
Say that I am developing in PyMEL and I do not want to have to import another module (Maya.cmds) - what changes needs to be done to the code?
Both your code Robert - and yours BigRoyNL - runs on Maya.cmds, so the ls command always returns a string. But in PyMEL ls returns an object.
The code breaks on the row where the selection is added into om.MSelectionList(). My first idea here is to just turn the class object into a string and feed it to the function, but that just feels awfully bad - even stupid!

My second question is in what Maya version API 2.0 was implemented? Roy´s code does not run in Maya 2012 so I assume 2013 was the first one with API 2.0. I have not tested Robert´s code in 2012.

If you’re going between PyMel and the API classes, you’re going to have to deal with the type ugliness that you mentioned. Either cast it to a string or use maya.cmds (or pm.cmds) for anything you want to pass to an API class.

The other option is to use the available PyMel attributes for accessing the underlying API classes for objects. PyMel stores (or allows easy access to) the underlying API classes for its objects. You can access the MDagPath of a PyNode with the method apimdagpath()

In the docs: http://download.autodesk.com/us/maya/2011help/pymel/advanced.html#api-classes-and-their-pynode-counterparts

You can access these by using the special methods apimobject, apihandle, apimdagpath, apimplug, and apimfn. ( Be aware that this is still considered internal magic, and the names of these methods are subject to change ):

[QUOTE=capper;27870]If you’re going between PyMel and the API classes, you’re going to have to deal with the type ugliness that you mentioned. Either cast it to a string or use maya.cmds (or pm.cmds) for anything you want to pass to an API class.

The other option is to use the available PyMel attributes for accessing the underlying API classes for objects. PyMel stores (or allows easy access to) the underlying API classes for its objects. You can access the MDagPath of a PyNode with the method apimdagpath()

In the docs: http://download.autodesk.com/us/maya/2011help/pymel/advanced.html#api-classes-and-their-pynode-counterparts[/QUOTE]

Thanks for the information.
I didn’t know that the Maya.cmds module was integrated into PyMEL! Handy!

I just realized that if I only want the shells which are active in my current UV selection, then I would need some additional code.
Like…

def getShells():
    selUV = pm.ls(selection=True, flatten=True)
    
    # code which gets all_shells - based of R.White's example
    
    shellList = []
    for shell in all_shells:
        for uv in sel:
            if uv in shell:
                if shell not in shellList:
                    shellList.append(shell)
    
    return shellList
shells = getShells()
print shells

But this is really slow. In fact, running those for-loops increases the processing time by over 50% compared to just returning all_shells
It would be so much easier if I could just iterate through the MeshUVs, ask them what shellID they belong to and then just get that shellID -set and place it into the shellList[].
But the MeshUV class doesn’t have any methods at all it seems.

NOTE: I want the return to be a list of the UV-shells so I can easily do pm.select(shells[0]) for example (or list of sets if Maya is clever enough to get all components inside a set() )

The getUvShellsIds function returns a flat list that contains a uv shell id for each uv index. Get the indices of the currently selected uvs, then look up the shell id for each using that flat list.

You still need the code to determine which uv ids comprise each shell, but this is probably the approach I’d try first for figuring out which shell each selected uv is in

I can’t test it right now but the general idea is:

uv_shell_ids = mfnMesh.getUvShellIds()
for item in pm.cmds.ls(sl=True, flatten=True): # cmds is faster for looping through large attribute selections
    shell_id = uv_shell_ids[int(item.split('[')[-1].split(']')[0])]

I appreciate all your help!

Final code:

import pymel.core as pm

# Converts UV selection to list of UV shells
# Returns list
def convertToShells():

    sel = pm.cmds.ls(selection=True, flatten=True)
    selObj = pm.ls(selection=True, objectsOnly=True)[0]

    # Create uvShellIds -array
    uvShellIDs, uvShellArray = selObj.getUvShellsIds()
    
    selShellsIDs = set() # Set with selected shells' IDs

    # Get shell IDs for each UV in selection
    for uv in sel:
        i = int( uv.split('[')[-1].split(']')[0] ) # Get UV index
        selShellsIDs.add(uvShellIDs[i])

    # Get selected shells' UVs as a dictionary
    uvShells = defaultdict(list)
    for i, shellID in enumerate(uvShellIDs):
        if shellID not in selShellsIDs:
            continue
        else:
            uv = "%s.map[%s]"%(selObj, i)
            uvShells[shellID].append(uv)
    
    # Return as list
    return uvShells.values()

shells = convertToShells()

Off-topic:
Is there any good reason to use delimiter-separated words compared to letter-case separated words or is it just a matter of taste?

Python style is usually to prefer under_score_names to camelCaseNames, but to use PascalCaseNames for classes

BTW.: You may get some minor perf benefits by keeping all your dictionary as integer > set(integer) mappings and using a generator to get the .map values back out, since any set actions you do will be a bit quicker if the sets contain integers instead of strings. It will vary depending on what use you make of the information, though, so if the above works for you that’s great.

[QUOTE=Theodox;27896]Python style is usually to prefer under_score_names to camelCaseNames, but to use PascalCaseNames for classes

BTW.: You may get some minor perf benefits by keeping all your dictionary as integer > set(integer) mappings and using a generator to get the .map values back out, since any set actions you do will be a bit quicker if the sets contain integers instead of strings. It will vary depending on what use you make of the information, though, so if the above works for you that’s great.[/QUOTE]

I see.
I will have a look at those suggestions tomorrow. But for now I am kinda happy: The new function is approx 200 times faster than my old one.