HDK
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
Creating a custom pixel filter COP

Introduction

A pixel filter is a variation of a regular filter, but with one major restriction. The output pixel must depend only on the corresponding input pixel's value - no neighboring pixels may be used, nor pixels from other planes or frames.

This restriction allows pixel filters to be highly optimized, as a string of pixel filters in a network can be collapsed into one operation.

Walkthrough

This example adds a constant vector value to all pixels in the image.

COP2_PIXEL_OP_SWITCHER(1, "Sample Pixel Add");

This defines the tab switcher for a pixel filter operation. It creates the Mask and Frame Scope tabs.

static PRM_Range addRange(PRM_RANGE_UI, -1, PRM_RANGE_UI, 1);
COP2_PixelAdd::myTemplateList[] =
{
&addRange),
};
OP_TemplatePair COP2_PixelAdd::myTemplatePair( COP2_PixelAdd::myTemplateList,
OP_VariablePair COP2_PixelAdd::myVariablePair( 0,&COP2_MaskOp::myVariablePair);
const char * COP2_PixelAdd::myInputLabels[] =
{
"Image to Add to",
"Mask Input",
0
};

This next portion creates the parameters (one), local variables (none) and the input labels for the node.

COP2_PixelAdd::myConstructor(OP_Network *net,
const char *name,
{
return new COP2_PixelAdd(net, name, op);
}
COP2_PixelAdd::COP2_PixelAdd(OP_Network *parent,
const char *name,
OP_Operator *entry)
: COP2_PixelOp(parent, name, entry)
{
}
COP2_PixelAdd::~COP2_PixelAdd() { }

Pixel operations derive from COP2_PixelOp, which in turn derives from COP2_MaskOp (masking), COP2_PixelBase (plane/frame scoping) and COP2_Node.

COP2_PixelOp does all the cooking work and provides a few parameters to control where to quantize and if the operation should be done in unpremultiplied space. Its parent class, COP2_MaskOp, allows the operation to be masked by another image wired to the mask input. This class derives from COP2_PixelBase, which provides plane and frame scoping of the operation.

The operation itself is defined by class derived from RU_PixelFunction, in this case, a simple vector add:

class cop2_AddFunc : public RU_PixelFunction
{
public:
cop2_AddFunc(float r,float g, float b, float a)
{ myAddend[0] = r; myAddend[1] = g; myAddend[2] = b; myAddend[3] = a; }
protected:

The next virtual method describes how your pixel function behaves. If the operation does the exact same operation on all components, such as inverting the pixel, then eachComponentDifferent() can return false. In this case, we are adding a vector to the pixel, so the operation does differ per component.

// the operation differs per component.
virtual bool eachComponentDifferent() const { return true; }

Pixel functions can be defined as scalar or vector operations. A vector operation requires all components at once, whereas in a scalar operation, they can be processed independently. In this example, adding a vector to our pixel does not require the other components as input values (though if we were taking the luminance of the pixel, we would).

// we don't need to process all the components together, as a vector.
virtual bool needAllComponents() const { return false; }

This is the actual pixel function itself. There is a function definition, add, which must be the same as the function signature here, and a virtual getPixelFunction() method which returns a pointer to our pixel function.

// Here's the heart of the pixel function - it simply adds our addend to
// the passed in val for the given component. This is the scalar version.
static float add(RU_PixelFunction *pf, float val, int comp)
{ return val +((cop2_AddFunc*)pf)->myAddend[comp]; }
// we return the static function above as our scalar function.
virtual RUPixelFunc getPixelFunction() const { return add; }

If instead we were doing a vector pixel function (like luminance), the vector function would need to be defined along with an override of getVectorFunction() to return our vector function. Not all components of the input vector may be present (if it's a scalar or vector3), and not all components may have been scoped by the user. If scope[] is false for any of the components, you can read from the component, but you cannot write to it.

// These are the methods you would use for a vector function. They are
// not used in this case, since needAllComponents is false.
// You can define a function with both a scalar and a vector function,
// and switch between the two based on parameters.
static void addvec(RU_PixelFunction *f, float **vals,
const bool *scope)
{
cop2_AddFunc *pf = (cop2_AddFunc *) f;
if(vals[0] && scope[0]) *vals[0] = *vals[0] + pf->myAddend[0];
if(vals[1] && scope[1]) *vals[1] = *vals[1] + pf->myAddend[1];
if(vals[2] && scope[2]) *vals[2] = *vals[2] + pf->myAddend[2];
if(vals[3] && scope[3]) *vals[3] = *vals[3] + pf->myAddend[3];
}
// we return the static function above as our vector function.
virtual RUVectorFunc getVectorFunction() const { return addvec; }

Finally, the pixel function class contains any data necessary to perform the operation. In this case, it's just the vector we want to add.

private:
float myAddend[4];
};

Back to COP2_PixelAdd now, the main method in a pixel operation COP is the addPixelFunction() method. The method must be overridden to return an instance of your pixel function.

COP2_PixelAdd::addPixelFunction(const TIL_Plane*plane,int,float t, int,int,int)
{
// The frame scope effect is only used if parms on the frame scope page
// are altered.
int index = mySequence.getImageIndex(t);
float effect = getFrameScopeEffect(index);
float r,g,b,a;
// Note that we treat the alpha plane differently than other planes.
if(plane->isAlphaPlane())
{
// use the alpha value for the alpha plane.
r = g = b = a = ADD(3, t) * effect;
}
else
{
// all other planes, use comps 0-3.
r = ADD(0,t) * effect;
g = ADD(1,t) * effect;
b = ADD(2,t) * effect;
a = ADD(3,t) * effect;
}
return new cop2_AddFunc(r,g,b,a);
}

This method defines the vector that is added to the input image, and creates a new pixel function with these values. The effect variable is defined by the parameters in the Frame Scope tab, and it is normally 1. If it isn't 1, we lessen (or increase) the effect of the addend vector by multiplying it by the effect.

Optionally, getOperationInfo() can be overridden to provide a short description of what your operator is doing in operator info popup.

const char *
COP2_PixelAdd::getOperationInfo()
{
// return a small string describing what this function does in the info
// popup.
float t = CHgetEvalTime();
int index = mySequence.getImageIndex(t);
float effect = getFrameScopeEffect(index);
static UT_WorkBuffer info;
info.sprintf("Add (%g, %g, %g, %g)",
ADD(0,t)*effect, ADD(1,t)*effect,
ADD(2,t)*effect, ADD(3,t)*effect);
return info.buffer();
}

Finally, the pixel operation needs to be registered like any other COP. The number of inputs is 1-2 - one required plus an optional mask input.

void
{
table->addOperator(new OP_Operator("hdk_samplepixadd",
"HDK Sample Pixel Add",
&COP2_PixelAdd::myConstructor,
&COP2_PixelAdd::myTemplatePair,
1,
2, // optional mask input
0));
}