2D Ranged Access

eAccessRanged2D tells us that for every output position, a kernel needs to access a two-dimensional range of positions from the given Image. Similar to 1D Ranged Access, using 2D Ranged Access allows for optimisations that come from knowing the exact range of access that a kernel will need, but applies to both the X and Y axes.

../../_images/safe.png

Safe and fast access regions for 2D ranged access.

The BoxBlur2DKernel

To demonstrate 2D ranged access we can extend the 1D box blur to work in two dimensions.

// Copyright (c) 2024 The Foundry Visionmongers Ltd.  All Rights Reserved.
kernel BoxBlur2D : public ImageComputationKernel<eComponentWise>
{
  Image<eRead, eAccessRanged2D, eEdgeClamped> src;
  Image<eWrite, eAccessPoint> dst;

param:
  int xRadius;  //The horizontal radius of the box blur
  int yRadius;  //The vertical radius of the box blur

local:
  int filterSize;

  void define() {
    defineParam(xRadius, "RadiusX", 5, eParamProxyScale);
    defineParam(yRadius, "RadiusY", 3, eParamProxyScale);
  }

  void init() {
    // Set the range we need to access from the source 
    src.setRange(-xRadius, -yRadius, xRadius, yRadius);

    filterSize = (2 * xRadius + 1) * (2 * yRadius + 1);
  }

  void process() {
    // Sum all the pixel values within radius
    float sum = 0.0f;
    for(int j = -yRadius; j <= yRadius; j++) {
      for(int i = -xRadius; i <= xRadius; i++) {
        sum += src(i, j);
      }
    }

    // Write out the average value
    dst() = sum / filterSize;
  }
};

Configuring 2D Ranged Image Access

Image Specification

First, an image specification must use eAccessRanged2D to allow for two-dimensional access to pixels. The desired behaviour for accessing outside of image edges is also required, either eEdgeClamped or eEdgeConstant.

The example BoxBlur2DKernel needs to access a two-dimensional range of pixels from its source image, with clamping edge behaviour, specified as follows:

  Image<eRead, eAccessRanged2D, eEdgeClamped> src;

Setting the Range

Similar to 1D access, you must specify the size of the desired range. This time, we set the range for both axes at the same time. This must be set up inside the init() function.

setRange(int xMin, int yMin, int xMax, int yMax)

This function sets the minimum and maximum extent of the range.

setRange(int rangeMin, int rangeMax)

This function sets the minimum and maximum extent of the range for both axes: equivalent to calling setRange(min, min, max, max).

For example, setRange(-2, 2) would allow accessing the 25-pixel square in the inclusive range [-2, 2] in both axes:

../../_images/2d-access.png

An example of 2D Ranged Access using setRange(-2, 2).

BoxBlur2DKernel example

In the box blur kernel, to set up the 2D range, we call setRange() inside the init() function. This time, we give it four parameters to specify the horizontal minimum, vertical minimum, horizontal maximum and vertical maximum respectively.

void init() {
  // Set the range we need to access from the source 
  src.setRange(-xRadius, -yRadius, xRadius, yRadius);

Accessing the 2D Range

Pixel positions are accessed using relative offsets from the current pixel position. Access to our 2D range must be done from inside the process() function. This is very similar to accessing the 1D range, except that we now need to specify both the X and Y offsets from the current position. For example, this kernel will access the pixels adjacent to the current pixel position being processed:

kernel RangedAccess2DReadExample : ImageComputationKernel<eComponentWise>
{
  Image<eRead, eAccessRanged2D, eEdgeClamped> src;
  Image<eWrite> dst;

  void init() {
    src.setRange(-1, 1);
  }

  void process() {
    float sum = 0;
    // Read the row of three adjacent pixels directly above the current position
    sum += src(-1, -1);
    sum += src( 0, -1);
    sum += src( 1, -1);

    // Read the row of three adjacent pixels at the current position
    sum += src(-1, 0);
    sum += src( 0, 0);
    sum += src( 1, 0);

    // Read the row of three adjacent directly below the current position
    sum += src(-1, 1);
    sum += src( 0, 1);
    sum += src( 1, 1);

    // output the average
    dst() = sum / 9.0f;
  }
};

It is also possible to write to a 2D range of the output image. For example:

kernel RangedAccess2DWriteExample : ImageComputationKernel<eComponentWise>
{
  Image<eRead> src;
  Image<eWrite, eAccessRanged2D, eEdgeClamped> dst;

  void init() {
    dst.setRange(-1, 1);
  }

  void process(int2 pos) {
    //Only perform the write if it will be within the image bounds
    if (pos.x > 0 && pos.y > 0) {
      // Write to the pixel to the left and above the current position
      dst(-1, -1) = src();
    }
  }
};

BoxBlur2DKernel example

The process() function of the BoxBlur2DKernel performs a nested loop, first looping over the vertical range, and for each vertical position, looping over the horizontal range, accumulating contributions to our box blur from each source position in turn. In the final step, the output pixel is set to the average value of all the contributions, using the value of filterSize we calculated inside the init() function.

void process() {
  // Sum all the pixel values within radius
  float sum = 0.0f;
  for(int j = -yRadius; j <= yRadius; j++) {
    for(int i = -xRadius; i <= xRadius; i++) {
      sum += src(i, j);
    }
  }

  // Write out the average value
  dst() = sum / filterSize;
}