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
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.getUcharArrayItemgetPixels
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*4In 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
Do you have an example where you can edit the RGBA/Depth channel and save it to a new image?
ReplyDeleteThanks for this! I've been struggling with MImage all day, but this post cleared it right up!
ReplyDelete