Skip to content

Instantly share code, notes, and snippets.

@tbttfox
Last active December 22, 2024 10:20
Show Gist options
  • Save tbttfox/9ca775bf629c7a1285c27c8d9d961bca to your computer and use it in GitHub Desktop.
Save tbttfox/9ca775bf629c7a1285c27c8d9d961bca to your computer and use it in GitHub Desktop.
Blazing fast maya api types to Numpy conversion
from maya import OpenMaya as om
import numpy as np
from ctypes import c_float, c_double, c_int, c_uint
_CONVERT_DICT = {
om.MPointArray: (float, 4, c_double, om.MScriptUtil.asDouble4Ptr),
om.MFloatPointArray: (float, 4, c_float , om.MScriptUtil.asFloat4Ptr),
om.MVectorArray: (float, 3, c_double, om.MScriptUtil.asDouble3Ptr),
om.MFloatVectorArray: (float, 3, c_float , om.MScriptUtil.asFloat3Ptr),
om.MDoubleArray: (float, 1, c_double, om.MScriptUtil.asDoublePtr),
om.MFloatArray: (float, 1, c_float , om.MScriptUtil.asFloatPtr),
om.MIntArray: (int , 1, c_int , om.MScriptUtil.asIntPtr),
om.MUintArray: (int , 1, c_uint , om.MScriptUtil.asUintPtr),
}
def _swigConnect(mArray, count, util):
'''
Use an MScriptUtil to build SWIG array that we can read from and write to.
Make sure to get the MScriptUtil from outside this function, otherwise
it may be garbage collected
The _CONVERT_DICT holds {mayaType: (pyType, numComps, cType, ptrType)} where
pyType: The type that is used to fill the MScriptUtil array.
numComps: The number of components. So a double4Ptr would be 4
cType: The ctypes type used to read the data
ptrType: An unbound method on MScriptUtil to cast the pointer to the correct type
I can still call that unbound method by manually passing the usually-implicit
self argument (which will be an instance of MScriptUtil)
'''
pyTyp, comps, ctp, ptrTyp = _CONVERT_DICT[type(mArray)]
cc = (count * comps)
util.createFromList([pyTyp()] * cc, cc)
#passing util as 'self' to call the unbound method
ptr = ptrTyp(util)
mArray.get(ptr)
if comps == 1:
cdata = ctp * count
else:
# Multiplication follows some strange rules here
# I would expect (ctype*3)*N to be an Nx3 array (ctype*3 repeated N times)
# However, it gets converted to a 3xN array
cdata = (ctp * comps) * count
# int(ptr) gives the memory address
cta = cdata.from_address(int(ptr))
# This makes numpy look at the same memory as the ctypes array
# so we can both read from and write to that data through numpy
npArray = np.ctypeslib.as_array(cta)
return npArray, ptr
def mayaToNumpy(mArray):
''' Convert a maya array to a numpy array
Parameters
----------
ary : MArray
The maya array to convert to a numpy array
Returns
-------
: np.array :
A numpy array that contains the data from mArray
'''
util = om.MScriptUtil()
count = mArray.length()
npArray, _ = _swigConnect(mArray, count, util)
return np.copy(npArray)
def numpyToMaya(ary, mType):
''' Convert a numpy array to a specific maya type array
Parameters
----------
ary : np.array
The numpy array to convert to a maya array
mType : type
The maya type to convert to out of: MPointArray, MFloatPointArray, MVectorArray,
MFloatVectorArray, MDoubleArray, MFloatArray, MIntArray, MUintArray
Returns
-------
: mType :
An array of the provided type that contains the data from ary
'''
util = om.MScriptUtil()
# Add a little shape checking
comps = _CONVERT_DICT[mType][1]
if comps == 1:
if len(ary.shape) != 1:
raise ValueError("Numpy array must be 1D to convert to the given maya type")
else:
if len(ary.shape) != 2:
raise ValueError("Numpy array must be 2D to convert to the given maya type")
if ary.shape[1] != comps:
msg = "Numpy array must have the proper shape. Dimension 2 has size {0}, but needs size {1}"
raise ValueError(msg.format(ary.shape[1], comps))
count = ary.shape[0]
mArray = mType(count)
npArray, ptr = _swigConnect(mArray, count, util)
np.copyto(npArray, ary)
return mType(ptr, count)
_NTYPE_DICT={
om.MFnNumericData.kInvalid: (om.MDataHandle.asDouble, om.MDataHandle.setDouble),
om.MFnNumericData.kFloat: (om.MDataHandle.asDouble, om.MDataHandle.setDouble),
om.MFnNumericData.kDouble: (om.MDataHandle.asDouble, om.MDataHandle.setDouble),
om.MFnNumericData.kByte: (om.MDataHandle.asInt, om.MDataHandle.setInt),
om.MFnNumericData.kChar: (om.MDataHandle.asChar, om.MDataHandle.setChar),
om.MFnNumericData.kShort: (om.MDataHandle.asShort, om.MDataHandle.setShort),
om.MFnNumericData.kInt: (om.MDataHandle.asInt, om.MDataHandle.setInt),
#om.MFnNumericData.kInt64: (om.MDataHandle.asInt, om.MDataHandle.setInt64),
om.MFnNumericData.kAddr: (om.MDataHandle.asInt, om.MDataHandle.setInt),
om.MFnNumericData.kLong: (om.MDataHandle.asInt, om.MDataHandle.setInt),
om.MFnNumericData.kBoolean: (om.MDataHandle.asBool, om.MDataHandle.setBool),
om.MFnNumericData.k2Short: (om.MDataHandle.asShort2, om.MDataHandle.set2Short),
om.MFnNumericData.k2Long: (om.MDataHandle.asInt2, om.MDataHandle.set2Int),
om.MFnNumericData.k2Int: (om.MDataHandle.asInt2, om.MDataHandle.set2Int),
om.MFnNumericData.k3Short: (om.MDataHandle.asShort3, om.MDataHandle.set3Short),
om.MFnNumericData.k3Long: (om.MDataHandle.asInt3, om.MDataHandle.set3Int),
om.MFnNumericData.k3Int: (om.MDataHandle.asInt3, om.MDataHandle.set3Int),
om.MFnNumericData.k2Float: (om.MDataHandle.asFloat2, om.MDataHandle.set2Float),
om.MFnNumericData.k2Double: (om.MDataHandle.asDouble2, om.MDataHandle.set2Double),
om.MFnNumericData.k3Float: (om.MDataHandle.asFloat3, om.MDataHandle.set3Float),
om.MFnNumericData.k3Double: (om.MDataHandle.asDouble3, om.MDataHandle.set3Double),
}
_DTYPE_DICT = {
om.MFn.kPointArrayData: (om.MFnPointArrayData, om.MPointArray),
om.MFn.kDoubleArrayData: (om.MFnDoubleArrayData, om.MDoubleArray),
om.MFn.kFloatArrayData: (om.MFnFloatArrayData, om.MFloatArray),
om.MFn.kIntArrayData: (om.MFnIntArrayData, om.MIntArray),
om.MFn.kUInt64ArrayData: (om.MFnUInt64ArrayData, om.MPointArray),
om.MFn.kVectorArrayData: (om.MFnVectorArrayData, om.MVectorArray),
}
def getNumpyAttr(attrName):
''' Read attribute data directly from the plugs into numpy
This function will read most numeric data types directly into numpy arrays
However, some simple data types (floats, vectors, etc...) have api accessors
that return python tuples. These will not be turned into numpy arrays.
And really, if you're getting simple data like that, just use cmds.getAttr
Parameters
----------
attrName : str or om.MPlug
The name of the attribute to get (For instance "pSphere2.translate", or "group1.pim[0]")
Or the MPlug itself
Returns
-------
: object :
The numerical data from the provided plug. A np.array, float, int, or tuple
'''
if isinstance(attrName, basestring):
sl = om.MSelectionList()
sl.add(attrName)
plug = om.MPlug()
sl.getPlug(0, plug)
elif isinstance(attrName, om.MPlug):
plug = attrName
#First just check if the data is numeric
mdh = plug.asMDataHandle()
if mdh.isNumeric():
# So, at this point, you should really just use getattr
ntype = mdh.numericType()
if ntype in _NTYPE_DICT:
return _NTYPE_DICT[ntype][0](mdh)
elif ntype == om.MFnNumericData.k4Double:
NotImplementedError("Haven't implemented double4 access yet")
else:
raise RuntimeError("I don't know how to access data from the given attribute")
else:
# The data is more complex than a simple number.
try:
pmo = plug.asMObject()
except RuntimeError:
# raise a more descriptive error. And make sure to actually print the plug name
raise RuntimeError("I don't know how to access data from the given attribute")
apiType = pmo.apiType()
# A list of types that I can just pass to mayaToNumpy
if apiType in _DTYPE_DICT:
fn, dtype = _DTYPE_DICT[apiType]
fnPmo = fn(pmo)
ary = fnPmo.array()
return mayaToNumpy(ary)
elif apiType == om.MFn.kComponentListData:
fnPmo = om.MFnComponentListData(pmo)
mirs = []
mir = om.MIntArray()
for attrIndex in xrange(fnPmo.length()):
fnEL = om.MFnSingleIndexedComponent(fnPmo[attrIndex])
fnEL.getElements(mir)
mirs.append(mayaToNumpy(mir))
return np.concatenate(mirs)
elif apiType == om.MFn.kMatrixData:
fnPmo = om.MFnMatrixData(pmo)
mat = fnPmo.matrix()
return mayaToNumpy(mat)
else:
apiTypeStr = pmo.apiTypeStr()
raise NotImplementedError("I don't know how to handle {0} yet".format(apiTypeStr))
raise NotImplementedError("Fell all the way through")
def setNumpyAttr(attrName, value):
''' Write a numpy array directly into a maya plug
This function will handle most numeric plug types.
But for single float, individual point, etc.. types, consider using cmds.setAttr
THIS DOES NOT SUPPORT UNDO
Parameters
----------
attrName : str or om.MPlug
The name of the attribute to get (For instance "pSphere2.translate", or "group1.pim[0]")
Or the MPlug itself
value : int, float, tuple, np.array
The correctly typed value to set on the attribute
'''
if isinstance(attrName, basestring):
sl = om.MSelectionList()
sl.add(attrName)
plug = om.MPlug()
sl.getPlug(0, plug)
elif isinstance(attrName, om.MPlug):
plug = attrName
else:
raise ValueError("Data must be string or MPlug. Got {0}".format(type(attrName)))
#First just check if the data is numeric
mdh = plug.asMDataHandle()
if mdh.isNumeric():
# So, at this point, you should really just use setattr
ntype = mdh.numericType()
if ntype in _NTYPE_DICT:
_NTYPE_DICT[ntype][1](mdh, *value)
plug.setMObject(mdh.data())
elif ntype == om.MFnNumericData.k4Double:
NotImplementedError("Haven't implemented double4 access yet")
else:
raise RuntimeError("I don't know how to set data on the given attribute")
else:
# The data is more complex than a simple number.
try:
pmo = plug.asMObject()
except RuntimeError:
# raise a more descriptive error. And make sure to actually print the plug name
raise RuntimeError("I don't know how to access data from the given attribute")
apiType = pmo.apiType()
if apiType in _DTYPE_DICT:
# build the pointArrayData
fnType, mType = _DTYPE_DICT[apiType]
fn = fnType()
mPts = numpyToMaya(value, mType)
dataObj = fn.create(mPts)
plug.setMObject(dataObj)
return
elif apiType == om.MFn.kComponentListData:
fnCompList = om.MFnComponentListData()
compList = fnCompList.create()
fnIdx = om.MFnSingleIndexedComponent()
idxObj = fnIdx.create(om.MFn.kMeshVertComponent)
mIdxs = numpyToMaya(value, om.MIntArray)
fnIdx.addElements(mIdxs)
fnCompList.add(idxObj)
plug.setMObject(compList)
return
else:
apiTypeStr = pmo.apiTypeStr()
raise NotImplementedError("I don't know how to handle {0} yet".format(apiTypeStr))
raise NotImplementedError("WTF? How did you get here??")
################################################################################
def test():
import time
from maya import cmds
meshName = 'pSphere1'
bsName = 'blendShape1'
meshIdx = 0
bsIdx = 0
# A quick test showing how to build a numpy array
# containing the deltas for a shape on a blendshape node
numVerts = cmds.polyEvaluate(meshName, vertex=True)
baseAttr = '{0}.it[{1}].itg[{2}].iti[6000]'.format(bsName, meshIdx, bsIdx)
inPtAttr = baseAttr + '.inputPointsTarget'
inCompAttr = baseAttr + '.inputComponentsTarget'
start = time.time()
points = getNumpyAttr(inPtAttr)
idxs = getNumpyAttr(inCompAttr)
ret = np.zeros((numVerts, 4))
ret[idxs] = points
end = time.time()
print "IDXS", idxs.shape
print "OUT", points.shape
print "RET", ret.shape
print "TOOK", end - start
if __name__ == "__main__":
test()
@benblo
Copy link

