Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 60 additions & 18 deletions docs/ops/doc/CallingOps.md
Original file line number Diff line number Diff line change
@@ -1,61 +1,96 @@
# 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](https://refactoring.guru/design-patterns/builder), allowing users to create complex Op calls by "chaining" or appending consecutive, simpler method calls.
Use of the Ops framework centers on a process of matching Op requests to algorithm implementations based on the parameters provided. The easiest way to make these queries is to use the `OpBuilder` syntax, which follows the [builder pattern](https://refactoring.guru/design-patterns/builder) to assemble the required components of an Op matching request from a particular `OpEnvironment`.

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`.
In this page, we start after having [identified a Gaussian Blur Op](SearchingForOps) that we would like to use. We assume we already have created an `OpEnvironment` named `ops`, as well as the input image to blur, and a pre-allocated output image for the result—`inImage` and `outImage`, respectively.

**Note:** we are incrementally constructing one line of code in this example. Running an intermediate step simply returns an appropriate builder that knows what has been set so far, and which step is next. If you're following along in an IDE or script editor, the code you actually *run* would be the last step, once our builder call is fully constructed.

## 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:
From the `OpEnvironment`, an `OpBuilder` chain is initialized with the `op(String)` method, which describes the name of the Op that we ultimately want to call:

```groovy
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.
With the name established in the `OpBuilder` chain, we can then specify our input(s) with the `.input()` method.

For our gaussian blur, we will pass as inputs our input image `inImage`, and a `double` as our sigma parameter:
For this Gaussian blur, we have two inputs: `inImage` is the image we want to blur, and a `double` value as our sigma parameter:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
For this Gaussian blur, we have two inputs: `inImage` is the image we want to blur, and a `double` value as our sigma parameter:
For this Gaussian blur, we have two inputs: `inImage` is the image we want to blur, and a `double` value as the standard deviation of the smoothing function:

What do you think about using the technical term?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the simpler our sigma parameter 😉


```groovy
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.
After specifying inputs, we provide a preallocated output container using the `.output()` method.

For our gaussian blur, we will pass as the output buffer our output image `outImage`:
For our Gaussian blur, we will pass our output image `outImage` as a receptacle for the result:

```groovy
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.
With all of our desired Op's inputs and output now specified, we can run it with the `.compute()` method.

```groovy
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 `Object`s
* Execute the Op on the provided input and output `Object`s.
In the call to `compute()`, the `OpEnvironment` will use all of the parameters provided 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.

After this step, `outImage` will contain the results of the Gaussian blur on `inImage`.

## Variations on use

## Additions: Repeating execution
### Using a *function* or *inplace* Op

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:
Calling our Gaussian blur as a *computer* above is great when we have pre-allocated output, but for other scenarios we can request Ops as *functions* or *inplaces*.

*Functions* are used when we want to *create* the final output, indicated by ending the builder with `.apply()`:

```groovy
var outImage = ops.op("filter.gauss").input(inImage, 2.0).apply()
```

*Inplaces* are used when we want to destructively modify one of the existing inputs (which is explicitly forbidden by *computers*; a *computer* Op's output should be a different object from all of its inputs). We indicate this by the `mutate#()` method, where the `#` corresponds to the *parameter index* that will be modified:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be confusing, or clarifying, to note this is 1-indexed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


```
# Modify the first input in-place
ops.op("filter.gauss").input(inImage, 2.0).mutate1()
```

Note that although the final method call changes for each mode of operation, *this is based on the path taken through the `OpBuilder` chain*. For example, we cannot call the `compute()` method if we haven't provided an `.output()`:

```
# Does not compute
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂

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

A key takeaway from this section is that how you **request** the Op does not necessarily need to match how the Op is **implemented**. *Functions* and *computers* should be largely interchangeable, thanks to the Ops engine's adaptation subsystem. For the 1.0.0 release we do not have the necessary adapters to go between *inplaces* and the other paradigms, but it is on our [development roadmap](https://github.com/scijava/scijava/issues/47)!

### Repeating execution

When you want to call an Op many times on different inputs, the `OpBuilder` can be used to return the *Op* itself, instead of performing the computation. Instead of calling the `.compute()` function at the end of our `OpBuilder` chain, we can use the `.computer()` method (or `.inplace()` or `.function()`, as appropriate) to get back the matched Op, which can then be reused via its `.compute()` method (or `.apply()` or `.mutate#()`, respectively):

```groovy
var gaussOp = ops.op("filter.gauss").input(inImage, 2.0).output(outImage).computer()
gaussOp.compute(inImage, 2.0, outImage)
gaussOp.compute(inImage, 2.0, outImage1)
gaussOp.compute(inImage, 5.0, outImage2)
```

While we do pass concrete inputs and outputs in this example, they are essentially just being used to reason about the desired *types* - which we'll cover in the next section.

*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
### Matching with classes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is classes better or worse than types?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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.
Expand All @@ -64,8 +99,15 @@ methods. These accept either a `Class` or a `Nil` - the latter allowing retentio
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.
In this case, we *must* use the `computer()` terminal method of the builder: we
can only *create* the Op, not directly execute it, since the parameters have
not been concretely specified yet. This is very sensible when we want to re-use a computer many times.

We can also use the `.outType()` methods to add type safety to our `Function` calls:

```java
Img outImage = ops.op("filter.gauss").input(inImage, 2.0).outType(Img.class).apply();
```

## Common Pitfalls: Wildcards

Expand All @@ -89,7 +131,7 @@ Generally speaking, op requests are **cached**, meaning repeated OpBuilder calls

### 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<?>`.
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!)

