+1 tag for Google+

Saturday, 20 May 2017

K-Means Clustering in Maya (Python)


Watch this video on Vimeo instead.

In this video, I show the results of me implementing a k-means clustering algorithm coded in Python, inside Maya.

After having read about what the K-means clustering algorithm is, and how it works, I challenged myself to create a Python function to perform that, using primitive objects in Maya to represent data points and clusters centroids.

After I wrote the code, I discovered SciKit Learn python library also has a k-means algorithm. However, there is more satisfaction to be had by the fact that I managed to code it myself, rather than just using whatever has already been written out there.

Monday, 24 April 2017

pymel.core Versus maya.cmds

Is PyMel slower than Maya's cmds module?

PyMel is an alternative to Maya's native maya.cmds. It was developed and maintained by Luma Pictures. It is open source, and lives in GitHub here: https://github.com/LumaPictures/pymel

Pymel was so intuitive and successful that Autodesk has shipped with pymel alongside its maya.cmds.

Today I stumbled across a discussion:
https://groups.google.com/forum/#!topic/python_inside_maya/n8WUdQHhw1k

Someone was asking if pymel performs as quickly as native maya.cmds. Another person referenced this article: http://www.macaronikazoo.com/?p=271

The author (Hamish McKenzie) did a comparison with his following code:
import time
import maya.cmds as cmd
MAX = 1000
start = time.clock()
for n in xrange( MAX ):
 cmd.ls()
print 'time taken %0.3f' % (time.clock()-start)

from pymel.core import ls
start = time.clock()
for n in xrange( MAX ):
 ls()  #NOTE: this is using pymel’s wrapping of the ls command
print 'time taken %0.3f' % (time.clock()-start)

When I run the code I get the following print-out.
time taken 3.983
time taken 77.059

The code lists all nodes in the current scene 1000 times with maya.cmds and pymel, then prints the time each method takes. The article was written in 2010. Hamish reports a speed difference of 350 times! It is now 2017, the pymel library has probably undergone iterations of improvements and optimisation. From the figures above, the execution time difference on my work machine is 19.34 times.

After reading that post, I went to modify his code to include a few more actions. Create a cube, rename the cube for a user-specified number of times, list all objects in the scene, then delete all the cubes.

The code now reads like this:

import time
import maya.cmds as mc
from pymel.core import *

def testMayaCmdsPymel(numIterList):
    '''
    a function to compare a time taken to perform similar 
    set of actions using maya.cmds and pymel.
    
    numIterList - a list consisting of the number of iterations
                    which to run the comparison tests on
    '''
    def testCmds(numIter):
        MAX = numIter
        start = time.clock()
        objList = []
        
        print '-- maya.cmds --'
        for n in xrange( MAX ):
            obj = mc.polyCube()[0]
            objList.append(mc.rename(obj, 'cube1'))
            mc.ls()
        mc.delete(objList)
        timeCmds = time.clock()-start
        print 'time taken %0.3f' % (timeCmds)
        return timeCmds

    def testPymel(numIter):
        MAX = numIter
        start = time.clock()
        objList = []
        print '-- pymel --'
        for n in xrange( MAX ):
            # using pymel’s wrapping of the polyCube() command
         obj = polyCube()[0]
         # using pymel's wrapping of rename() command
         objList.append(rename(obj.name(), 'cube1')) 
         ls() # using pymel’s wrapping of the ls() command
        delete(objList) # using pymel’s wrapping of the delete() command
        timePymel = time.clock()-start
        print 'time taken %0.3f' % (timePymel)
        return timePymel
        
    timeDiffDict = {}
    mainStart = time.clock()
    for x in numIterList:
        print 'duplicating %i objects' % (x)
        timeCmds = testCmds(x)
        timePymel = testPymel(x)
        timeDiff = float(timePymel)/float(timeCmds)
        timeDiffDict[x] = [timeCmds, timePymel, timeDiff]
        print 'difference: %0.3f times\n' % (timeDiff)

    print 'Results of test series:'
    print 'num iterations\ttime cmds\ttime pymel\ttime difference'
    print '--------------\t---------\t----------\t---------------\t'
    for x in sorted(timeDiffDict.keys()):
        print '%04i objects\t\t%0.3f s\t\t%0.3f s\t\t%0.3f times' % \
                (x, timeDiffDict[x][0], timeDiffDict[x][1], timeDiffDict[x][2])
    print '\ntotal time taken:', time.clock()-mainStart

