Planar Iop: Image Processing with 2-Dimensional Outputs

Nuke 8.0 introduces support for Planar Iops, in addition to the usual Row-based Iops. These are primarily intended for Iops that find producing their output in single rows to be unnatural, and would prefer instead to output data containing multiple rows at a time (either the entire image, or smaller stripes.)

Planar Iops have two levels of abstraction: there is a core PlanarI interface, which provides a mechanism that PlanarIop then uses. We expect that most users will want to implement PlanarIop.

PlanarIop provides a function

void renderStripe(ImagePlane& imagePlane)

This is the main rendering function. The implementation of this should output a rendered stripe. The image plane has been set up with the required bounding box and channels already; all the implementation needs to do is to fill in the data for this region and channels.

By default renderStripe() will be called for the entire bounding box of the iop, as set by _validate. This is ideal for plugins that take the entire input image as a single input and produce a single output image. Additionally, renderStripe() will be called for each layer individually.

PlanarIops also should not implement _request; instead they should implement getRequests(), which is a similar, but non-recursive, function.

You may want a different pattern of access.

If you want to be called for stripes rather than the full image, you can implement the function

bool useStripes() const

to return true.

You can then implement the function

int stripeHeight() const

to describe the height of the stripes that you wish to be called for. Stripes will start at the origin, be of stripeHeight(); and then clipped to the bounding box size. Stripes are always full-width for purposes for interoperability with the Row-based parts of Nuke.

By default renderStripe will called for the requested channels in each layer individually. It is possible to override this behaviour with the function

PlaneID is a presently a typedef to ChannelSet (in subsequent versions it might end up being a simpler class that has a default-constructor from ChannelSet). The default implementation implements the following mapping

Chan_Red Mask_RGBA
Chan_Green Mask_RGBA
Chan_Blue Mask_RGBA
Chan_Alpha Mask_RGBA
Chan_U Mask_MoVec
Chan_V Mask_MoVec
Chan_Backward_U Mask_MoVec
Chan_Backward_V Mask_MoVec

If you wanted the forward and backward vectors to be calculated seperately, you could override the function to provide the following mapping

There is a further virtual function you can implement

The return value of this indicates whether the Iop prefers to produce packed or unpacked output planes. If it returns ePackedPreferenceNone it has no strong preference and renderStripe() can be called with either depending upon which the calling code finds more convenient; if it returns ePackedPreferencePacked ePackedPreferenceUnpacked then renderStripe() will only be called to fill a packed or unpacked stripe.

PlanarI

Some plugins may wish to directly implement the slightly higher-level class PlanarI. This might be because you also need to inherit from some other type of Iop. The functions

virtual PackedPreference packedPreference() const = 0;

and

virtual PlaneID getPlaneFromChannel(Channel chan);

are to be implemented as with PlanarIop. Rather than implementing stripeHeight(), PlanarI allows you to have a stripes of various different heights. Implement

virtual size_t getStripeCount() const = 0;

virtual Box getStripeBox(int idx) const = 0;

virtual size_t rowToStripeIndex(int y) const = 0;

The PlanarI does not have an actual render call; you must override the function

virtual void doFetchPlane(ImagePlane& imagePlane);

Unlike renderStripe() which is constrained so that it will only ever be called with the imageplane in the preferred format, doFetchPlane() should be able to cope with any combination of bounding boxes; the stripe/planes/packed preferences are hints rather than mandatory.

Image Planes

The ImagePlane class is at the heart of the Planar code. ImagePlanes can be of various formats, with different bounding box, channels and packness. The bbox can be obtained with the accessor function bounds(); the channels with channels() and the packness with packed().

In all modes there is no padding. For example, in packed mode, with a 2x2 image, with RGBA channels, the floats would be

red 0,0 green 0,0 blue 0,0 alpha 0,0
red 0,1 green 0,1 blue 0,1 alpha 0,1
red 1,0 green 1,0 blue 1,0 alpha 1,0
red 1,1 green 1,1 blue 1,1 alpha 1,1

whereas in unpacked mode the data is represented by

red 0,0 red 0,1 red 1,0 red 1,1
green 0,0 green 0,1 green 1,0 green 1,1
blue 0,0 blue 0,1 blue 1,0 blue 1,1
alpha 0,0 alpha 0,1 alpha 1,0 alpha 1,1

ImagePlane provides several accessor functions for its data.

Low-level access

ImagePlane::readable() points at the start of the allocation. For the examples above, this will point at the red at (0, 0).

rowStride(), colStride() and chanStride() can then be used to multiply y, x, and channel values, to add to these. For example, in the first,

colStride() 4
rowStride() 8 (2 * 4)
chanStride() 1

and in the second

colStride() 1
rowStride() 2
chanStride() 4

data() points at the bottom-left corner of the image, which is not necessarily at the origin (0, 0).

There is no support for images with origins at the top-left.

Higher-level access

You can access individual pixels with

const float& ImagePlane::at(int x, int y, int z)

z is the channel number /within/ the ChannelSet for the ImagePlane, i.e. for a Mask_RGBA image plane then red = 0, green = 1, blue = 2, alpha = 3. If it was just red, green and alpha, then, then this would make red = 0, green = 1, alpha = 2. The function ImagePlane::chanNo() can be used to look this up but this iterates over the ChannelSet and you should avoid calling it within a tight loop.

There is also a

const float& ImagePlane::at(int x, int y, Channel z)

which does the lookup of the Channel for you.

These functions give references to the actual image data. If you want to abstract some of that away, there are functions that return strided pointers.

const ImageTilePtr ImagePlane::readableAt(int y, int z);

This returns a pointer to (0, y, z). The pointer is strided: so it knows what chanStride() is, so that ++ increments it to the next x value.

Note that returns a pointer to (0, y, z) even if the x bounds of the origin start rightwards of the origin. This means that the pointer cannot be safely deferenced without incrementing by (at a minimum) the x bounds. This is there to make multiplying in the offset slightly easier, and is consistent with the way that Row::operator[] works.

Writing to ImagePlane

ImagePlanes use shallow copying when possible.

If you are writing to an ImagePlane that might be shared with what ought to be read-only copies then you should call

void ImagePlane::makeUnique()

If you are writing to a new ImagePlane that you made, you should call

void ImagePlane::makeWritable()

One pattern that is valid, for example, is

void renderStripe(ImagePlane& outPlane) {

  input0().fetchPlane(outPlane);

  outPlane.makeUnique()

  Box box = outPlane.bounds();

  foreach(z, outPlane.channels()) {
    for (Box::iterator it = box.begin(); it != box.end(); it++) {
      outPlane.writableAt(outPlane.chanNo(z), it.x, it.y) *= 2;
    }
  }
}

If there is another copy that that shares backend data then it will copy the data and then make the modifications in-place. If there is no extra copy that is needed, because it has decided that the input does not need caching (perhaps it is really cheap to calculate), then it will not need to even do a copy, thus reducing peak memory use.

If you want to build an output from scratch, you can do something like:

outPlane.makeWritable()

foreach(z, outPlane.channels()) {
  for (Box::iterator it = box.begin(); it != box.end(); it++) {
    outPlane.writableAt(outPlane.chanNo(z), it.x, it.y) = 0.5;
  }
}

Table Of Contents

Previous topic

Iop: Spatial Operators

Next topic

Working with Channels