Expand Down
32 changes: 20 additions & 12 deletions docs/ops/doc/ScriptingInFiji.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,52 @@
# Scripting in Fiji

Using SciJava Ops within scripts unlocks the most powerful aspects of Ops. The following page will explain how you can write a script in Fiji's Script Editor that utilizes Ops for image processing.
Scripts provide a simple, familiar interface for accessing SciJava Ops and allows combination with additional resources for image processing and beyond. The following page will explain how you can write a script in Fiji's [Script Editor](https://imagej.net/scripting/) that utilizes Ops for image processing.

## Obtaining an OpEnvironment

To run Ops, scripts require an `OpEnvironment`. The easiest way to obtain an `OpEnvironment` with all available Ops is to declare an `OpEnvironment` as a script parameter:
To run Ops we always start with an `OpEnvironment`. Within Fiji, the easiest way to obtain an `OpEnvironment` is to declare it as a [script parameter](https://imagej.net/scripting/parameters):

```text
#@ OpEnvironment ops
```

## Obtaining inputs
## Setting inputs and outputs

Scripts using SciJava Ops obtain inputs like any other SciJava script, and the lines below will provide us with an `Img` input parameter and an `Img` output parameter.
A good starting point is to declare script parameters that match your desired Op's parameters. When performing image processing, we are operating on an existing image; we also want to create an output image in the script so that our result will be shown. The following lines use SciJava script parameters to obtain the active image as an input along with a user-defined sigma, while establishing our output.

```text
#@ Img imgInput
#@ Double sigma
#@output Img out
```

For more information on SciJava scripting parameters, please visit [this page](https://imagej.net/scripting/parameters).

## Calling Ops

The OpBuilder syntax should be used to retrieve and execute Ops from the `OpEnvironment`. The following line executes a Gaussian Blur on an input image using a `filter.gauss` Op:
The [OpBuilder syntax](CallingOps) should be used to retrieve and execute Ops from the `OpEnvironment`. The following line executes a Gaussian blur on an input image using a `filter.gauss` Op:
```text
out = ops.op("filter.gauss").input(imgInput, new Double(3.0)).apply()
out = ops.op("filter.gauss").input(imgInput, sigma).apply()
```

## Putting it all together

The below script can be pasted into the Script Editor. **Ensure that the Script Editor is configured to run a Groovy script**.
The below script can be pasted into the Script Editor. **Ensure that the Script Editor is configured to run a Groovy script** (*Language &rarr; Groovy* in the Script Editor menu).

```text
#@ OpEnvironment ops
#@ Img imgInput
#@ Double sigma
#@output Img out

// Call some Ops!
out = ops.op("filter.gauss").input(imgInput, new Double(3.0)).apply()
// Call our Op!
out = ops.op("filter.gauss").input(imgInput, sigma).apply()
```

Scripting in Fiji is a convenient gateway to accessing SciJava Ops. To see more, check out some examples, such as [image deconvolution](examples/deconvolution.rst) or [FLIM analysis](examples/flim_analysis.rst)!
## Add your Op to the menu

If you want to reuse an Op outside of the script editor: good news! All SciJava scripts are runnable as ImageJ commands, and can be [installed](https://imagej.net/plugins/index#installing-plugins-manually) into your Fiji installation. For example, suppose we create new nested folders in our `Fiji.app/scripts` directory: first a `Plugins` folder (if it does not already exist), and then inside of that, a new `Ops` folder. If we then save our script there as `Filter_Gauss.groovy` (or similar&mdash;just don't forget the `_`!) then after re-starting Fiji we can run our Op from the *Plugins &rarr; Ops* menu, which matches the folder structure we created. The command will also be accessible from the [search bar](https://imagej.net/learn/#the-search-bar).

## Next steps

Check out the How-To Guides for important information like how to [explore the available Ops](SearchingForOps).

Check out some examples such as [image deconvolution](examples/deconvolution.rst) or [FLIM analysis](examples/flim_analysis.rst) to see more complete cases of Ops being used in the Script Editor!
64 changes: 39 additions & 25 deletions docs/ops/doc/SearchingForOps.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
# Searching for Ops in the Environment

As the `OpEnvironment` is fully extensible, different `OpEnvironment`s might contain different Ops, and it is important to be able to query an `OpEnvironment` about the available Ops.
The first step when working with Ops is always to obtain an `OpEnvironment`: your gateway to all Ops functionality.

The `OpEnvironment.help()` API allows you to query an `OpEnvironment` about the types of Ops it contains, as we show in the following sections. **Note that the example printouts from the help API may not reflect the Ops available in *your* Op environment**.
If you're working in a [Fiji script](ScriptingInFiji) then this is done with a script parameter:

## Searching for operations
```
#@ OpEnvironment ops
```

The no-argument method `OpEnvironment.help()` is designed to give you a broad overview over the *categories* of Ops available within the `OpEnvironment`:
Otherwise we can import and build one ourselves:

```groovy
```
import org.scijava.ops.api.OpEnvironment
ops = OpEnvironment.build()
```

Typically we would only want to do this once per application, to avoid diverging environments and reincurring the performance cost of the build. All code examples in this section will assume we have created an `OpEnvironment` named `ops`.

As the `OpEnvironment` is fully extensible, different `OpEnvironment`s might contain different Ops, so it is important to be able to query an `OpEnvironment` about its available Ops. We also need to be able to get information about the usage of these Ops, to know what parameters may be required.

The `OpEnvironment.help()` API is your window into the `OpEnvironment`. In the following sections we cover the different types of information that can be obtained. **Note that the exact printouts from the help API may be different from the Ops available in *your* environment**.

## Listing Namespaces

The no-argument method `ops.help()` is designed to give you a broad overview over the *categories* (namespaces) of Ops available within the `OpEnvironment`:

```python
print(ops.help())
```

This gives the following printout:
Might print output such as:

```
```text
Namespaces:
> coloc
> convert
Expand Down Expand Up @@ -45,19 +59,19 @@ Namespaces:
> types
```

## Interrogating a Namespace
These namespace categories can then be interrogated further to explore the particular Ops in each.

## Querying a Namespace

You can choose one of the above namespaces, and `ops.help()` will give you information about the algorithms contained within:
```groovy
import org.scijava.ops.api.OpEnvironment
ops = OpEnvironment.build()

```python
print(ops.help("filter"))
```

This gives the following printout:
Prints the current list of `filter` ops in the `OpEnvironment`:

```
```text
Names:
> filter.dog
> filter.addNoise
Expand Down Expand Up @@ -96,35 +110,33 @@ Names:
> filter.variance
```

## Signatures for Op Names

Finally, you can use `OpEnvironment.help()` on any Op name to see the list of signatures:
## Querying Op Signatures

```groovy
import org.scijava.ops.api.OpEnvironment
ops = OpEnvironment.build()
Finally, you can use `ops.help()` on any Op name to see the list of signatures:

```python
print(ops.help("filter.gauss"))
```

```
```text
filter.gauss:
- (input, sigmas, @CONTAINER container1) -> None
- (input, sigmas, outOfBounds = null, @CONTAINER container1) -> None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not part of this PR I know, but this output mixes nulls and Nones... we should make that more consistent.

- (input, sigma, @CONTAINER container1) -> None
- (input, sigma, outOfBounds = null, @CONTAINER container1) -> None
```

Note that these descriptions are simple, and you can obtain more verbose descriptions by instead using the method `OpEnvironment.helpVerbose()`:
## In-depth Op Information

```groovy
import org.scijava.ops.api.OpEnvironment
ops = OpEnvironment.build()
The basic descriptions from `ops.help()` are intentionally simplified to avoid providing overwhelming amounts of information. However, you can obtain more complete descriptions, including documentation (if available), from `ops.helpVerbose()`:

```
print(ops.helpVerbose("filter.gauss"))
```

```
Gives us actual typing and usage notes for the parameters:

```text
filter.gauss:
- org.scijava.ops.image.filter.gauss.Gaussians.defaultGaussRAI(net.imglib2.RandomAccessibleInterval<I>,double[],net.imglib2.outofbounds.OutOfBoundsFactory<I, net.imglib2.RandomAccessibleInterval<I>>,net.imglib2.RandomAccessibleInterval<O>)
> input : net.imglib2.RandomAccessibleInterval<I>
Expand All @@ -147,3 +159,5 @@ filter.gauss:
> container1 : @CONTAINER net.imglib2.RandomAccessibleInterval<O>
the preallocated output image
```

The `ops.helpVerbose()` method can be used interchangeably whenever you would use `ops.help()`, as needed.
6 changes: 3 additions & 3 deletions docs/ops/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,15 @@ The combination of these libraries allows declarative image analysis workflows,

WhyOps
Installation
ScriptingInFiji

.. toctree::
:maxdepth: 2
:caption: 🪄 How-to guides

CallingOps
SearchingForOps
CallingOps
IntrospectingOps
Benchmarks
ScriptingInFiji

.. toctree::
:maxdepth: 2
Expand All @@ -43,6 +42,7 @@ The combination of these libraries allows declarative image analysis workflows,

WritingYourOwnOpPackage
Migrating Ops Written for ImageJ Ops<MigratingFromImageJOps>
Benchmarks

.. toctree::
:maxdepth: 2
Expand Down