HDK
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
Creating a Full Image Filter

Introduction

There are times when an algorithm cannot easily be fit into tile-based processing, or the amount of effort to do so is prohibitive. In these instances, you can design a full image filter using convenience methods found in COP2_Node.

In this example, COP2_FullImageFilter, pixels are randomly translated from their original spot in the image based on a 'size' parameter and the value in the alpha matte.

Example Walkthrough

#include <OP/OP_Context.h>
#include <PRM/PRM_Parm.h>

These headers are generally required for any HDK Operator.

#include <SYS/SYS_Math.h>
#include <SYS/SYS_Floor.h>

SYS_Math is required for SYSrandomZero(), and SYS_Floor is required for SYSrint(), both used in the cook method.

These headers are used by this COP operator for processing image data. Since a full image filter uses image regions instead of tiles, we need TIL_Region. TIL_Plane and TIL_Sequence are used when fetching the input alpha plane, and COP2_CookAreaInfo is used when building input dependency information. Finally, the corresponding header is included.

COP_MASK_SWITCHER(1, "Sample Full Image Filter");
static PRM_Name names[] =
{
PRM_Name("size", "Size"),
};
static PRM_Default sizeDef(10);
static PRM_Range sizeRange(PRM_RANGE_UI, 0, PRM_RANGE_UI, 100);
COP2_FullImageFilter::myTemplateList[] =
{
PRM_Template(PRM_FLT_J, TOOL_PARM, 1, &names[0], &sizeDef, 0,
&sizeRange),
};
OP_TemplatePair COP2_FullImageFilter::myTemplatePair(
COP2_FullImageFilter::myTemplateList,

This code builds the parameter dialog for the operation. It only has one parameter, size, but it also includes all the parameters for masking and scoping, found in COP2_MaskOp::myTemplatePair. The COP_MASK_SWITCHER and PRM_SWITCHER template provides the tabs to nicely partition the parameters into the Mask and Scope tabs.

OP_VariablePair COP2_FullImageFilter::myVariablePair(0,
const char * COP2_FullImageFilter::myInputLabels[] =
{
"Image to Filter",
"Mask Input",
0
};

This node has no local variables, though we label its inputs for ease of use.

COP2_FullImageFilter::myConstructor( OP_Network *net,
const char *name,
{
return new COP2_FullImageFilter(net, name, op);
}
COP2_FullImageFilter::COP2_FullImageFilter(OP_Network *parent,
const char *name,
OP_Operator *entry)
: COP2_MaskOp(parent, name, entry)
{
setDefaultScope(true, true, 0);
}
{
;
}

This filter would be useful as a maskable filter, so it derives from COP2_MaskOp. In addition, the setDefaultScope() call restricts the operation to color and alpha (though this is only the default; the user can change it). If this was not called, all planes would be affected by this operation.

COP2_FullImageFilter::newContextData(const TIL_Plane * /*plane*/,
int /*arrayindex*/,
float t, int xres, int /*yres*/,
int /*thread*/, int /*maxthreads*/)
{
cop2_FullImageFilterData *sdata = new cop2_FullImageFilterData();
// xres may not be the full image res (if cooked at 1/2 or 1/4). Because
// we're dealing with a size, scale down the size based on our res.
// getXScaleFactor will return (xres / full_xres).
sdata->mySize = SIZE(t) * getXScaleFactor(xres);
return sdata;
}

This method evaluates and stashes parms and any other data that needs to be setup. Parms cannot be evaluated concurently in separate threads. This function is guaranteed to be single threaded, and is only called once for a cook. Context data instances can also be cached between cooks to reduce the amount of work done.

void
COP2_FullImageFilter::computeImageBounds(COP2_Context &context)
{
// expands or contracts the bounds to the visible image resolution
context.setImageBounds(0,0, context.myXres-1, context.myYres-1);
}

computeImageBounds() is used to tell the compositor how this operation modifies the canvas (image bounds). In this case, it simply crops it (or expands it) to the frame bounds.

void
COP2_FullImageFilter::getInputDependenciesForOutputArea(
COP2_CookAreaInfo &output_area,
const COP2_CookAreaList &input_areas,
COP2_CookAreaList &needed_areas)
{
// this makes a dependency on the input plane corresponding to the output
// area's plane.
area = makeOutputAreaDependOnInputPlane(0,
output_area.getPlane().getName(),
output_area.getArrayIndex(),
output_area.getTime(),
input_areas, needed_areas);
// Always check for null before setting the bounds of the input area.
// in this case, all of the input area is required.
if (area)
// If the node depends on its input counterpart PLUS another plane,
// we need to add a dependency on that plane as well. In this case, we
// add an extra dependency on alpha (same input, same time).
area = makeOutputAreaDependOnInputPlane(0,
getAlphaPlaneName(), 0,
output_area.getTime(),
input_areas, needed_areas);
// again, we'll use all of the area.
if (area)
getMaskDependency(output_area, input_areas, needed_areas);
}

This method builds the input dependencies for our operation. It needs the full input image corresponding to the plane that is being filtered, plus the alpha plane. Since this is a maskable operation, getMaskDependency() is called as well in order to add any dependency on a mask image.

COP2_FullImageFilter::doCookMyTile(COP2_Context &context, TIL_TileList *tiles)
{
cop2_FullImageFilterData *sdata =
static_cast<cop2_FullImageFilterData *>(context.data());
return cookFullImage(context, tiles, &COP2_FullImageFilter::filter,
sdata->myLock, true);
}

Normally, this is where you would process your tile for a masked filter. However, cookFullImage() is a convenience function which assembles a full image from input tiles and does all the proper locking for you, then calls your filter function (below).

const TIL_Region *input,
TIL_Region *output,
COP2_Node *me)
{
// since I don't like typing me-> constantly, just call a member function
// from this static function.
return ((COP2_FullImageFilter*)me)->filterImage(context, input, output);
}
COP2_FullImageFilter::filterImage(COP2_Context &context,
const TIL_Region *input,
TIL_Region *output)
{
// retrieve my context data information (built in newContextData).
cop2_FullImageFilterData *sdata =
static_cast<cop2_FullImageFilterData *>(context.data());

In this callback, we have a blank output region, and an input region filled with whatever plane we've been told to cook. Both are in the same format, as his node didn't alter the data formats of any planes.

// make a copy of the alpha plane & set it to FP format.
TIL_Plane alphaplane(*mySequence.getPlane(getAlphaPlaneName()));
alphaplane.setFormat(TILE_FLOAT32);
alphaplane.setScoped(1);
TIL_Region *alpha = inputRegion(0, context, // input 0
&alphaplane,0, // FP alpha plane.
context.getTime(), // at current cook time
0, 0, // lower left corner
context.myXsize-1, context.myYsize-1); //UR
if(!alpha)
{
// something bad happened, possibly error, possibly user interruption.
}
int comp;
int x,y;
char *idata, *odata;
float *adata;
// my silly algorithm is as follows: it will take the value of the alpha
// plane multiplied by the user defined size and move the source point
// up to that distance away from its original location. It just adds the
// pixel over any pixel at that location, for simplicities sake.
adata = (float *) alpha->getImageData(0);

We need the alpha plane, so grab it (generally, you'd want to check if context.myPlane->isAlphaPlane() first, and then just use the 'input' region if we were cooking alpha, but for simplicity's sake we won't bother). For convenience, we'll grab it as floating point image. In order to do this, a copy of our alpha plane is made, and the format is changed to FLOAT32.

// go component by component. PLANE_MAX_VECTOR_SIZE = 4.
for(comp = 0; comp < PLANE_MAX_VECTOR_SIZE; comp++)
{
idata = (char *) input->getImageData(comp);
odata = (char *) output->getImageData(comp);
if(odata)
{
// since we aren't guaranteed to write to every pixel with this
// 'algorithm', the output data array needs to be zeroed.
memset(odata, 0, context.myXsize*context.myYsize * sizeof(float));
}
if(idata && odata)
{
// myXsize & myYsize are the actual sizes of the large canvas,
// which may be different from the resolution (myXres, myYres).
for(y=0; y<context.myYsize; y++)
for(x=0; x<context.myXsize; x++)
{
float *pix = (float *) idata;
float *out = (float *) odata;
unsigned seed = x * context.myYsize + y;
float dx = SYSrandomZero(seed);
float dy = SYSrandomZero(seed);
int idx, idy;
int nx, ny;
dx *= adata[x + y * context.myXsize] * sdata->mySize;
dy *= adata[x + y * context.myXsize] * sdata->mySize;
idx = (int) SYSrint(dx);
idy = (int) SYSrint(dy);
nx = x+idx;
ny = y+idy;
if(nx < 0 || nx >= context.myXsize ||
continue;
pix += (x+y*context.myXsize);
out += (nx+ny*context.myXsize);
out = *out + *pix;
}
}
}

This is the heart of our algorithm. It loops through each component, picks random numbers for X and Y translations, and then multiplies them by the alpha plane value and the stashed parameter 'size'. The result is added to output region, in a randomized location.

releaseRegion(alpha);
return error();
}

It is important to release regions and tiles you request with inputRegion & inputTile, otherwise they will just sit around until the end of the cook taking up memory. If someone puts down many of your nodes in a network, this could be problematic. However, the input and output regions are allocated and released by COP2_Node::cookFullImage, so don't release them.

void
{
table->addOperator(new OP_Operator("hdk_samplefull",
"HDK Sample Full Image Filter",
COP2_FullImageFilter::myConstructor,
&COP2_FullImageFilter::myTemplatePair,
1,
2, // optional mask input.
&COP2_FullImageFilter::myVariablePair,
0, // not generator
COP2_FullImageFilter::myInputLabels));
}

Finally, the operation is registered. It has a required input for the filtered image, and an optional input for a mask.