Converting MaxScript -> Maya Python - Struggling with setting pixel info on an image

I’m attempting to re-write a script made for Max that will take information over time (vertex animation or timeline animation) from a mesh and generate an array that is then relocated to pixels to output an image that other software can read (UE4 in this case).

The part I’m having trouble with is this:

global FinalTexture = bitmap numberofVerts (MorphVertOffsetArray.count) filename:TextureName hdr:true;--TextureName ;
for i=0 to (MorphVertOffsetArray.count-1) do (
	setPixels FinalTexture [0, i] MorphVertOffsetArray[(i+1)]
)

It is using bitmap and setPixels to make a texture and set the values of pixels in the relevant positions.

I need to do the same in Maya, and I’ve got a solution in place using the Maya Python API, however none of the pixels are being set, or when I do out_image.setPixels() it is not setting it correctly there. I am at a loss.

This is the Maya Python attempt I have made thus far. The “pixels” variable comes from an “image” variable that was read in earlier, currently it just takes the width/height from there (correctly)

pixels = image.pixels()
depth = 3
array_len = len(self.morph_vert_offset_array[0])
print array_len  # 66 in my test scene
for i in range(0, array_len, depth):
    color = self.morph_vert_offset_array[0][i]
    color[0] = color[0] if color[0] > 0 else 0
    color[1] = color[1] if color[1] > 0 else 0
    color[2] = color[2] if color[2] > 0 else 0
    print int(math.floor(color[2]))  # valid values ranging 0-255
    om.MScriptUtil.setUcharArray(pixels, i+0, int(math.floor(color[0])))
    om.MScriptUtil.setUcharArray(pixels, i+1, int(math.floor(color[1])))
    om.MScriptUtil.setUcharArray(pixels, i+2, int(math.floor(color[2])))

out_image = om.MImage()
out_image.setPixels(pixels, width, height)
out_image.writeToFile("D:/01_Projects/texture3.jpg", ext)

I assume your setting the variable ext to a valid extension in writeToFile?

Yeah have double checked by changing it to hardcoded temporarily: out_image.writeToFile(“D:/01_Projects/texture3.jpg”, “jpg”) , the file writes, it’s just all 100% white pixels. With png there’s no pixels and fully transparent.

I haven’t played with the image stuff from open maya. We’ve been using PIL and OpenCV for our image processing needs. I hesitate to suggest using one of those since You would need to find/compile compatible versions for your version of Maya and because I want to eventually use the OM image stuff and your success is my success. :slight_smile:

Maybe you need a simpler task. Have you tried just setting some pixel values without dragging the other stuff into the mix? Once you know you can set pixel values, then you’re GTG on that front.

import maya.OpenMaya as om
import random, math

height = 256
width = 256
image = om.MImage()
image.create(height, width)
pixels = image.pixels()
depth = 3
array_len = width * height * depth

for i in range(0, array_len, depth):
    # Generating a random color
    color = [random.random() * 255, random.random() * 255, random.random() * 255]
    color[0] = color[0] if color[0] > 0 else 0
    color[1] = color[1] if color[1] > 0 else 0
    color[2] = color[2] if color[2] > 0 else 0
    om.MScriptUtil.setUcharArray(pixels, i+0, int(math.floor(color[0])))
    om.MScriptUtil.setUcharArray(pixels, i+1, int(math.floor(color[1])))
    om.MScriptUtil.setUcharArray(pixels, i+2, int(math.floor(color[2])))

out_image = om.MImage()
image.setPixels(pixels, height, width)
image.writeToFile("E:/texture3.jpg", '.jpg')

So this seems to work for me. I don’t know how you’re generating your source data, but at least random values gets me a noise texture written.

Yeah found the issue, I was setting the width and height incorrectly. Bah I knew it would be simple!

Ah at least btribble gets his code on a silver platter when he uses OM for images :wink:

For my purposes it will generate a row of x amount of pixels for each mesh selected.

Here’s the relevant code:


width = self.vert_count
height = len(self.morph_vert_offset_array)

image = om.MImage()
image.create(width, height)

pixels = image.pixels()
depth = 3

array_len = len(self.morph_vert_offset_array[0])
for i in range(0, array_len, depth):
    color = self.morph_vert_offset_array[0][i]
    color[0] = color[0] if color[0] > 0 else 0
    color[1] = color[1] if color[1] > 0 else 0
    color[2] = color[2] if color[2] > 0 else 0
    om.MScriptUtil.setUcharArray(pixels, i+0, int(math.floor(color[0])))
    om.MScriptUtil.setUcharArray(pixels, i+1, int(math.floor(color[1])))
    om.MScriptUtil.setUcharArray(pixels, i+2, int(math.floor(color[2])))

out_image = om.MImage()
out_image.setPixels(pixels, width, height)
out_image.writeToFile("D:/01_Projects/texture.jpg", "jpg")

Edit - Can do this with QImage:

Starting from just under the loop, omitting the out_image stuff and below


pixel_pointer = ctypes.cast(pixels, ctypes.POINTER(ctypes.c_char))
pixel_string = ctypes.string_at(pixel_pointer, width * height * depth)

out_image = QImage(pixel_string, width, height, QImage.Format_RGB16)
out_image.save("D:/01_Projects/texture.tif", "TIF")

Edit2 - Neither MImage nor QImage support 16 bits per channel! Any ideas?

I’d use PIL

It’s no good if it wants me to install it, should be able to just run this script and it works without prior setup. No one’s going to bother installing dependencies for a simple Maya script.

MTexture might do what you want, it is in the OpenMayaRender namespace. (This is totally a guess)

JTaylor
Which file format you want use for output data? I can write a stripped functional version just for write to image file.

Looks like cmds.psdExport has an option for bytes per channel.
http://download.autodesk.com/us/maya/2011help/CommandsPython/psdExport.html
Maybe this can work for you? though it probably requires that someone have photoshop or some other psd capable app.

Glad to see you got it working! Now I just have to figure out how to rasterize tris into an image. OpenCV has direct support for this, but then… Maya, so…

I can’t find a single working example of using MTexture on google, maybe I’m searching for it wrong, but the documentation doesn’t have anything either. Although it does seem to take 16-bits.

[QUOTE=R.White;29885]Looks like cmds.psdExport has an option for bytes per channel.
http://download.autodesk.com/us/maya/2011help/CommandsPython/psdExport.html
Maybe this can work for you? though it probably requires that someone have photoshop or some other psd capable app.[/QUOTE]

How did you intend for it to be used? Not sure if it would work, it needs to be given a psd and exports it as another format, which means I’d need to output a 16-bit / channel psd in the first place… which would solve the problem without needing to export as tif :stuck_out_tongue:

[QUOTE=Styler;29883]JTaylor
Which file format you want use for output data? I can write a stripped functional version just for write to image file.[/QUOTE]

That would be incredibly helpful, thanks for offering! The files need to be EXR ultimately but there’s a lot of tools that can convert TIF->EXR, so TIF will suffice since PIL supports it. RGB 16bit/channel.

Ok, here is a quick solution. It dumps data directly to EXR 2.0 file (RGB16 uncompressed)
No extra modules are required for installing, just a standard Python 2.6+
>> Download <<
Enjoy!


import os
import struct
import binascii