benblo commented Sep 24, 2020

Hey, can I ask where you got a Maya-compatible numpy? any chance you could share a build, for Maya 2020 + Win64?

@tbttfox
Copy link
Author

tbttfox commented Sep 24, 2020

@tbttfox
Copy link
Author

tbttfox commented Sep 24, 2020

Also, you can install pip for mayapy.exe, and then install numpy. From a windows command prompt, navigate to the maya bin folder then do this:

mayapy.exe -m ensurepip --upgrade
mayapy.exe -m pip install <path-to-that-numpy-wheel>

@benblo
Copy link

benblo commented Sep 25, 2020

Thanks! I actually saw this link before, but there is no more numpy wheel in that Google Drive folder, only a numexpr wheel :/
I ended up finding another site which pointed to a repo with Maya-compatible packages, here's the procedure:

enabling pip for Maya:

see https://discourse.techart.online/t/numpy-1-13-1-scipy-0-19-1-for-maya-2018/9121/11
and https://forums.autodesk.com/t5/maya-programming/numpy-1-13-1-scipy-0-19-1-for-maya-2018/td-p/7362541

  • Get pip to run on MayaPY: form an elevated command prompt navigate to mayapy folder and run:
mayapy -m ensurepip
  • Update pip:
mayapy -m pip install --upgrade pip
mayapy -m pip install --upgrade setuptools

