The plugin Protocols provide a graphical environment that enables anyone to create graphically and without any prior knowledge in programming a bioimage analysis workflow, and automate it on many images (batch processing). Protocols can also contain blocks with custom scripts and be run from the command line, which makes it a very powerful tool in Icy. You can find more information about Protocols in the end-user documentation. In this article, we explain how to write your own protocol block.
To get you quickly started and inspired, you can also browse the online collection of Icy protocols or search directly through the Icy software search bar with your favorite keywords. The video below shows an example of protocol and its execution.
Protocols are composed of blocks with various input and output variables. Blocks are connected with each other, the output variable of the first block being the input variable of the next one.
Protocols is fully compatible with Maven to manage dependencies and create the JAR file that Icy uses to run the plugin. If you want more details about it, please check our Maven tutorial.
This plugin is working similarly as EzPlug by minimizing as much as possible the code to write.
You can begin from our plugin template available on Gitlab and select MyIcyPlugin as your main class (it is possible to rename it of course) and delete MyEzPlugIcyPlugin as we do not need EzPlug to create a new protocol block.
The Block interface
To write a block from scratch or to convert an existing plug-in into a block, the main class should extend Plugin (as usual) and implement the Block interface:
package plugins.adufour.blocks.lang.Block; public interface Block extends Runnable { void declareInput(VarList inputMap); void declareOutput(VarList outputMap); }
These two methods are quite explicit: the first one lets the developer add all block inputs to the specified input map, while the second one does likewise for outputs. Also, since this interface extends Runnable, your block should also contain a method called run() containing the main code.
Note to EzPlug users: since EzPlug already implements the Runnable interface, the run() method need not be implemented anymore. Hence, the main computation code may remain in your execute() method.
Handling and encapsulating objects with Var
Block inputs and outputs are encapsulated using Var objects, provided by EzPlug in the plugins.adufour.vars package. Var objects simplify the encapsulation of objects by adding various features such as value-change listeners and a referencing mechanism (a variable may be asked to transparently ‘point to’ another variable). In a nutshell, here is a short example of how Var objects work:
Integer i = 2; VarInteger varInt = new VarInteger("my number", 0); // the second argument is the default value VarInteger varInt2 = new VarInteger("some other number", 0); VarListener listener = new VarListener { public void valueChanged(Var source, Integer oldValue, Integer newValue) { System.out.println("the value of " + source.getName() + " changed from " + oldValue + " to " + newValue); } public void referenceChanged(Var source, Var<? extends Integer> oldReference, Var<? extends Integer> newReference) { System.out.println("the reference of " + source.getName() + " changed from " + oldReference + " to " + newReference); } } varInt.addListener(listener); varInt2.addListener(listener); varInt2.setReference(varInt); //will display "the reference of my other number changed from null to my number" varInt.setValue(i); // will display "the value of my number changed from 0 to 2" System.out.println(varInt2.getValue()); // will display "2"
Declaring block inputs
We will now focus on declaring inputs (declaration of outputs is analogous). Each block input should be added to the VarList parameter of the declareInput method, using any of the following methods:
- void add(Var<?> variable)
- void add(String id, Var<?> variable)
It is highly recommended to use the second version, which explicitly asks for a unique variable identifier. This unique identifier is used to save parameter values inside the protocol, and must be unique throughout the map (an exception will be thrown if a similar id already exists). This id must not be changed to ensure that parameter values will be correctly reloaded from disk. This being said, you are free to change the name of the variable itself at anytime (this name is the one displayed on the graphical interface). In case you opt for the first version, the variable name is considered as the id, and you therefore cannot change it afterwards.
Note to EzPlug users: since your plugin extends EzPlug, most of your parameters are probably EzVarXXX objects. If this is the case, no need to create new Var objects to represent input variables. Each EzVar contains a variable field of type Var. Hence, within the declareInput(..) method, all you have to do is to add one line per object in the form inputMap.add(“some unique ID”, myEzVar.variable).
Important note: in block mode, the initialize() method is no longer called. Therefore, make sure your EzVar objects are initialized upon declaration of the instance field and not in the initialize() method. It is not recommended to simply call the initialize() method within the declareInput(..) method, as the graphical user interface system works a little bit differently in standalone and block mode, and this may result in performance decrease and memory leaks.
Declaring block outputs
As you may guess, declaring block outputs is analogous to input case, nothing fancy here. Keep in mind however that output Var objects should be filled with the output of the algorithm, however this output should not be displayed directly to the user (or stored in some sort), as this is the goal of other blocks dedicated to these tasks.
Note to EzPlug users: it is presumable that your plug-in does nothing much about the output data (it is either sent to screen for display, to disk, to the swimming pool etc.). Therefore, the output should be (as usual) stored in Var instance fields, however one should ensure that nothing is displayed / stored when the plug-in is in block mode. To do so, use the EzPlug.isHeadless(), which will return false if the EzPlug runs standalone, or true if it is run headless, i.e., called from within another plugin or as a block.
Example:
// old code public void execute() { Sequence sout; // assume this contains a result addSequence(sout); // display the result on screen } // new code VarSequence outputSequence = new VarSequence("output sequence", null); public void execute() { Sequence sout; // assume this contains a result outputSequence.setValue(outputSequence); // store the result if (!EzPlug.isHeadless) { addSequence(sout); } }
Note that when submitting your plugin online, you will have to list both Blocks and EzPlug as dependencies, even though your plugin’s main does not extend EzPlug.
Handling user interruption
It may happen quite often that you (or any user) wish to interrupt a running protocol (wrong parameters, taking too long to do the job, etc.). Interrupting the execution is possible at two levels: at the protocol level, and at the block level:
- Interrupting a protocol: this is the easiest scenario (nothing to do on the developer side). When a user clicks on the Stop button (on top of the Protocols interface), Protocols waits for the currently running block to finish its job and will stop there. This is not necessarily the most convenient use-case, since the number one reason to stop a process is because a block itself is taking forever to finish the job.
- Interrupting a block: this is where you kick in. The entire workflow (i.e. your block) runs in a specific thread. When the previous Stop button is clicked, Protocols actually doesn’t just wait for the current Block to end its process, it also interrupts your main block thread by calling Thread.interrupt() on it. Note that this will not automatically kill your process (remember that proper Java practice dictates that threads shouldn’t be killed “brutally”, as this could leave the memory in a weird state). Instead, it changes the thread state to interrupted, and you can easily check this state using Thread.currentThread.isInterrupted() in your main code. All you have to do is check the value of this method every now and then throughout your process (at the beginning/end of a recursion for instance) and exit your process smoothly before exiting (i.e. closing file streams & network connections, stop other custom threads you started yourself, etc.).