Showing posts with label Maya API. Show all posts
Showing posts with label Maya API. Show all posts

Wednesday, 21 March 2012

Using the Maya API with Qt Creator

I've just started writing next terms Maya API lectures and decided it would be a good idea to use Qt Creator as the ide for development as it's our main IDE. It also has the advantage of automatically creating the make file required for the projects and helping to make multi platform builds easier to manage.

Qt Creator Project file
qmake and Qt Creator both use the .pro file for the projects. In this case I'm going to create builds for both Linux 64bit and Mac OSX. qmake uses the pre-fix values linux-g+-64 for all flags relating to linux 64 bit and macx for Mac OSX. 

The following .pro file shows how this is done.
# This file is split into Three sections
# The first configures Qt and the source files for all platforms
# The second is the linux build
# The third the mac build
# (if your using windows you will need to add a fourth one!)
# first lets remove Qt core and gui not going to need it
QT       -= core gui
# set the variable for the Maya Location, this will be different depending
# upon the platform / version of maya used
TARGET = HelloMaya
# for for mac we need a bundle so change the name
macx:TARGET=HelloMaya.bundle
# here we add the source files (and headers if required)
SOURCES+=HelloMaya.cpp
# these are defines required by Maya to re-define some C++
# stuff, we will add some more later to tell what platform
# we are on as well
DEFINES+=REQUIRE_IOSTREAM \
         _BOOL
# These are the maya libs we need to link to, this will change depending
# upon which maya framework we use, just add them to the end of
# this list as required and they will be added to the build
MAYALIBS=-lOpenMaya \
        -lFoundation

# now tell linux we need to build a lib
linux-g++-64:TEMPLATE = lib

# this tells qmake where maya is
linux-g++-64:MAYALOCATION=/usr/autodesk/maya2011-x64/
# under linux we need to use the version of g++ used to build maya
# in this case g++412
linux-g++-64:QMAKE_CXX = g++412
# set the include path for linux
linux-g++-64:INCLUDEPATH += $$MAYALOCATION/include \
                        /usr/X11R6/include
# set which libs we need to include
linux-g++-64:LIBS += -L$$MAYALOCATION/lib \
                   $$MAYALIBS
# tell maya we're building for linux
linux:DEFINES+=linux

# tell maya we're building for Mac
macx:DEFINES+=OSMac_
macx:MAYALOCATION=/Applications/Autodesk/maya2011
macx:CONFIG -= app_bundle
macx:INCLUDEPATH+=$$MAYALOCATION/devkit/include
# under mac we need to build a bundle, to do this use
# the -bundle flag but we also need to not use -dynamic lib so
# remove this
macx:LIBS +=-bundle
mac:LIBS -=-dynamiclib

macx:LIBS += -L$$MAYALOCATION/Maya.app/Contents/MacOS \
             $$MAYALIBS
There are several sections to the .pro file above, mainly these are to make it easier to add to the project when needed. The two main distinctions between the linux and the Mac OSX version are how the plugin should be generated. Under linux the plugin is a normal shared object file (.so), however the mac version requires a bundle. To do this the -bundle flag is used and we also need to ensure that the extension .bundle is added to the Target.

Finally we need to add some defines to the build, depending upon the platform we need to either use -DOSMac_ or -Dlinux as well as the two flags REQUIRE_IOSTREAM which includes the correct iostream library for using std::cout and _BOOL to re-define bool.

Test Program
The following test program will send a message to both the shell maya was run from and the maya command window.

#include <maya/MSimple.h>
#include <maya/MIOStream.h>
#include <maya/MGlobal.h>

// This is a macro to create a simple command
// the compiler expands it to a bunch of code

DeclareSimpleCommand( HelloMaya , "NCCA", "Maya 2011"); 

MStatus HelloMaya::doIt( const MArgList& )
{
  std::cout<<"This should come from the shell\n";
  MGlobal::displayInfo("Hello Maya in the maya command shell");
  return MS::kSuccess;
}
Once this program is build depending upon the platform there should be a plugin ready to load in the maya plugin manager. The image below show this and the info set from the Command above
We now have two options to execute the loaded command. In the mel window we can invoke the command as follows, For the mel version
And for the python version we need to import the maya.cmds module first.


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