installing numpy/scipy:

see https://forums.autodesk.com/t5/maya-programming/guide-how-to-install-numpy-scipy-in-maya-windows-64-bit/td-p/5796722

pip install -i https://pypi.anaconda.org/carlkl/simple numpy
pip install -i https://pypi.anaconda.org/carlkl/simple scipy

@tbttfox
Copy link
Author

tbttfox commented Sep 25, 2020

... But there is a numpy wheel in that google drive folder from the autodesk link. I checked before I posted. It was down for a while, but he re-uploaded it about 3 months ago according to the last message in that thread.
Also the stuff on the anaconda link is latest 1.11.0

@maumemoli
Copy link

maumemoli commented Jul 3, 2024

Hi thanks for sharing these scripts they are very useful!
I am encountering a problem trying to get the targetWeights attribute from a blendshape target.
baseAttr = '{0}.inputTarget[{1}].inputTargetGroup[{2}].targetWeights'.format(bsName, meshIdx, bsIdx)
I only get one value out of that. Is this one of those circumstances where you advise to use the getAttr cmds?
Thank you!

@tbttfox
Copy link
Author

tbttfox commented Jul 3, 2024

Short answer: Yep, use cmds.getAttr

Longer answer:

See the input points target returns a "typed" type and "False" for multi

