Random Access

eAccessRandom allows for any pixel position in the specified Image to be accessed from any other pixel position. This is mostly used when the read or write positions cannot be specified before runtime. This means that the optimisations that can be used during ranged access cannot be applied when using random access, and kernels can take much longer to run.

The ConvolutionKernel

This Convolution kernel demonstrates random access. a convolution is a weighted two-dimensional blur. The blur weights are taken from a second input image, filter, so for every output pixel, the kernel needs to access all the pixels from the filter image. We do this using random access.

// Copyright (c) 2024 The Foundry Visionmongers Ltd.  All Rights Reserved.

/// Convolution kernel: Convolves the input image with a filter.
// Warning: connecting a large image to the filter input will cause the kernel to run very slowly!
// If running on a GPU connected to a display, this will cause problems if the time taken to 
// execute the kernel is longer than your operating system allows. Use with caution!
kernel ConvolutionKernel : public ImageComputationKernel<ePixelWise>
{
  Image<eRead, eAccessRanged2D, eEdgeClamped> src;
  Image<eRead, eAccessRandom> filter;  
  Image<eWrite> dst;

local:
  int2 filterOffset;

  void init()
  {
    // Get the size of the filter input and store the radius.
    int2 filterRadius(filter.bounds.width() / 2, filter.bounds.height() / 2);

    // Store the offset of the bottom-left corner of the filter image from the current pixel
    filterOffset.x = filter.bounds.x1 - filterRadius.x;
    filterOffset.y = filter.bounds.y1 - filterRadius.y;

    // Set up the access for the src image
    src.setRange(-filterRadius.x, -filterRadius.y, filterRadius.x, filterRadius.y);
  }

  void process() {

    SampleType(src) valueSum(0.f);
    ValueType(filter) filterSum(0.f);

    // Iterate over the filter image
    for(int j = filter.bounds.y1; j < filter.bounds.y2; j++) {
      for(int i = filter.bounds.x1; i < filter.bounds.x2; i++) { 
        // Get the filter value
        ValueType(filter) filterVal = filter(i, j, 0);

        // Multiply the src value by the corresponding filter weight and accumulate
        valueSum += filterVal * src(i + filterOffset.x, j + filterOffset.y);

        // Update the filter sum with the current filter value
        filterSum += filterVal;
      }
    }

    // Normalise the value sum, avoiding division by zero
    if (filterSum != 0.f) {
      valueSum /= filterSum;
    }

    dst() = valueSum;
  }
};

Configuring Random Access

Image Specification

An image specification must be specified to use eAccessRandom, along with an optional desired behaviour for edges, eEdgeClamped or eEdgeConstant. For example:

Image<eRead, eAccessRandom, eEdgeClamped> src;

The ConvolutionKernel needs to access the whole of the filter input at every output pixel. In this case access to any pixels outside the filter input is not intended, so no edge method is specified.

Image<eRead, eAccessRandom> filter;  

Initialisation

Since random access lets you access any input pixel at any output location, there is no need to specify anything about the access requirements for the Image in the init() function.

However, it is often useful to know the bounds of a random access input. In the ConvolutionKernel example, the size of the filter input is required to determine the size of the convolution. To do this, the kernel uses the bounds member accessed via filter.bounds.

void init()
{
  // Get the size of the filter input and store the radius.
  int2 filterRadius(filter.bounds.width() / 2, filter.bounds.height() / 2);

  // Store the offset of the bottom-left corner of the filter image from the current pixel
  filterOffset.x = filter.bounds.x1 - filterRadius.x;
  filterOffset.y = filter.bounds.y1 - filterRadius.y;

  // Set up the access for the src image
  src.setRange(-filterRadius.x, -filterRadius.y, filterRadius.x, filterRadius.y);
}

Accessing Randomly

Pixels are accessed using absolute pixel positions, with no reference to the current iteration pixel position. For example, this kernel accesses the pixels using absolute positions using eAccessRandom, and performs the equivalent operation to the kernel in the 1D Ranged Access example.

kernel RandomAccessReadExample : ImageComputationKernel<eComponentWise>
{
  Image<eRead, eAccessRandom, eEdgeClamped> src;
  Image<eWrite> dst;

  void process(int2 pos) {
    float sum = 0.f;
    // Read the left adjacent input pixel
    sum += src(pos.x - 1, pos.y);
    // Read the current position input pixel
    sum += src(pos.x, pos.y);
    // Read the right adjacent input pixel
    sum += src(pos.x + 1, pos.y);
    // output the average
    dst() = sum / 3.0f;
  }
};

It is also possible to write to a random pixel position of the output image. For example:

kernel RandomAccessWriteExample : ImageComputationKernel<eComponentWise>
{
  Image<eRead> src;
  Image<eWrite, eAccessRandom, eEdgeClamped> dst;

  void process(int2 pos) {
    // Only perform the write with one thread
    if (pos.x == 0 && pos.y == 0) {
      // Write a value at the [100, 100] position of the output image
      dst(100, 100) = src();
    }
  }
};

ConvolutionKernel example

Inside the process() function, the ConvolutionKernel iterates over the filter image to perform the convolution. The filter is centred over the current output position and each input pixel covered by the filter is multiplied by the corresponding filter weight. These weighted input values are accumulated, as are the filter weights; if the filter weights do not sum to one, this is compensated for at the end in order to preserve the brightness of the input.

void process() {

  SampleType(src) valueSum(0.f);
  ValueType(filter) filterSum(0.f);

  // Iterate over the filter image
  for(int j = filter.bounds.y1; j < filter.bounds.y2; j++) {
    for(int i = filter.bounds.x1; i < filter.bounds.x2; i++) { 
      // Get the filter value
      ValueType(filter) filterVal = filter(i, j, 0);

      // Multiply the src value by the corresponding filter weight and accumulate
      valueSum += filterVal * src(i + filterOffset.x, j + filterOffset.y);

      // Update the filter sum with the current filter value
      filterSum += filterVal;
    }
  }

  // Normalise the value sum, avoiding division by zero
  if (filterSum != 0.f) {
    valueSum /= filterSum;
  }

  dst() = valueSum;
}

The ConvolutionKernel has pixel-wise access to its src input so it can access a whole pixel at a time. A pixel has the SampleType of the input, accessed here using SampleType(src). All images in Nuke are floating point, so SampleType here will be a vector of floating point values. The number of components in the vector will be the same as the number of components in src; for example, this will be 4 if src is an RGBA image. The declaration above initialises all of the components to zero.

With pixel-wise access it is also possible to access a single component from an input pixel. This is done with the filter input, as only the first component is used for the filter weights. A single component’s type will be the ValueType of its input. filterSum, is declared as type ValueType(filter) and its single value is initialised to zero. The kernel iterates over the bounds of the filter image in order to accumulate the weighted src values.

As random access is not relative to the current position, the filter input is accessed using the position (i, j) inside the filter image. filter(i, j, 0) returns the zeroth component at this position as a ValueType(filter).

The src is accessed with a 2D range, as seen in the BoxBlur2DKernel. This time an entire pixel is accessed, rather than a single component. The access src(xOffset, yOffset) returns a value of type SampleType(src). This is multiplied by the filter weight and accumulated into valueSum.

At the end of the process() function, the valueSum is normalised by the accumulated filter weights to preserve the brightness of the input. The ConvolutionKernel doesn’t have any control over the values in its filter input, which might contain all zero values, so care is needed to avoid a division by zero.