SciJava in Detail

This notebook dives into technical details of the SciJava Common library upon which ImageJ2 is built.

It is recommended that you first read and understand the Fundamentals of ImageJ notebook before tackling this one.

Let's get started by discussing a little bit about the Architecture of ImageJ.

The ImageJ software stack

The ImageJ software stack is composed of the following core libraries:

  • SciJava Common - The SciJava application container and plugin framework.
  • ImgLib2 - The N-dimensional image data model.
  • ImageJ Common - Metadata-rich image data structures and SciJava extensions.
  • ImageJ Ops - The framework for reusable image processing operations.
  • SCIFIO - The framework for N-dimensional image I/O.

These libraries form the basis of ImageJ-based software.

Important design goals of ImageJ include:

  • Modularity. ImageJ libraries provide a good separation of concerns. Developers in need of specific functionality may depend on only those components which are relevant, rather than needing to add a dependency to the entire ImageJ software stack.

  • UI agnosticm. The core libraries take great pains to be UI agnostic with no dependencies on packages such as java.awt or javax.swing. It should be possible to build a user interface (UI) on top of the core libraries without needing to change the library code itself. There are several proof-of-concept UIs for ImageJ using different UI frameworks, including Swing, AWT, Apache Pivot and JavaFX.

  • Extensibility. ImageJ provides many different types of plugins, and it is possible to extend the system with your own new types of plugins. See the "Extending ImageJ" tutorials for details.

For further details, see the Architecture page.

SciJava plugins

First and foremost, SciJava Common is a plugin framework—a base for developing highly modular and extensible Java applications. All plugins available on Java's classpath are automatically discovered and made available. This is accomplished by scanning classpath resources for the file path META-INF/json/org.scijava.plugin.Plugin. Such files are generated at compile time by a Java annotation processor that writes them in response to <code>@Plugin</code> annotations on Java classes.

For example, here is the metadata describing plugins of the net.imagej:imagej artifact itself:

In [1]:
%classpath config resolver scijava.public https://maven.scijava.org/content/groups/public
%classpath add mvn net.imagej imagej 2.0.0-rc-71
net.imagej.ImageJ.class.getResource("/META-INF/json/org.scijava.plugin.Plugin").
    openStream().getText().replaceAll("\\}\\{", "}\n{")
Added new repo: scijava.public
Out[1]:
{"class":"net.imagej.ImageJ","values":{"type":"org.scijava.Gateway"}}
{"class":"net.imagej.app.ToplevelImageJApp","values":{"name":"ImageJ","priority":101.0,"type":"org.scijava.app.App"}}

This metadata aligns precisely with the @Plugin declarations in ImageJ.java and ToplevelImageJApp.java, respectively:

@Plugin(type = Gateway.class)
public class ImageJ extends AbstractGateway

@Plugin(type = App.class, name = ImageJApp.NAME,
    priority = ImageJApp.PRIORITY + 1)
public class ToplevelImageJApp extends ImageJApp

Comparison with ImageJ 1.x

Here is a "cheat sheet" listing the available plugin types of ImageJ 1.x, and their ImageJ2 counterparts:

Plugin type ImageJ 1.x ImageJ2
General-purpose command ij.plugin.PlugIn net.imagej.command.Command
Image processing operation ij.plugin.filter.PlugInFilter net.imagej.ops.Op
Tool (toolbar icon + behavior) ij.plugin.tool.PlugInTool net.imagej.tool.Tool
File format reader/writer ij.plugin.PlugIn + HandleExtraFileTypes org.scijava.io.IOPlugin

There are many other SciJava and ImageJ2 plugin types; see Fundamentals of ImageJ for a complete list.

The SciJava Context

Everything in a SciJava application is unified by a single org.scijava.Context object: a collection of Service plugins, each of which provide API and holds local state information. The Context keeps track of the currently available plugins and services.

Each application is responsible for creating its own Context to manage plugins and contextual state.

Consider the following (hopefully familiar by now) invocation:

In [2]:
ij = new net.imagej.ImageJ()
"ImageJ v${ij.getVersion()} is ready to go."
Out[2]:
ImageJ v2.0.0-rc-71 is ready to go.
[INFO] Success!

The cell above creates a new net.imagej.ImageJ gateway object, which creates a new org.scijava.Context internally.

You can obtain the Context itself from the gateway:

In [3]:
ctx = ij.context()