Invoking the function with the code:
testMayaCmdsPymel([10, 15, 20])

I get the following print-out:
Results of test series:
num iterations time cmds time pymel time difference
-------------- --------- ---------- ---------------
0010 objects 0.084 s 0.837 s 9.999 times
0015 objects 0.088 s 1.248 s 14.216 times
0020 objects 0.115 s 1.730 s 15.005 times

total time taken: 4.12330471431

Invoking the function with the code:
testMayaCmdsPymel([10, 50, 100, 200, 300, 400, 500, 1000])

I get the following output:
Results of test series:
num iterations time cmds time pymel time difference
-------------- --------- ---------- ---------------
0010 objects 0.085 s 0.847 s 9.980 times
0050 objects 0.294 s 4.299 s 14.628 times
0100 objects 0.610 s 9.391 s 15.391 times
0200 objects 1.280 s 20.519 s 16.030 times
0300 objects 1.963 s 34.734 s 17.692 times
0400 objects 2.617 s 50.107 s 19.150 times
0500 objects 3.484 s 68.700 s 19.720 times
1000 objects 8.033 s 190.390 s 23.700 times

total time taken: 397.417362303

I see that the time difference in execution generally increases, even though the increase starts to slow down after 300 objects. However, the increase in speed that we get from maya.cmds versus pymel is at least 10.8 times, and in the worst cases 24.5 times!

I know it sounds really late to be discovering this, but it is never too late to switch. I really like how pymel makes maya.cmds wrapper more pythonic, readable and consistent to work with.

Now I am deliberating whether to continue with pymel or completely jump back to maya.cmds. My inclination is to use both, native maya.cmds for operations I know will iterate across huge number of objects, and pymel for other less intensive tasks.

Friday, 24 February 2017

New Documentary: Hollywood's GreatestTrick



I just became aware of a 2-day old article about a documentary available in full, showing the current situation of visual effects workers in Hollywood's film industry.

Hollywood's Greatest Trick

It talks about how the industry has been able to continue to raise the bar on the quality and increased number of visual effects shots in an average Hollywood film.

The revenue generated by these films are also discussed. This is put in context when compared to the overheads and marginal profits that can be made from film to film. The documentary then shows that on top of struggling just to break even, what visual effects vendors and companies are up against when bidding for movies to be awarded to them from a limited and fixed pool of client studios.

At the artists level, the film talks about how artists are expected to work long hours, and their passion for their work was being taken for granted, oftentimes for the benefit of the clients.

More and more companies are hiring on a per-project basis, and they set-up offices in places that allow them to operate at the cheapest costs (labour costs and tax breaks offered to film-related business activities). Artists are forced to move with the companies, in order to find work, and that may only last for a few months to a year, as most companies hire on contract terms nowadays.

If you have friends, family or relatives working in the visual effects industry, or if you just enjoy Hollywood films, or maybe you enjoy watching behind the scenes featurettes, fascinated by what visual effects can do, it is definitely worth your time watching this film.

It is my hope that more people become aware that behind the beautifully magical visuals of Hollywood films lie not just advanced image manipulation technology and software, but also a huge team of highly skilled workers who work the software and craft their shots.

In the middle of the article, there is an excellent infographics animation that clearly introduces the major stages of how a visual effects shot is created. It is really enlightening and can be easily understood by the lay person. I highly recommend watching this!

Sunday, 15 January 2017

Maya Expressions Part 04 - Twisting Cubes



Video also available on Vimeo.com.

In Maya Expressions Part 4, we start to use expressions across multiple objects. This results in an interesting rig where the objects driven by expression can arrange themselves in interesting ways.

In my set up, every cube has a slight offset in translation and rotation to the one before, As the first cube moves along its x-axis, the spacing between cubes and each of their rotations change and react to reflect that change.

In the video I also show how I structure my expression so that as much as possible, the same code can be applied to each cube with minimal alteration. With the standardised code and minimal change for each of the cube, it will become relatively easy to replicate this behaviour to as many extra cubes as required without hassle.

Maya Expressions is my running series introducing and using the powerful expression function in Maya to achieve tasks that would otherwise be very manual or inefficient to set up and manage.

Watch the previous videos!
Maya Expressions Part 1
Maya Expressions Part 2
Maya Expressions Part 3

Tuesday, 13 December 2016

Python Sorting Part 2: Classes and Attributes

Python Sorting with Classes and Attributes

