In this section, we look at a few practical examples of working with channels, as it’s often a topic that causes a little confusion.
First up, the simplest manipulation of all: adding or removing channels from the stream. There are two examples in the NDK named, appropriately, AddChannels.cpp and Remove.cpp. Note that AddChannels is listed in the interface under an ‘Add’ menu entry (in the Channel menu). The Op itself couldn’t simply be called ‘Add’, as that is already taken by the Color > Maths > Add node.
In both cases, outside of knob storage and set-up, the work happens in _validate. Indeed, both samples inherit from NoIop, so they don’t even have to go to the effort of implementing engine methods copying the actual image data through. The two _validate methods are reproduced below:
void AddChannels::_validate(bool for_real)
{
copy_info();
ChannelSet newchan = channels;
newchan += (channels2);
newchan += (channels3);
newchan += (channels4);
set_out_channels(newchan);
info_.turn_on(newchan);
}
void Remove::_validate(bool for_real)
{
copy_info();
ChannelSet c = channels;
c += (channels2);
c += (channels3);
c += (channels4);
if (operation) {
info_.channels() &= (c);
set_out_channels(info_.channels());
}
else {
info_.turn_off(c);
set_out_channels(c);
}
}
In both cases, we build a temporary ChannelSet from the Channels used by the knobs, then use this to set the channels present in the output (either adding them or removing them). We then additionally use set_out_channels to inform NUKE what channels are changed by this operator (either added or removed).
Many calculations require access to multiple channels simultaneously. Let’s say, for example, a colorspace conversion from RGB to YUV - producing any of the output YUV channels requires access to all of the input RGB channels. To handle circumstances like this effectively, you should register interest in all required input channels. Then when calculating the output, calculate all the output channels possible from the available input channels and keep an internal list of those done, so that when your foreach channel loop hits a channel you’ve already calculated, you don’t redo the work. The Saturation.cpp sample is an excellent example of this technique being applied.
class SaturationIop : public PixelIop
{
//...
void in_channels(int input_number, ChannelSet& channels) const
{
// Must turn on the other color channels if any color channels are requested:
ChannelSet done;
foreach (z, channels) {
if (colourIndex(z) < 3) { // it is red, green, or blue
if (!(done & z)) { // save some time if we already turned this on
done.addBrothers(z, 3); // add all three to the "done" set
}
}
}
channels += done; // add the colors to the channels we need
}
//...
};
void SaturationIop::pixel_engine(const Row& in, int y, int x, int r,
ChannelMask channels, Row& out)
{
ChannelSet done;
foreach (z, channels) { // visit every channel asked for
if (done & z)
continue; // skip if we did it as a side-effect of another channel
// If the channel is not a color, we return it unchanged:
if (colourIndex(z) >= 3) {
out.copy(in, z, x, r);
continue;
}
// Find the rgb channels that belong to the set this channel is in.
// Add them all to "done" so we don't run them a second time:
Channel rchan = brother(z, 0);
done += rchan;
Channel gchan = brother(z, 1);
done += gchan;
Channel bchan = brother(z, 2);
done += bchan;
// pixel_engine is called with the channels indicated by in_channels()
// already filled in. So we can just read them here:
const float* rIn = in[rchan] + x;
const float* gIn = in[gchan] + x;
const float* bIn = in[bchan] + x;
// We want to write into the channels. This is done with a different
// call that returns a non-const float* pointer. We must call this
// *after* getting the in pointers into local variables. This is
// because in and out may be the same row structure, and calling
// these may change the pointers from const buffers (such as a cache
// line) to allocated writable buffers:
float* rOut = out.writable(rchan) + x;
float* gOut = out.writable(gchan) + x;
float* bOut = out.writable(bchan) + x;
//.... Loop across pixels calculating saturation for all three channels
//.... and setting them in the output row.
}
Here, we use the ChannelSet done to keep track of what we’ve already calculated and what we haven’t. At the start of every channel loop, we check to see if it’s done and skip if it is, otherwise we do the three.
We can use a very similar technique to access data from any other Channel(Set) in the incoming image - we simply add them to the channels requested from the input (either using in_channels in a PixelIop or _request in an Iop), at which point we can call on them in our engine method with impunity. Take the following code from the AccessMotion.cpp example:
virtual void in_channels(int, ChannelSet& mask) const {
mask += _requestChannels;
}
virtual void _request (int x, int y, int r, int t, ChannelMask channels, int count) {
for (int n = 0; n < inputs(); n++) {
Iop * thisInput = dynamic_cast<Iop*>(Op::input(n));
ChannelSet in = channels;
in_channels(n, in);
if (thisInput && in) {
thisInput->request(in, 0);
}
}
}
This essentially reimplements the use of in_channels found in PixelIop, from our _request for convenvience and improved readability. The _requestChannels in this case is initialized to a Mask_MoVec to allow access to the predefined motion vector channels found in NUKE. The remainder of the example uses the motion vectors to warp previous and next frames onto the current one, before medianing them, in a full frame buffer access manner. The _validate coupled with the engine simply copy the incoming channels down the tree, so that the motion vectors stay accessible after the current Op.