The Context keeps two very central data structures: a PluginIndex and a ServiceIndex:

In [4]:
"There are ${ctx.getPluginIndex().size()} plugins, ${ctx.getServiceIndex().size()} of which are services."
Out[4]:
There are 1562 plugins, 91 of which are services.

Requesting services

Here are five ways to request services.

1. Retrieve a core service from the gateway

In [5]:
ij.event()
Out[5]:
org.scijava.event.DefaultEventService [priority = 100000.0]

2. Retrieve it from the context via service

In [6]:
ctx.service(org.scijava.event.EventService.class)
Out[6]:
org.scijava.event.DefaultEventService [priority = 100000.0]

If the service does not exist, service throws NoSuchServiceException.

3. Retrieve it from the context via getService

In [7]:
ctx.getService(org.scijava.event.EventService.class)
Out[7]:
org.scijava.event.DefaultEventService [priority = 100000.0]

If the service does not exist, getService returns null.

4. Declare a #@ script parameter

When writing a SciJava script, you can use the #@ parameter syntax:

In [8]:
script = """
#@ EventService es
"Got an EventService: " + es
"""
ij.script().run('script.groovy', script, true).get().getReturnValue()
Out[8]:
Got an EventService: org.scijava.event.DefaultEventService [priority = 100000.0]

If no such service is available, an exception will be thrown.

5. Annotate a field with @Parameter

When writing a Java class, the @Parameter annotation can be used to mark fields for which you want SciJava services to be injected automatically.

Typically, ImageJ plugin developers will be writing Service and/or Command plugins. If you need to use another plugin—for example, the LogService—you should not manually create it as this effectively disconnects you from your Context. Instead, you should ask your Context for an instance by adding a field of the desired type and annotating it with the @Parameter annotation. For example:

In [9]:
import org.scijava.command.Command
import org.scijava.log.LogService
import org.scijava.plugin.Parameter
import org.scijava.plugin.Plugin

@Plugin(type = Command.class)
public class MyPlugin implements Command {
 
  // This @Parameter notation is 'asking' the Context
  // for an instance of LogService.
  @Parameter
  private LogService log;
  
  @Parameter
  private String message;
 
  @Override
  public void run() {
    // Just use the LogService!
    // There is no need to construct it, since the Context
    // has already provided an appropriate instance.
    log.info(message);
  }
}

// Save a reference to the class for later.
myPluginClass = MyPlugin.class

// TODO: Figure out why the log message in this command is swallowed.
// Execute our sample command.
ij.command().run(MyPlugin.class, true, "message", "Success!").get()
Out[9]:
MyPlugin

This will allow the Context to provide you with—i.e., inject—the appropriate instance of your requested service.

In some rare cases, manual plugin construction is unavoidable. Understand that if the MyPlugin class above is manually constructed—i.e. via new MyPlugin()—the LogService parameter will be null. Automatic population only occurs if the plugin instance itself is retrieved via the framework. When you must manually construct a plugin instance, you can still re-connect it to an existing Context via its injection mechanism:

In [10]:
// Manually create a plugin instance.
// It is not connected to a Context yet
plugin = new MyPlugin()
 
// Inject the plugin instance with our Context.
ctx.inject(plugin)
 
// Now that our plugin is injected, we can use it with the
// knowledge that its service parameters have been populated.
plugin.run() // but message is still null
[INFO] null
Out[10]:
null

Services

Services are—surprise!—SciJava Plugins. Just like plugins, there are Service interfaces and implementing classes. This allows a proper separation between the Service's public contract and the details of its implementation.

Services are defined as interfaces, with concrete implementations as plugins. This design provides seams in the right places so that behavior at every level can be customized and overridden.

Services provide two important functions to the SciJava framework: utility methods and persistent state. If you want to add reusable Java methods that can be used throughout the SciJava framework, then you should create a Service to provide this functionality. If you need to track Context-wide variables or configuration, a Service should be used to encapsulate that state.

Conceptually, a Service satisfies the role of static utility classes on a per-Context basis. In this way, only one instance of each Service class can be associated with a given Context instance; an association that occurs automatically during Context creation. Furthermore, when a Context is asked for an implementation of a given Service, only the highest priority instance will be returned.

Services often build on or reuse functionality defined in each other. For example, the PluginService sees ubiquitous use in retrieving and working with plugin instances. For such reuse, <code>@Parameter</code> annotation can be used to declare inter-service requirements. During Context startup, these relationships will be resolved automatically.

In [11]:
import org.scijava.service.SciJavaService
import org.scijava.service.Service
import org.scijava.service.AbstractService
import org.scijava.app.StatusService
import org.scijava.plugin.Plugin
import org.scijava.plugin.Parameter

// Example Service Interface:
public interface HelloService extends SciJavaService {
  public void sayHello()
}

// Example implementation:
@Plugin(type = Service.class)
public class DefaultHelloService extends AbstractService implements HelloService {

  @Parameter
  private StatusService status;

  @Override
  public void initialize() {
          // initialize as little as possible here
  }

  @Override
  public void sayHello() {
          status.showStatus("Howdy!");
  }
}

"HelloService ready"
Out[11]:
HelloService ready

Commands

Whereas Services provide internal functionality, Commands are plugins designed to be executed as one-offs, typically interacting with users to achieve some desired outcome. When opening the ImageJ GUI, Commands are what populate your menu structure: exposing functionality and algorithms in a way that can be consumed by non-developers.

When writing Commands you will often declare @Parameters on fields that cannot be resolved automatically by the Context—for example, numeric values or file paths. Instead of being instantiated at Context startup as a Service would be, Commands are created and executed on demand.

When a Command is executed, it goes through a series of pre-processing steps to populate its @Parameters using its associated Context. If any parameters are left unresolved and a UI is available, the framework will automatically build and display an appropriate dialog to get user input. In this way, input harvesting is decoupled from functional operation—allowing developers to focus on what's really important without repetition of code. This also means that Commands can typically run headlessly without any extra development effort.

A common pattern in Command development is to wrap Service functionality. For example, opening an image from a path is a fundamental operation in ImageJ. To this end, developers can directly use the DatasetIOService. Users then get this same functionality from the menus via the OpenDataset command—which itself simply calls into the DatasetIOService.

Gateways

A Gateway is a plugin intended to make life easier for developers. It wraps a Context, offering type-safe access to core services. Everything you can do with a gateway you can also do without it—but the gateway object makes the API much more succinct and convenient.

Each major layer of the ImageJ software stack has its own Gateway:

In [12]:
ij.plugin().getPluginsOfType(org.scijava.Gateway.class).stream().map{info -> [
    "Class": info.loadClass().getName(),
    "Location": info.getLocation().replaceAll('.*/(.*\\.jar)$', '$1')
]}.collect()

TODO: corresponding service marker interfaces: SciJavaService, ImageJService, SCIFIOService

In [13]:
// Create a new SciJava gateway wrapping our existing Context.
sj = new org.scijava.SciJava(ctx)

// Now bask in the convenience!
import org.scijava.service.Service
[
    "Plugin count"      : sj.plugin().getPlugins().size(),
    "Module count"      : sj.module().getModules().size(),
    "Service count"     : sj.plugin().getPluginsOfType(Service.class).size(),
    "SciJava version"   : sj.getVersion(),
    "Where is SciJava?" : sj.getApp().getLocation()
]
In [14]:
// We don't _need_ the gateway; we could use each service directly instead.
pluginService = ctx.service(org.scijava.plugin.PluginService.class)
moduleService = ctx.service(org.scijava.module.ModuleService.class)

import org.scijava.service.Service
[
    "Plugin count"      : pluginService.getPlugins().size(),
    "Module count"      : moduleService.getModules().size(),
    "Service count"     : pluginService.getPluginsOfType(Service.class).size()
]

Other plugins and services

Because virtually everything is a plugin in ImageJ, there are too many to explicitly enumerate, let alone cover in a tutorial. To get ideas for functionality that can be added, a good starting point is to look for services in the javadoc, or the ImageJ search portal. Many service types have supplemental plugins for easy functional extension. In particular, the imagej-common and scijava-common repositories will contain plugin definitions for many essential operations.

A brief list of some of the more useful plugin types to extend:

  • Ops provide a reusable set of image processing algorithms.
  • Image formats allow new types of images to be opened in ImageJ.
  • Converters allow the framework to interchange types, outside of normal Java class hierarchy restrictions.
  • Input Preprocessors give you control over the population of <code>@Parameters</code>.
  • Displays control how UI elements are presented to users.

If you know the function you want to modify but can't determine its location in the code, please ask other developers. You're part of the community now!