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 is composed of the following core libraries:
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.
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 @Plugin
annotations on Java classes.
For example, here is the metadata describing plugins of the net.imagej:imagej
artifact itself:
%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
{"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
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.
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:
ij = new net.imagej.ImageJ()
"ImageJ v${ij.getVersion()} is ready to go."
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:
ctx = ij.context()
org.scijava.Context@28191fb1
The Context
keeps two very central data structures: a PluginIndex
and a ServiceIndex
:
"There are ${ctx.getPluginIndex().size()} plugins, ${ctx.getServiceIndex().size()} of which are services."
There are 1562 plugins, 91 of which are services.
Here are five ways to request services.
ij.event()
org.scijava.event.DefaultEventService [priority = 100000.0]
service
¶ctx.service(org.scijava.event.EventService.class)
org.scijava.event.DefaultEventService [priority = 100000.0]
If the service does not exist, service
throws NoSuchServiceException
.
getService
¶ctx.getService(org.scijava.event.EventService.class)
org.scijava.event.DefaultEventService [priority = 100000.0]
If the service does not exist, getService
returns null
.
#@
script parameter¶When writing a SciJava script, you can use the #@
parameter syntax:
script = """
#@ EventService es
"Got an EventService: " + es
"""
ij.script().run('script.groovy', script, true).get().getReturnValue()
Got an EventService: org.scijava.event.DefaultEventService [priority = 100000.0]
If no such service is available, an exception will be thrown.
@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:
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()
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:
// 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
null
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, @Parameter
annotation can be used to declare inter-service requirements. During Context
startup, these relationships will be resolved automatically.
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"
HelloService ready
Whereas Service
s provide internal functionality, Command
s 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 Command
s you will often declare @Parameter
s 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, Command
s are created and executed on demand.
When a Command
is executed, it goes through a series of pre-processing steps to populate its @Parameter
s 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 Command
s 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
.
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
:
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
// 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()
]
// 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()
]
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 @Parameters
.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!