Contributors to this project include:
- Joel Rogers (tweaking ROI erosion and dilation)
- All of the happy (and patient) alpha/beta/gamma testers out there!
Protocols - the missing link between developers and users
Developing a plug-in (for Icy or other software) generally involves the following steps:
- Design a graphical interface to receive input data and/or user interaction,
- Write the main code
- Generate result(s) that are displayed, saved, or made available to other plug-ins for further use
While many developers (like me) enjoy taking care of all three steps, most of them (also including me) don't necessarily have the time to focus on steps 1 and 3 all the time, and rather need to jump to step 2 as fast as possible (raise your hand if you never had a deadline to meet...). Users, on the other hand, mostly focus on steps 1 and 3. Yes, they do care about user interaction (and will run away if your interface looks complicated). They also know (or figure out soon enough) that solving a given problem involves more than one plug-in.
To summarize, plug-ins must be user-friendly, do some good work, communicate with one another, and all of that should be simple and intuitive. With these concepts in mind, We designed 'Protocols', a user-friendly graphical interface that simplifies the life of both users and developers:
- Users get to create entire image processing protocols without any programming knowledge. Plug-ins are represented as graphical blocks with inputs and outputs that can be linked together, and the engine automatically handle the ordering, execution and data communication between the plug-ins.
- Developers get a very simple API to write (or adapt existing plug-ins into) blocks, such that you never have to worry about steps 1 and 3 anymore.
This page contains developer-oriented information on how to create Blocks for use in the Protocols environment. If you are a user and just needs general information on how to use the Protocols graphical interface, please check the Protocols page.
The Block interface
Similarly to the EzPlug library, Blocks aims at minimizing the coding effort by taking care of all the tedious aspects such as writing the graphical user interface and handling input/output synchronization. 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:
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);
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 explicitely 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.
// 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
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.).