def dump_to_exr_rgb16uncomp(height, width, data, filename):
    """
    :param height: Height of image
    :param width: Width of image
    :param data: A sequence (list/tuple) of float point values in format [r, g, b, r, g, b ...]
    :param filename: filename for output
    """
    def make_chlist_value(name, typ, linear=0, samx=1, samy=1):
        return ''.join([
            name, '\x00',
            struct.pack('I', typ),
            struct.pack('B', linear),
            '\x00\x00\x00',             # reserved
            struct.pack('I', samx),
            struct.pack('I', samy)])

    def make_attr(name, typ, size, value):
        return ''.join([
            name, '\x00',
            typ, '\x00',
            struct.pack('I', size),
            value])

    def pack_half(value):
        F16_EXPONENT_BITS = 0x1F
        F16_EXPONENT_SHIFT = 10
        F16_EXPONENT_BIAS = 15
        F16_MANTISSA_BITS = 0x3ff
        F16_MANTISSA_SHIFT = (23 - F16_EXPONENT_SHIFT)
        F16_MAX_EXPONENT = (F16_EXPONENT_BITS << F16_EXPONENT_SHIFT)

        a = struct.pack('>f', value)
        b = binascii.hexlify(a)

        f32 = int(b, 16)
        f16 = 0
        sign = (f32 >> 16) & 0x8000
        exponent = ((f32 >> 23) & 0xff) - 127
        mantissa = f32 & 0x007fffff

        if exponent == 128:
            f16 = sign | F16_MAX_EXPONENT
            if mantissa:
                f16 |= (mantissa & F16_MANTISSA_BITS)
        elif exponent > 15:
            f16 = sign | F16_MAX_EXPONENT
        elif exponent > -15:
            exponent += F16_EXPONENT_BIAS
            mantissa >>= F16_MANTISSA_SHIFT
            f16 = sign | exponent << F16_EXPONENT_SHIFT | mantissa
        else:
            f16 = sign
        return f16

    depth = 3
    fdata = ['\x76\x2f\x31\x01\x02\x00\x00\x00']  # magic and version

    if width*height*depth != len(data):
        raise ValueError('Data length does not fit with image size')

    # build header
    channels = '%s\x00' % ''.join([make_chlist_value(name, typ) for name, typ in (('R', 1), ('G', 1), ('B', 1))])
    fdata.append(make_attr('channels', 'chlist', 18*depth+1, channels))
    fdata.append(make_attr('compression', 'compression', 1, '\x00'))  # 0 - uncompressed

    windata = ''.join(['\x00'*8, struct.pack('i', width-1), struct.pack('i', height-1)])

    fdata.append(make_attr('dataWindow', 'box2i', 16, windata))
    fdata.append(make_attr('displayWindow', 'box2i', 16, windata))
    fdata.append(make_attr('lineOrder', 'lineOrder', 1, '\x00'))     # inc y
    fdata.append(make_attr('pixelAspectRatio', 'float', 4, struct.pack('f', 1.0)))
    fdata.append(make_attr('screenWindowCenter', 'v2f', 8, struct.pack('ff', 0, 0)))
    fdata.append(make_attr('screenWindowWidth', 'float', 4, struct.pack('f', 1)))
    fdata.append('\x00')  # end of the header

    # calc lines offset
    offtab_size = sum([len(x) for x in fdata]) + height*8
    line_size = 8 + width*2*depth

    # fill offsets table
    for i in xrange(height):
        fdata.append(struct.pack('Q', offtab_size + i*line_size))

    data_size = struct.pack('I', width*2*depth)

    # add data by scanlines
    for j in xrange(height):
        line = [struct.pack('i', j), data_size]
        n = j*width*depth

        for i in xrange(depth):
            chdata = [pack_half(data[n+x+i]) for x in xrange(0, width*depth, depth)]
            line.append(struct.pack('%sH' % len(chdata), *chdata))
        fdata.append(''.join(line))

    # write to file
    dirname = os.path.dirname(filename)
    if not os.path.exists(dirname):
        os.makedirs(dirname)

    with open(filename, 'wb') as f:
        f.write(''.join(fdata))


# test
img1x11_rgb = [
    1.0, 0.0, 0.0,
    0.0, 1.0, 0.0,
    0.0, 0.0, 1.0,
    0.5, 0.0, 0.0,
    0.0, 0.5, 0.0,
    0.0, 0.0, 0.5,
    0.25, 0.0, 0.0,
    0.0, 0.25, 0.0,
    0.0, 0.0, 0.25,
    0.5, 0.5, 0.5,
    1.0, 1.0, 1.0
]

dump_to_exr_rgb16uncomp(11, 1, img1x11_rgb, r'c:\1\out1x11.exr')


Thanks so much Styler! Works really well :slight_smile: