Wednesday 6 April 2011

Using the Maya MScriptUtil class in Python

Been using some maya and python and discovered there was no way to access some of the MImage methods as they use pass by reference to return values. This after some investigation lead me to the MScriptUtil class.

Now a couple of things struck me when reading the documents, the first being
"This class is cumbersome to use but it provides a way of building parameters and accessing return values for methods which would normally not be scriptable".
To my mind this reads as
"This is a hack but we couldn't be arsed to re-factor loads of our code to add proper accesors and mutators"

On further reading you get even more of a sense of that. What I wanted to do was to access the width and height of an MImage and access the RGB(A) data of the MImage and the only way of really doing this within python is to use the MScriptUtil Class.

MImage Class Design
I decided to wrap up all the functionality I needed into a simple python class which could be re-used within maya, the basic class design is as follows

The class will be constructed with a filename and will automatically read the image file and grab the image dimensions as well as a pointer to the data.

import maya.OpenMaya as om
import sys

class MayaImage :
 """ The main class, needs to be constructed with a filename """
 def __init__(self,filename) :
  """ constructor pass in the name of the file to load (absolute file name with path) """
  # create an MImage object
  self.image=om.MImage()
  # read from file MImage should handle errors for us so no need to check
  self.image.readFromFile(filename)
  # as the MImage class is a wrapper to the C++ module we need to access data
  # as pointers, to do this use the MScritUtil helpers
  self.scriptUtilWidth = om.MScriptUtil()
  self.scriptUtilHeight = om.MScriptUtil()

  # first we create a pointer to an unsigned in for width and height
  widthPtr = self.scriptUtilWidth.asUintPtr()
  heightPtr = self.scriptUtilHeight.asUintPtr()
  # now we set the values to 0 for each
  self.scriptUtilWidth.setUint( widthPtr, 0 )
  self.scriptUtilHeight.setUint( heightPtr, 0 )
  # now we call the MImage getSize method which needs the params passed as pointers
  # as it uses a pass by reference
  self.image.getSize( widthPtr, heightPtr )
  # once we get these values we need to convert them to int so use the helpers
  self.m_width = self.scriptUtilWidth.getUint(widthPtr)
  self.m_height = self.scriptUtilHeight.getUint(heightPtr)
  # now we grab the pixel data and store
  self.charPixelPtr = self.image.pixels()
  # query to see if it's an RGB or RGBA image, this will be True or False
  self.m_hasAlpha=self.image.isRGBA()
  # if we are doing RGB we step into the image array in 3's
  # data is always packed as RGBA even if no alpha present
  self.imgStep=4
  # finally create an empty script util and a pointer to the function
  # getUcharArrayItem function for speed
  scriptUtil = om.MScriptUtil()
  self.getUcharArrayItem=scriptUtil.getUcharArrayItem



Initially the class was designed to check to see if alpha was present and determine if the data was packed as RGB or RGBA and step through the packed data accordingly, however on further reading of the documents I discovered

"The image is stored as an uncompressed array of pixels, that can be read and manipulated directly. For simplicity, the pixels are stored in a RGBA format (4 bytes per pixel)".
So this was not required and was removed.

Using MScriptUtil
There are several ways to use MScriptUtil, the various constructors allow us to generate an object by passing in an object as a reference value, or we can create an instance of the class and then associate an object as a reference.

Initially I generated one MScriptUtil class, and associated the pointers from an already instantiated object. However this didn't work correctly. After reading the help I found the following note


So if you need to use two pointers at the same time you need to create two MScriptUtil objects.
self.scriptUtilWidth = om.MScriptUtil()
self.scriptUtilHeight = om.MScriptUtil()

# first we create a pointer to an unsigned in for width and height
widthPtr = self.scriptUtilWidth.asUintPtr()
heightPtr = self.scriptUtilHeight.asUintPtr()
Next we set the values to 0 for both the pointers, whilst this is not required, it make sure when we load the actual values if nothing is returned we have a null value.

Next a call to the MImage getSize method is called


As you can see in this method both the width and the height values are passed by reference. The code below passes the new pointers we have created using the script util class into the getSize method and these will be associated.

self.scriptUtilWidth.setUint( widthPtr, 0 )
self.scriptUtilHeight.setUint( heightPtr, 0 )
self.image.getSize( widthPtr, heightPtr )
Finally we need to extract the values that the pointers are pointing to, and load them into our python class which is done in the following code

self.m_width = self.scriptUtilWidth.getUint(widthPtr)
self.m_height = self.scriptUtilHeight.getUint(heightPtr)

See all this code could be avoided if MImage had getWidth and getHeigh accessor methods!
Now for some speedups

To access the pixel data we need to grab the array of data from the MImage class, this is done with the following method call

self.charPixelPtr = self.image.pixels()

The help says that
"Returns a pointer to the first pixel of the uncompressed pixels array. This array is tightly packed, of size (width * height * depth * sizeof( float)) bytes".

So we also have a pointer that we need to convert into a python data type. This can be done with the getUcharArrayItem however this would need to be created each time we try to access the data.

In python however it is possible to create a reference to a function / method in the actual code. This is done by assigning a variable name to a function and then using this instead of the actual function call. This can significantly speed up methods as the python interpretor doesn't have to lookup the method each time.

The following code shows this and the pointer to the method is stored as part of the class
scriptUtil = om.MScriptUtil()
self.getUcharArrayItem=scriptUtil.getUcharArrayItem
getPixels
To get the pixel data we need to calculate the index into the pointer array (1D) as a 2D x,y co-ordinate. This is a simple calculation as follows


index=(y*self.m_width*4)+x*4
In this case we hard code the 4 as the MImage help states that the data is always stored as RGBA if this were not the case we would have to query if the data contained an alpha channel and make the step 3 or 4 depending upon this.

The complete method is as follows
def getPixel(self,x,y) :
 """ get the pixel data at x,y and return a 3/4 tuple depending upon type """
 # check the bounds to make sure we are in the correct area
 if x<0 or x>self.m_width :
  print "error x out of bounds\n"
  return
 if y<0 or y>self.m_height :
  print "error y our of bounds\n"
  return
 # now calculate the index into the 1D array of data
 index=(y*self.m_width*4)+x*4
 # grab the pixels
 red = self.getUcharArrayItem(self.charPixelPtr,index)
 green = self.getUcharArrayItem(self.charPixelPtr,index+1)
 blue = self.getUcharArrayItem(self.charPixelPtr,index+2)
 alpha=self.getUcharArrayItem(self.charPixelPtr,index+3)
 return (red,green,blue,alpha)

As you can see the index is calculated then the method saved earlier is called to grab the actual value at the index location (Red) then index+1 (green) index+2 (blue) and index+3 (alpha). For convenience I also wrote a getRGB method as shown

def getRGB(self,x,y) :
    r,g,b,a=getPixel(x,y)
    return (r,g,b)

Other methods shown below are also added to the class to allow access to the attributes, whilst python allows access to these class attributes directly, when porting C++ code usually we will have getWidth / getHight style methods so I just added them.

def width(self) :
    """ return the width of the image """
    return self.m_width

def height(self) :
    """ return the height of the image """
    return self.m_height

def hasAlpha(self) :
    """ return True is the image has an Alpha channel """
    return self.m_hasAlpha


Using the Class
The following example prompts the used for a file name then loads the image, anything in the red channel of the image with a pixel value greater than 10 is used to generate a cube of height r/10

import maya.OpenMaya as om
import maya.cmds as cmds



basicFilter = "*.*"

imageFile=cmds.fileDialog2(caption="Please select imagefile",
             fileFilter=basicFilter, fm=1)


img=MayaImage(str(imageFile[0]))
print img.width()
print img.height()a
xoffset=-img.width()/2
yoffset=-img.height()/2

for y in range (0,img.height()) :
 for x in range(0,img.width()) :
  r,g,b,a=img.getPixel(x,y)
  if r > 10 :
   cmds.polyCube(h=float(r/10))
   cmds.move(xoffset+x,float(r/10)/2,yoffset+y)

Using the following image

We produce the following