Calling Ops with the OpBuilder

Ops are designed to be called from the Op matcher, using the OpBuilder syntax. OpBuilder chains follow the builder pattern, allowing users to create complex Op calls by β€œchaining” or appending consecutive, simpler method calls.

On this page, we will be constructing an OpBuilder call on an OpEnvironment ops to execute a Gaussian Blur on an input image inImage, with our output buffer outImage.

Specifying the name with .op()

From the OpEnvironment, an OpBuilder call is initialized with the method OpEnvironment.op(String), which is used to describe the name of the Op that the OpBuilder must return:

ops.op("filter.gauss")

Passing the inputs with .input()

With the name defined in the OpBuilder call, we can then chain the inputs with the input() method.

For our gaussian blur, we will pass as inputs our input image inImage, and a double as our sigma parameter:

ops.op("filter.gauss").input(inImage, 2.0)

Passing an output buffer with .output()

Now that the inputs are specified, we can chain the output buffer using the output() method.

For our gaussian blur, we will pass as the output buffer our output image outImage:

ops.op("filter.gauss").input(inImage, 2.0).output(outImage)

Computing with .compute()

With all of the components of the needed Op specified, we can begin computation with the .compute() method.

ops.op("filter.gauss").input(inImage, 2.0).output(outImage).compute()

In the call to compute(), the OpEnvironment will use the components of the OpBuilder syntax to:

  • Match an Op based on the name provided, as well as the types of the provided input and output Objects

  • Execute the Op on the provided input and output Objects.

Additions: Repeating execution

When an Op should be executed many times on different inputs, the OpBuilder syntax can be modified to return the Op instead. Instead of calling the .compute() function at the end of our OpBuilder call, we can instead call the .computer() method to get back the matched Op:

var gaussOp = ops.op("filter.gauss").input(inImage, 2.0).output(outImage).computer()
gaussOp.compute(inImage, 2.0, outImage)

Note that the default OpEnvironment implementations cache Op requests - this means that repeated OpBuilder requests targeting the same action will be faster than the original matching call.

Additions: Matching with classes

In addition to the .input() and .output() builder steps, there are parallel .inType() and .outType() methods. These accept either a Class or a Nil - the latter allowing retention of generic types.

var computer = ops.op("filter.gauss").inType(ImgPlus.class, Double.class).outType(ImgPlus.class).computer()

When using the *Type methods of the builder, the terminal steps will only allow creation of the Op, not direct execution, since the parameters have not been concretely specified yet.

Common Pitfalls: Wildcards

Using wildcards, such as Img<?> inImage, can make Op reuse difficult. For example, the following code segment will not compile in a Java runtime:

Img<?> inImage = ArrayImgs.unsignedBytes(128, 128);
var gaussOp = ops.op("filter.gauss").input(inImage, 2.0).output(outImage).computer();
gaussOp.compute(inImage, 2.0, outImage);

Solution 1: Use compute instead of computer

If you don’t need to save the Op to a variable, just call it directly:

ops.op("filter.gauss").input(inImage, 2.0).output(outImage).compute();

Generally speaking, op requests are cached, meaning repeated OpBuilder calls that directly execute Ops will not significantly decrease performance.

Solution 2: Avoid using wildcards

If you know that your Img will always contain unsigned byte values, for example, define your variable as an Img<UnsignedByteType> rather than using Img<?>.

Solution 3: Use raw casts (not type-safe!)

Img<?> inImage = ArrayImgs.unsignedBytes(128, 128);
var gaussOp = ops.op("filter.gauss").input(inImage, 2.0).output(outImage).computer();
gaussOp.compute((Img) inImage, 2.0, outImage);

This method should only be used as a last resort.