# Here we define a class, containing student names
# each student contains an list attribute of varying from 1 to 7 in length
# populated by random integer values from 0 to 500


import random # imports the random module

# declares the class object
class myClass(object):
    def __init__(self, inputName):
        self.myName = inputName # creates a myName attribute
        # create a list of random length
        self.myList = [random.randint(0,500) \
                        for x in range(random.randint(1, 7))]
        
    def greet(self):
        # prints out name and list of values the instance holds
        print 'Hi I am {}, my list is {}'.format(self.myName, self.myList)

random.seed(0) # providing a seed to generate consistent random values
# Now we create instances of myClass for 5 students,
# and store the result as a list in studentNames
studentNames = ('tommy', 'sally', 'summer', 'jane', 'jonathan')
# For each item in studentNames, create a myClass instance
classList = [myClass(x) for x in studentNames]

# What kind of data does the list contain? Let's find out
print classList
# Result: [<__main__.myClass object at 0x0000000057D90470>,
# <__main__.myClass object at 0x0000000057D90320>,
# <__main__.myClass object at 0x0000000057D90240>,
# <__main__.myClass object at 0x0000000057D906A0>,
# <__main__.myClass object at 0x0000000057D906D8>] # 

# They are instances of myClass, that each hold name attribute values of
# 'tommy', 'sally', 'summer', 'jane', 'jonathan' in that order respectively
# They will also each have a list containing 1 to 7 items which are integers

# Loop through each myClass instance and call their greet() method
for person in classList:
    person.greet()
# Result:
# Hi I am tommy, my list is [379, 210, 129, 256, 202, 392]
# Hi I am sally, my list is [238, 292, 454]
# Hi I am summer, my list is [141, 378, 309, 125]
# Hi I am jane, my list is [492, 405, 451, 155, 365, 450, 342]
# Hi I am jonathan, my list is [50, 217, 306, 457] #

# return just the names stored in myClass.myName attribute
[classList[x].myName for x in range(len(classList))]
# Result: ['tommy', 'sally', 'summer', 'jane', 'jonathan'] # 

# sorting the classes (and returning their myName values 
# by the length of their myName strings sorted([x.myName for x in classList], key=len)
# Result: ['jane', 'tommy', 'sally', 'summer', 'jonathan'] # 
# 'jane' is the shortest string and 'jonathan' is the longest string, correct ascending order

# return the name stored in myClass.myName attribute
# in the order of the length of the list contained in their myList attribute
y.myName for y in sorted([x for x in classList], 
  key=lambda(student): len(student.myList))]
# Result: ['sally', 'summer', 'jonathan', 'tommy', 'jane'] # 
# 'sally' has a myList attribute containing 3 items
# 'jane' has a myList attribute containing 7 items
# correct ascending order

# we can find out the sum of the items in each instance's list
[(x.myName, sum(x.myList)) for x in classList]
# Result: [('tommy', 1568),
 ('sally', 984),
 ('summer', 953),
 ('jane', 2660),

 ('jonathan', 1030)] # 


# Now we shall try to sort the class instances 
# ordered by the sum of their list items
[y.myName for y in sorted([x for x in classList], key=lambda(inst):sum(inst.myList))]

# Result: ['summer', 'sally', 'jonathan', 'tommy', 'jane'] # 
# 'summer' has a list with the sum of 953
# 'jane' has a list with the sum of 2660
# This is the correct ascending order according to each instance's sum

# When we enclose list comprehensions like that it is very confusing 
# and may be difficult to read.

# Here's that statement again, with colour coded blocks that we can examine in parts
[y.myName for y in sorted([x for x in classList]
key=lambda(inst):sum(inst.myList))]

# Let's break it down, starting from the inner most list comprehension:
[x for x in classList]
# This gives us the list of myClass instances in the order of the 
# studentNames list, which the class instances were created

# Expanding from that, we come to this statement
sorted([x for x in classList], key=lambda(inst):sum(inst.myList))
# We sort this list of class instances by using the key argument
# The lambda function returns to the sum of the instance's myList items
# the inst variable passed into the lambda function in this case will contain
# the myClass instance as the list of instances are passed in to be sorted

# With that sum's value as the key argument, the sorted([x for x...]) part of the statement 
# will return a list containing the class instances ordered by sum of the list's items

# Finally, the outermost list comprehension
[y.myName for y in sorted([x for x...], key=lambda...)]
# This simply take the already ordered list of class instances 
# (ordered by the sum of each instance's list items)
# and return the myName attribute