IceMan Scripting and Python

IceMan Scripting and Python

The goal is create a command called bracket that would take a high dynamic range image (HDRI) and create a series of images that would represent mapping that HDRI at a variety of exposure values. Instead of outputting a new image for each exposure level, we are going to create a montage image containing all the results.

We're going to use the crop window, which the user drags out in the GUI. We're also going to be iterating over an image and shuffling about the results and creating a montage, something IceMan scripts excel at.

As with the introductory tutorial, you don't need a Maya scene file for this. All you need is an image to run the script on. An HDRI image will show off the particular utility of this script, but modifying the example to perform any other image processing operation should be easy.

1 - ADDING AN EXTENSION

First, let's set up the new extension script files. See the previous example for how to set up a local user it.ini file. This time, add two new lines to it.ini, one for each script file we'll be working on.

LoadExtension python /path/to/your/scripts/bracket.py
LogMsg NOTICE "Loaded bracket extension"

Change the path according to where you actually put your scripts. The LogMsg is just there as a reminder that something extra was loaded.

2 - THE INTERACTING WITH "it" PART

We are going to split the function into two parts. The first part is going to do all the interacting with "it", and the second part will perform all the image processing.

So here's bracket.py:

def Bracket(elow, ehigh, nsteps, outname="_montage", input=None):
    '''Bracket - create a montage of various exposure levels of the current
image operating on the current crop region. Exposures are
made with the ExrToneMap operator.

elow   - initial exposure value
ehigh  - maximum exposure value
nsteps - the number of brackets to make
optional args:
  outname - output image name (default _montage)
  input   - input image (default current image)'''

    if input == None:
        input = it.GetCurrentElement()

    if input == None:
        it.app.Error("no image spefied")
        return

    image = input.GetImage()
    crop = it.GetCurrentCrop()

    o = bracket(image, crop, elow, ehigh, nsteps)

    elem = it.AddImage(o)
    elem.SetFilename(outname)
    elem.SetLabel(outname)

Notice how we start the function with a long comment string. This is a very convenient way to document the usage and arguments in Python; if you pass this function to the Python builtin help() the comment is printed. If the user doesn't use an outname argument the output of this script will be called "_montage". A catalog always has a crop region; if none has been dragged out in the GUI it defaults to the entire image. To actually make the new image the script calls another routine called bracket. This doesn't exist yet, so we'd better create that now. First we'll just create a stub to get this going and test the first part.

So, add the following somewhere in your bracket.py file:

def bracket(input, crop, elow, ehigh, nsteps):
    '''calculate the size of the montage image. We're going
to separate each element of the bracket by a 20 pixel
border and we're always going to do five small images
across'''
    it.app.Notice("bracket not implemented. crop was %s" % str(crop))

Now to check that it all works, start "it", load an image, and then, in the console window, run it with:

it.extensions.Bracket(0, 3, 9)

If you open the Message Log window and set the filter level to Debug you'll be able to see what your script is doing. If you drag out a crop window on the image first and then run your script you'll see the crop window coordinates being passed down to the IceMan script.

3 - THE IMAGE PROCESSING PART

Time to make the IceMan part do something for real. Change the bracket function to:

def bracket(input, crop, elow, ehigh, nsteps):
    '''calculate the size of the montage image. We're going
to separate each element of the bracket by a 20 pixel
border and we're always going to do five small images
across'''
    cols = 5
    border = 20
    bb = ice.DenormalizeBox(crop, input)

    cWidth = ice.BoxWidth(bb)
    cHeight = ice.BoxHeight(bb)
    mWidth = ((cWidth + border) * cols) + border
    rows = int(nsteps/cols + 0.5)
    mHeight = (cHeight + border) * rows + border

    outBox = (0, mWidth -1, 0, mHeight - 1)
    # make a big image to put all the little copies into
    result = ice.FilledImage(input.ComponentType(), outBox, input.Ply())

    #crop out the part of the source image we're going to work on
    src = input.SubImage(bb)

    e = elow

    for r in range(0, rows):
        for  c in range(0, cols):
            # translate the cropped image and then "tone map" it.
            offset = (border+c*(border+cWidth), border+r*(border+cHeight))
            small = src.Move(offset)
            small = small.EXRToneMap(e, 0, 0, 5, 2.2)

            result = result.CopyFrom(small, small.DataBox())
            if e == ehigh:
                break
            e = e + ((ehigh - elow) / float(nsteps))

    return result

A couple of important things to note here:

First, notice how in the loop we keep referring back to the cropped portion of the original image in the variable src. No need to make explicit copies of this image; IceMan handles all that for you.

Second, notice how we can use the result of an operation (like EXRToneMap above) and assign it back to the same variable. Since you are no doubt wondering, IceMan keeps track of all these images and when they go out of scope they get released. No need to worry about deleting these intermediate images; life is good!

Now to try this out, run "it", load up an image, and drag out a smallish crop window, say about 100 pixels square. Then, in the console window, type:

it.extensions.Bracket(-1, 3, 9)

You should see a nice montage of your crop window at various exposure levels. Cool... you can show this to the lighting director to get her to buy off on one of the levels.

4 - THE TEXT LABEL PART

Now if you've ever had to do this in a darkened screening room with who knows who else in the room, causing distractions, you'll know that sinking feeling when the director says, "The one second to the left..." and then, as she walks out the door, says, "Actually no, the other left."

Right then you'd wished the battery in your laser pointer hadn't just died. Well, we have another solution up our sleeves! We'll use IceMan's oh-so-handy text rendering feature to put labels onto each exposure bracket, spelling out which value was used. No mistakes in our reviews...

Here's the new version of bracket function with text labels:

def bracket(input, crop, elow, ehigh, nsteps):
    '''calculate the size of the montage image. We're going
to separate each element of the bracket by a 20 pixel
border and we're always going to do five small images
across'''
    cols = 5
    border = 20
    textSize = int(border * 0.75)
    bb = ice.DenormalizeBox(crop, input)

    cWidth = ice.BoxWidth(bb)
    cHeight = ice.BoxHeight(bb)
    mWidth = ((cWidth + border) * cols) + border
    rows = int(nsteps/cols + 0.5)
    mHeight = (cHeight + border) * rows + border

    outBox = (0, mWidth -1, 0, mHeight - 1)
    # make a big image to put all the little copies into
    result = ice.FilledImage(input.ComponentType(), outBox, input.Ply())

    #crop out the part of the source image we're going to work on
    src = input.SubImage(bb)

    e = elow

    for r in range(0, rows):
        for  c in range(0, cols):
            # translate the cropped image and then "tone map" it.
            offset = (border+c*(border+cWidth), border+r*(border+cHeight))
            small = src.Move(offset)
            small = small.EXRToneMap(e, 0, 0, 5, 2.2)

            result = result.CopyFrom(small, small.DataBox())

            # Create the label
            label = ice.DrawString("%.2f" % e, (textSize, textSize))
            # Use the same position offset which due to where DrawString
            # renders text means this will convienient be in the border. Copy
            # the single change into two channels to use second as alpha.
            label = label.Move(offset).Shuffle([0,0])
            result = result.Over(label)

            if e == ehigh:
                break
            e = e + ((ehigh - elow) / float(nsteps))

    return result

You might notice that in this final version we have combined some operations on one line. So the construction of the small variable is the result of a Move, followed by a EXRToneMap.

To put the text onto the montage we used the Over operator, which implies the presence of an alpha channel. Since the DrawString operator doesn't provide an alpha channel we fake one by duplicating the single channel in the text image using Shuffle. Over doesn't mind if the background (the montage) is rgb and the text is single channel. It just extends the single channel out so your text will be white.

Here's a sample run made on a image of Mount Tamalpais in Marin County, CA. The source image was one of the sample images you can find at the OpenEXR website.

images/Exr_montage.jpg