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.