This chapter covers the following advanced programming issues:
  • Addressing the Image Memory
  • Changing Output Image Size, Depth, Channels
  • Using Multiple Inputs & Multiple Regions
  • Allowing Input Size/Depth Mismatches
  • Using Time in Chalice
  • Custom Monitors
  • Multiprocessing
  • User help

  • Addressing the Image Memory

    The *result pointer that comes into upiProcessImage() points to a block of data representing the image, and a structure which contains information about the image. The format of these structures is
    typedef struct
    {
        unsigned int    sizeX;
        unsigned int    sizeY;
        unsigned int    fullX;
        unsigned int    fullY;
        unsigned int    offsetX;
        unsigned int    offsetY;
        unsigned int    channels;
        float           time;
        CPI_PelType     pelType;
        unsigned int    input;
    } CPI_ImageContext;
    
    typedef struct
    {
        CPI_ImageContext info;
        void *data;
    } CPI_Image;
    
    typedef enum
    {
        P_INT8,
        P_INT16,
        P_FLOAT32,
        P_UNKNOWN
    } CPI_PelType;
    
    The image data is stored in scan-line order, starting from the upper left corner of the image. The format of each scanline is
    [chan 1|chan 2|...|chan n] [chan 1|chan 2|...|chan n] etc
    that is, each pixel appears, with all of its relevant channels, then the next pixel appears, etc. Usually, this becomes
    [R|G|B|A] [R|G|B|A] etc

    The size of each pixel is dependent on the format of the pixels, and is given by this table

    pelType bytes
    P_INT8 1
    P_INT16 2
    P_FLOAT32 4

    This information is also accessible via the call cpiGetByteSize() (see Chapter 6, Section 3 )

    So to access an individual pixel whose address is (x, y), the offset into the *data array would be y*sizeX*pelsize*channels + x*pelsize*channels For example, this fragment sets the point (xpos, ypos) to [0, 0, 0, 0].

    template <class T>
    void clearPixel(T *image, unsigned int offset, unsigned int channels)
    {
        int        i;
    
        image += offset;
    
        for (i = 0; i < channels; i++)
        {
            *image = (T)0;
             image++;
        }
    }
    
    CPIDSOEXPORT int upiProcessImage( CPI_Image *result )
    {
        int        xpos, ypos, pelsize, channels, offset;
    
    ... assume that coming in to here we have the x and y position of the
    pixel in question in (xpos, ypos)
    
        channels = result->info.channels;
        switch( result->info.pelType)
        {
            case P_INT8:    
                pelsize = channels * 1;
                offset = ypos * result->info.sizeX * pelsize + xpos * pelsize;
                clearPixel((unsigned char *)result->data, offset, channels);
                break;
            case P_INT16:
                pelsize = channels * 2;
                offset = ypos * result->info.sizeX * pelsize + xpos * pelsize;
                clearPixel((unsigned short *)result->data, offset, channels);
                break;
            case P_FLOAT32:
                pelsize = channels * 4;
                offset = ypos * result->info.sizeX * pelsize + xpos * pelsize;
                clearPixel((float *)result->data, offset, channels);
                break;
            default: 
                cpiError("Unknown pixel type"); 
                return 1;
        }
    
        return 0;
    }
    
    The complete version of this plugin is in example file SetPixel.C

    Changing Output Image Parameters

    Simple filter plugins, which take an image and modify its contents in some way, can usually do their work and return the result in the same image block that they were passed. For example, adding noise to an image can be done by modifying the incoming image directly. This is the default behavior.
    In this case, the programmer can choose which input memory to use to return the output image. By default, the result is placed in the memory used by the first input; in mult-input nodes, the programmer can choose which input to use. Inputs are labled A, B, C, etc, and are set up by this call:
    To do this, the routine
    int upiResultInput( CPI_ImageContext *result )
    can be used. The input argument can be used to know the state of the current image (sizeX, sizeY, etc). Then the routine returns one of the following:
        INPUT_A        (default)
        INPUT_B
        INPUT_C    
        INPUT_D    
        RESULT_SEPARATE
    
    The first 4 refer to the first 4 inputs of the node; they tell Chalice that the results will be stored in that input. RESULT_SEPARATE tells Chalice that a new output will have to be allocated. For example:
    CPIDSOEXPORT int upiResultInput( CPI_ImageContext *result )
    {
        return RESULT_SEPARATE;
    }
    
    This will be the case when the result cannot be returned in the same memory space as was passed in; in particular, nodes which change the size, type and/or number of channels in an image, must allocate and return new memory as the output.
    The parameters of an output image can be changed using the call upiOutputContext(). Typically, in defining this routine, the programmer will first call cpiInputContext() to get the incoming format, then change those aspects which need to change. For example, the Tile.C plugin which is supplied with Chalice, will change the output to be a multiple of the input size:
    /*
     * Calculate our output size from the input size
     * and parameters.
     */
    CPIDSOEXPORT int upiOutputContext( CPI_ImageContext *info, float time )
    {
        /* Get the input information */
        if( cpiInputContext( INPUT_A, info, time ) == 0 )
        {
            /* Calculate our output size */
            float t;
            cpiGetFloat( "Tile X", time, &t );
            info->fullX *= t;
            cpiGetFloat( "Tile Y", time, &t );
            info->fullY *= t;
    
            /* Make sure our image is at least one pixel */
            if( info->fullX <= 0 )
                info->fullX = 1;
            if( info->fullY <= 0 )
                info->fullY = 1;
            return 0;
        }
        else
        {
            cpiError( "Couldn't get input size" );
            return 1;
        }
    }
    
    /*
     * When our parameters (Tile X and Tile Y) change, we 
     * need to tell chalice our output info has changed.
     */
    CPIDSOEXPORT void upiParameterChanged( char *name )
    {
        cpiInvalidateOutput();
    }
    
    /*
     * We can't use the input as our result image
     */
    CPIDSOEXPORT int upiResultInput( CPI_ImageContext *result )
    {
        return RESULT_SEPARATE;
    }
    

    Multiple Inputs & Input Regions

    RegionsNeeded() defines 1 or more regions which the node will need to read in during the course of its processing. A region is not an image, although it may be; a region is some rectangular portion of an image, up to and including the whole image.

    Thus it is possible to specify multiple blocks from the same input image as required regions. Each call to cpiNeedRegion() defines a region, in order, which can be retrieved with cpiCookRegion(). Regions are numbered from 0, so the first call to cpiNeedRegion() will define a region which will be returned with cpiCookRegion(0, &input), the next will come from cpiCookRegion(1, &otherinput), etc.

    Here is a table which shows how regions map to node inputs:

    Input Result in Which Cook
    0 *result None - passed in
    1 region 0 first cook
    2 region 1 second cook
    N region N-1 Nth cook

    Usually, regions do conform to entire images, and cpiNeedRegion() is used to define multiple inputs to a node. For example, here is a code fragment from a node that takes 2 inputs; the first input, by default, is copied into *result before it is passed to upiProcessImage(). The second input comes from the call to cpiCookRegion(0, &input). Chalice knows what to return from this call, because by default the next region to cook is the second input to the node. Calls to cpiNeedRegion() redefine this default mapping.

    CPIDSOEXPORT void upiNumberOfInputs( int *min, int *max )
    {
        *min = 2;
        *max = 2;
    }
    
    CPI_PluginType upiPluginType( void )
    {
        return T_MATTE;
    }
    
    CPIDSOEXPORT int upiProcessImage( CPI_Image *result )
    {
        CPI_Image     input;
        int            pixels;
    
        // result has a copy of input 0 (by default)
        // get the other image by cooking region 0 (not 1)
        // see documentation for why
        if( cpiCookRegion( 0, &input ) != 0 )
            return -1;
    
        pixels = result->info.sizeX * result->info.sizeY * result->info.channels;
    
        switch( result->info.pelType )
        {
            case P_INT8:
                dosomething((unsigned char *)result->data, 
                            (unsigned char *)input.data, pixels);
                break;
            case P_INT16:
                dosomething((unsigned short *)result->data, 
                            (unsigned short *)input.data, pixels);
                break;
            case P_FLOAT32:
                dosomething((float *)result->data, (float *)input.data, pixels);
                break;
            default:
                cpiError( "Unknown pixel type" );
                return 1;
        }
    
        return 0;
    }
    
    There is also a cpi routine, cpiNumberOfInputs(), which returns to the user program the number of currently connected inputs. This information is potentially necessary in multi-input nodes, for example to decide whether an optional control image is connected.

    Allowing Input Size/Depth Mismatches

    Although Chalice does not allow it in its own nodes, you may write plugins which accept multiple inputs of different sizes and/or bit depths - or you may check for specific combinations of depth and size, depending on your application.

    The routines for doing this are upiCheckInputSize() and upiCheckInputDepth(). Each routine is called before the node begins to cook - if the routine exists and returns a non-zero value, an error is returned and cooking stops. If you detect an error, you should call cpiError() and return a non-zero value.

    If you do not care if sizes are different, for example, you could simply write

    int upiCheckInputSize( CPI_ImageContext *result )
    {
        return 0;
    }
    

    Time in Chalice, getting input images out of sequence, etc

    In Chalice, part of the context of a cook is "what is the time that the cook was requested?" This means the time, in seconds and fractions of a second, in the sequence that the cook was requested. Time is part of each image (refer to the CPI_ImageContext structure), and on input it represents the current time.

    For example, in a 150 frame animation at 24 frames per second, if the user requests a monitor from a node at frame 50, then the time of that request would be 50 / 24 = 2.08333.

    Time can be converted between frames and seconds with the following utilities:

        int cpiGetFrame( float time )
            Returns the frame value of the given time (in seconds).
    
        float cpiGetTime( int frame )
            Returns the time (in seconds) of the given frame.
    
    When cooking an image, Chalice assumes that the input frame the node needs is the one which occurs at the current time. But that does not need to be true. It is possible to request an input image from any time in the overall sequence. The routine upiRegionsNeeded() is used to tell Chalice which input is required for the node.

    For example, say we want a node which randomizes its input frames, delivering as output any random frame from the sequence plugged into its input. Then we would do this:

    CPIDSOEXPORT int upiResultInput( CPI_ImageContext *result )
    {
        return RESULT_SEPARATE;
    }
    
    CPIDSOEXPORT void upiRegionsNeeded( CPI_ImageContext *result )
    {
        int seed;
        if( cpiGetInteger( "Seed", result->time, &seed ) != 0 )
        {
            cpiError( "Could not get seed" );
            return;
        }
        srand( seed );
    
        long start, end;
        if( cpiInputFrameRange( 0, &start, &end ) != 0 )
        {
            cpiError( "Couldn't get input frame range" );
            return;
        }
    
        long frame = rand() % (end-start) + start;
    
        result->input = 0;
        result->time = cpiGetTime( frame );
    
        cpiNeedRegion( result );
    }
    
    CPIDSOEXPORT int upiProcessImage( CPI_Image *result )
    {
        CPI_Image input;
        
        if( cpiCookRegion( 0, &input ) != 0 )
            return -1;
        
        int pelsize;
        if (result->info.pelType == P_INT8)
            pelsize = 1;
        else if (result->info.pelType == P_INT16)
            pelsize = 2;
        else if (result->info.pelType == P_FLOAT32)
            pelsize = 4;
        else
            cpiError( "Unknown pixel type" );
    
        int memsize = result->info.sizeX * result->info.sizeY *
                result->info.channels * pelsize;
    
        // copy input into result
        memcpy( result->data, input.data, memsize );
    
        return 0;
    }
    
    The three upi routines work together to accomplish this. First, upiResultInput() tells Chalice that a separate image will be passed to the output.
    Next, upiRegionsNeeded() sets up a time that the input will be needed. Notice that it uses cpiGetTime() to convert from a frame number to a time value.
    Finally, upiProcessImage() uses cpiCookRegion() to cook the node input at the desired time. This is then copied to the result data area.
    The complete version of this plugin can be found in Random.C.

    Note that *result always points to a place to put the result image. The difference between setting RESULT_SEPARATE and not is that, if it is set, the memory that *result points to will be separate from the memory pointed to by the input image. If it is not set, the memory location that contains the input image will be the same memory location pointed to by *result. Thus your node will be more memory efficient if you can avoid using RESULT_SEPARATE unless absolutely necessary.


    Custom Monitors and Custom UI

    With the current definition of the plugin API, it is not possible to have custom monitors or to do custom user interaction. For example, it is not possible to do a rotospline node as a custom node.

    These extensions are under development. If you have an immediate need for this capability, please contact Silicon Grail support.


    Multiprocessing

    Chalice provides two routines to enable multiprocessing of a node. The first is

    int cpiNumProcessors( void )

    which returns the number of available processors. The second is

    void cpiMultiProcess(void (*func)(int, int, void *), void *arg)

    which hands off to Chalice the name of a routine (func) to be called once per available processor. This function will get all of its arguments via the void *arg pointer which usuallly points to a structure containing data.

    The function itself is called by Chalice once for each available processor; each call looks like

    void func(int proc, int numProc, void *arg)

    The first argument is the processor number that is being called - these start at 0 (zero). numProc is the total number of processors available, and *arg points to a block of data which contains any information that func() needs to complete its work.

    One example of such data is pointers to the area of image memory that this particular instance of func() should work on. Chalice does not divide up images and hand each piece off to a separate call to func() - it is the programmer's job to do that.

    For an example of this division, and calls to multiple versions of func(), see the source to MultiBright.C


    help for users

    It is not currently possible to link in help pages for users of plugin nodes. This ability is under development.

    However, it is possible to use the upi function upiMessageInfo() to pass information back to the user via the node's info button. When the info button is clicked by the user, whatever string returned by this function is added to the block of information shown to the user. For example:

    char *upiMessageInfo(float time)
    {
        static char help[] = "This message can be used as a\n\
    help message, which can run on multiple lines\n\
    Is that ok?";
    
        return(help);
    }
    
    Note that newline characters (\n) can be used, and that the continuation character (\) is used to run the string definition onto multiple lines of the file. It is also possible to build this string dynamically, by using the sprintf() calls that are part of the C language.
    [Previous Page] [Next Page]
    [Table of Contents] [Index]

    Copyright © 1998 Silicon Grail Inc.
    710 Seward Street, Hollywood, CA 90038