attr = 'inputTarget.inputTargetGroup.inputTargetItem.inputPointsTarget'
cmds.attributeQuery(attr.split('.')[-1], node='blendShape1', attributeType=True)  # returns "typed"
cmds.attributeQuery(attr.split('.')[-1], node='blendShape1', multi=True)  # returns "false"

However, the targetWeights will return "Float" and "True"

attr = 'inputTarget.inputTargetGroup.targetWeights'
cmds.attributeQuery(attr.split('.')[-1], node='blendShape1', attributeType=True)  # returns "float"
cmds.attributeQuery(attr.split('.')[-1], node='blendShape1', multi=True)  # returns "True"

This library handles when the type of the single attribute is some kind of arrayType (like MFloatArray or MPointArray) which is reported as a "typed" attributeType.
However, targetWeights is not a single attribute, it's a multi-attribute. So you'd have to loop over (in this case) the vertex indices with a baseAttr like this:
baseAttr = '{0}.inputTarget[{1}].inputTargetGroup[{2}].targetWeights[{3}]'.format(bsName, meshIdx, bsIdx, vertIdx)
And I'm 99% sure that'd be slower than just using cmds.getAttr

@maumemoli
Copy link

Thanks for the answer and the detailed explanation!
I wonder if there is anything faster than getAttr. I am not having the best performances using it.
Cheers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment