Docs
Search…
ImageJ Example
Create a module that based on ImageJ/Fiji and its internal macro language.

Introduction

What is different?

When running as a module ImageJ/Fiji will not have access to a graphical output. Ideally when designing your module you make sure that all user interactions or graphical outputs are prevented. This is easily done when using Python, Javascript or similar scripting approaches.
Using ImageJ's built-in macro language you can run ImageJ and the corresponding macro script from the command line using the --headless parameter. This will work nicely most scripts however not all UI interactions or window displays can be prevented reliably.
Nevertheless you can use script with ImageJs macro language by initializing a virtual screen in your module utilizing Xvfb. You still need to make sure that the required input is performed by your script logic though and all windows are handled correclty.
There are a number of ressources on the web explaining in more detail about the "headless" mode and using Xvfb to run ImageJ/Fiji on clusters.
For more details on Xvfb please refer to the ressources on the web such as: (Xvfb Wikipedia) During the tutorial you will see an example of how to set it up in your module.
For a complete overview and capabilities of the ImageJ Macro language please refer to the ImageJ documentation and tutorials:

ImageJ Example Module

Writing the module

As an example we will discuss a simple module that takes all output images of the previous one in a workflow and generates a tiff stack. This macro will use ImageJ's "Import Sequence...". This module is also available on the platform (see Image Stack Generator)
Below you see an overview of the files we will generate for this tutorial. All these files except the Dockerfile and the json will be utilitzed within the Dockercontainer.
1
├── CallLog.class (binaries of the support script CallLog.java)
2
├── CallLog.java (a support script to dump log text to the command line)
3
├── Dockerfile (the file neccessary to generate the Docker container)
4
├── generate_stack.ijm (the actual ImageJ/Fiji Macro)
5
├── JSON_Read.js (a support script to read in JSON files)
6
├── module_specification.json (module input/output and GUI specification for the WFE)
7
└── start.sh (shell script to run ImageJ/Fiji from the command line)
Copied!
During execution, the Workflow Engine (or WFE) running the module provides the inputs in a json string in the environment variable in two ways:
  • as environment varible WFE_INPUT_JSON.
  • as json file in the path /params/WFE_INPUT_JSON.json
In this tutorial we will be accessing the JSON file in the /params/ directory. Most of the input parameters are derived from your module specification file we will discuss below. An example for a WFE_INPUT_JSON string would look something like this at runtime:
1
{
2
"input_files": "/input/some_path/some_image.tif",
3
"parameter_text": "some text",
4
"parameter_number": 250,
5
"WFE_output_params_file" : "/output/path/to/output.json"
6
}
Copied!
With the ImageJ macro language you are not able to access a json conveniently compared to other languages such as Python, Javascript, etc. You could devise a different solution to access the json directly and write your own way to get the values directly in the macro. For this tutorial however we are using a seperate small Javascript that will parse the json for us and return the values.
JSON_Read.js script contains one short function readJSON the whole script works as follow:
  • First we import the ImageJ class.
  • next we define the readJSON() function the call argument will be the json key we are calling to get the associated value
  • define a jsonDir variable that contains the path to the WFE json file
  • the variable "settings" will contain the contents of the json file, here we are using the Javscript JSON parsing and ImageJ file opening
  • next line will querry the json to get the value we want and return it
  • last the getArgument() function will catch the paramter we supply when calling this script from our main ImageJ Macro
JSON_Read.js
1
/*
2
* JSON read JSON and return the supplied key
3
* Robert Kirmse
4
*/
5
6
importClass(Packages.ij.IJ);
7
8
function readJSON(call) {
9
10
//Path to the WFE_input_params.json
11
var jsonDir = "/params/WFE_input_params.json";
12
var settings = JSON.parse(IJ.openAsString(jsonDir));
13
// var allPropertyNames = Object.keys(settings); // put JSON keys in new Array
14
// var count = Object.keys(settings).length; // count number of JSON keys
15
return (eval(call));
16
}
17
18
var arg = getArgument();
19
readJSON(arg);
Copied!
Using the Javascript to get the JSON values in your ImageJ/Fiji macro works like so:
1
JSON_READER =/JSON_Read.js”;
2
WFEOUTPUT = runMacro(JSON_READER, “settings.WFE_output_params_file”);
Copied!
  • load the javascript into JSON_READER Variable
  • Define a variabel of your choice and run the script code inside your macro by calling setting.[the json key you want to querry] in this case the key WFE_output_params_file.
  • The variable WFEOUTPUT now contains the value i.e. tha path where you should store the output json file for your module.
We are using ImageJ in a virtual screen environment print commands etc. will go to the UI's log window. For sending logging text to the command line of the Docker container we are using a very simple Java program CallLog.java we can call in a similar fashion like the Javascript above.
In the zip archive containing the project files you find the corresponding ready to use CallLog.class if you want to make changes you need to generate the class file yourself from the CallLog.java file.
CallLog.java
1
import java.io.PrintStream;
2
3
public class CallLog { public CallLog() {}
4
public static String shout(String paramString) { System.out.println("[LOG]: " + paramString);
5
return null;
6
}
7
}
Copied!
The idea is very simple we have a small program that we can call with a parameter from our ImageJ macro. The parameter we supply will then be printed using the standard system.out of Java.
You could also combine the functionality to access the json contents and the Java program for the log output into one that could do both.
Next up lets look into the ImageJ Macro code generate_stack.ijm. We will go through it block by block, you can either copy paste it from each section into one complete *.ijm file or copy and paste the code snippet given below.
Step by step (doing it right)
Copy & paste (quick and dirty)
Macro Part I
1
/*
2
ImageJ Script to open sequences individual images
3
and safe these as a corresponding TIFF-Stack
4
using virtual stacks to conserver RAM
5
so in theory even large number if images files --> stacks should work
6
*/
7
8
// Global Variables
9
RESULTSPATH = "/output/";
10
BATCHMODE = "true";
11
12
// Read WFE JSON Values
13
14
call("CallLog.shout", "calllog Trying to read WFE_JSON");
15
16
WFE_file = "/params/WFE_input_params.json";
17
if (!File.exists(WFE_file)) {
18
call("CallLog.shout", "WFE_input_params.json does not exist... exiting...");
19
eval("script", "System.exit(0);");
20
}
21
else {
22
call("CallLog.shout", "WFE_input_params.json found... reading file...");
23
WFE_JSON = File.openAsString(WFE_file);
24
}
25
26
call("CallLog.shout", "WFE_JSON contents: " + WFE_JSON);
27
28
// Read JSON WFE Parameters
29
JSON_READER = "/JSON_Read.js";
30
if (!File.exists(JSON_READER)) {
31
call("CallLog.shout", "JSON_Read.js does not exist... exiting...");
32
eval("script", "System.exit(0);");
33
}
34
else {
35
call("CallLog.shout", "JSON_Read.js found... reading file...");
36
}
37
38
call("CallLog.shout", "Reading JSON Parameters");
39
40
// Script Parameters collected from WFE_JSON
41
INPUTFILES = runMacro(JSON_READER, "settings.input_files[0]");
42
PREFIX = runMacro(JSON_READER, "settings.prefix");
43
STACKNAME = runMacro(JSON_READER, "settings.name");
44
WFEOUTPUT = runMacro(JSON_READER, "settings.WFE_output_params_file");
45
46
// Getting input file path from WFE input_files
47
path_substring = lastIndexOf(INPUTFILES, "/");
48
IMAGEDIR_WFE = substring(INPUTFILES, 0, path_substring+1);
49
50
main();
Copied!
For better clarity all variable defined or read from the JSON in this section are all uppercase indicating that they are global and useable from all functions below.
In the beginning of the ImageJ macro we start of with setting some global variables.
  • We set the output path. By convention this is /output/
  • We also define a BATCHMODE variable to conveniently trigger ImageJ to batchmode (i.e. this prevents most UI outputs)
  • next we "call" the Java program for the first time to send some log text to the command line (I will be using loggin a lot, however you can decide how often and what information you want to send to the commandline yourself. This text will also be visible later in the logfiles on the resultpage)
  • the first "if block" sets the path to the WFE parameter JSON file which is by convention always at /params/WFE_input_params.json inside of the if block we make sure if the files exists and stop the macro of we can't access the file
  • if the file is successfully found we use the logging again to print the complete JSON file to the commandline for later checking in the logs
  • next we continue reading out the JSON parameters for neccessary for our module. These parameters are defined by you in the module specifiaction. We cover the specification file later. For now we use the same mechanism as above. First we set the path to our Javascript for reading the JSON file. The second "if" block tests if the file is present. If the file is successfully found we continue with accessing the paramters
  • Accessing the parameter is done by defining a corresponing variabel, i.e. INPUTFILESand then using the ImageJ macro function to execute a script i.e. the runMacro command. In our case we use runMacro(JSON_READER, "settings.input_files[0]"); i.e. executing the JSON_Read.js and using the argument _settings.input_files[0]. Remember we stored the _WFE_input_params.json in the settings variable so all keys can now be accessed by the "." syntax.
  • Next we read out the rest of the parameters we need.
  • Before we call our main() macro function we extract the path of the input files from the file list. We need to do this because we want to access the folder where all input files are stored. During the execution of a workflow the WFE puts all the files into a specific folder for the particular step. By reading the first file of the input list (i.e settings.input_files[0]) we have the path and using two ImageJ string functions (lastIndexOf and substring) we extract the input path.
  • last we start the main macro.
Macro Part 2
1
function main() {
2
3
call("CallLog.shout", "Starting opening files, time: " + currentTime());
4
5
if (BATCHMODE=="true") {
6
setBatchMode(true);
7
}
8
9
importData();
10
savingStack();
11
jsonOut();
12
13
call("CallLog.shout", "DONE! " + currentTime());
14
run("Close All");
15
call("CallLog.shout", "Closed");
16
eval("script", "System.exit(0);");
17
}
Copied!
Part 2 of the macro code is pretty short here we define the main() function that can call the individual parts of the macro.
  • We start again with some log output the function currentTime is defined later but gives you the system time for the log. Batchmode is set to true in the if-statement
  • we then call three different parts of the macro defined as individual functions.
  • end the end we again output some information for the log
  • _run("Close All"); is an ImageJ macro function that closes all remaining open windows
  • the final line eval("script", "System.exit(0);"); forces ImageJ to quit. This method give better results then the equivalent run("Quit");. Using _run("Quit"); in the virtual framebuffer environment produces a Java error.
For this macro we could have just written the program in one go without using dividing the macro into different functions. However using the functions might be helpful for you if you want to build on this macro and replacing or adding your own functions.
Macro Part 3
1
function importData() {
2
call("CallLog.shout", "Importing Data");
3
4
if (PREFIX == "no-filter") {
5
call("CallLog.shout", "opening sequence in: "+ IMAGEDIR_WFE + " with no filter");
6
run("Image Sequence...", "open=" +IMAGEDIR_WFE +" sort use");
7
}
8
else {
9
call("CallLog.shout", "opening sequence in: "+ IMAGEDIR_WFE + " with filter: " + PREFIX);
10
run("Image Sequence...", "open=" +IMAGEDIR_WFE +" file="+ PREFIX +" sort use");
11
}
12
}
13
14
function savingStack() {
15
16
if (STACKNAME=="output") {
17
call("CallLog.shout", "writing tif stack with default name: output.tif");
18
saveAs("Tiff", "/output/output.tif");
19
}
20
else {
21
call("CallLog.shout", "writing tif stack with user name: " + STACKNAME + ".tif");
22
saveAs("Tiff", "/output/" + STACKNAME + ".tif");
23
}
24
}
Copied!
Part 3 of the macro consists of the two functions importData() and savingStack(). Both are simple functions called from the main() part of the macro.
  • in the function importData() we output some log text, then use an if/else block to use one of the json parameters from the module specification to decide whether we supply the Image Sequence with some additional parameters or not
  • finally we call the built-in ImageJ macro function run("Image Sequence...") to open all files in the input directory
  • the second function savingStack() then uses the same logic to save the resulting image stack as tiff.
  • if the user supplied a file name the stack is written using the user input, otherwise a default name is selected.
Macro Part 4
1
function jsonOut() {
2
call("CallLog.shout", "Starting JSON Output");
3
jsonout = File.open(RESULTSPATH + "json_out.txt");
4
call("CallLog.shout", "File open: JSON Output");
5
6
print(jsonout,"{");
7
print(jsonout,"\"RESULTSDATA\": [");
8
9
if (STACKNAME=="output") {
10
print(jsonout,"\t\"/output/output.tif\"");
11
}
12
else {
13
print(jsonout,"\t\"/output/"+ STACKNAME + ".tif\"");
14
}
15
print(jsonout,"\t]");
16
print(jsonout,"}");
17
File.close(jsonout);
18
File.rename(RESULTSPATH + "json_out.txt", RESULTSPATH + WFEOUTPUT);
19
20
call("CallLog.shout", "Done with JSON Output");
21
}
22
23
/*
24
* Below here are functions for support tasks
25
*/
26
// SystemTime from https://imagej.nih.gov/ij/macros/GetDateAndTime.txt
27
function currentTime() {
28
MonthNames = newArray("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");
29
DayNames = newArray("Sun", "Mon","Tue","Wed","Thu","Fri","Sat");
30
31
getDateAndTime(year, month, dayOfWeek, dayOfMonth, hour, minute, second, msec);
32
33
timeString = DayNames[dayOfWeek]+" ";
34
35
if (dayOfMonth<10) {timeString = timeString + "0";}
36
timeString = timeString+dayOfMonth+"-"+MonthNames[month]+"-"+year+" @ ";
37
38
if (hour<10) {timeString = timeString + "0";}
39
timeString = timeString+hour+":";
40
41
if (minute<10) {timeString = timeString + "0";}
42
timeString = timeString+minute+":";
43
44
if (second<10) {timeString = timeString + "0";}
45
timeString = timeString+second;
46
47
return timeString;
48
}
Copied!
Part 4 of the macro contains two final functions
  • the jsonOut() function is containing the block of statements that writes the required output specifications of your module
  • we start with the basic ImageJ macro functions to open an intermediate json_out.txt file and fill it with the required output specifications in JSON format since ImageJ macros cannot directly output json we print the syntax manually
  • Here we just need to output the information that our module generates one file. the final JSON will look like this:
1
{
2
"RESULTSDATA": [
3
"/output/test1.tif"
4
]
5
}
Copied!
  • If your future modules output more data and or files lists, you have to adapt the output json accordingly so that the WFE knows which parameters are produces and can be passed along to the next module.
  • Finally we close the intermediate json_out.txt file and rename it according the the WFE specification. Remember the variable WFEOUTPUT contains the parameter for this file as dictated by the WFE_output_params_file key from the WFE json file we read out in the beginning of the macro.
  • The last function currentTime() is not strictly necessary and was taken from ImageJ Macro repository it is used to translate the system time into user readable format and use it during the log outputs. It is not strictly necessary and just used for convenience.
generate_stack.ijm
1
/*
2
ImageJ Script to open sequences individual images and safe these as a corresponding TIFF-Stack
3
using virtual stacks to conserver RAM so in theory even large number if images files --> stacks should work
4
*/
5
6
// General global vars
7
RESULTSPATH = "/output/";
8
BATCHMODE = "true";
9
10
// Read JSON Variables
11
call("CallLog.shout", "calllog Trying to read WFE_JSON");
12
13
WFE_file = "/params/WFE_input_params.json";
14
if (!File.exists(WFE_file)) {
15
call("CallLog.shout", "WFE_input_params.json does not exist... exiting...");
16
eval("script", "System.exit(0);");
17
}
18
else {
19
call("CallLog.shout", "WFE_input_params.json found... reading file...");
20
WFE_JSON = File.openAsString(WFE_file);
21
}
22
23
call("CallLog.shout", "WFE_JSON contents: " + WFE_JSON);
24
25
// Read JSON WFE Parameters
26
JSON_READER = "/JSON_Read.js";
27
28
if (!File.exists(JSON_READER)) {
29
call("CallLog.shout", "JSON_Read.js does not exist... exiting...");
30
eval("script", "System.exit(0);");
31
}
32
else {
33
call("CallLog.shout", "JSON_Read.js found... reading file...");
34
}
35
36
call("CallLog.shout", "Reading JSON Parameters");
37
38
// Get WFE Json values as global vars
39
INPUTFILES = runMacro(JSON_READER, "settings.input_files[0]");
40
PREFIX = runMacro(JSON_READER, "settings.prefix");
41
STACKNAME = runMacro(JSON_READER, "settings.name");
42
WFEOUTPUT = runMacro(JSON_READER, "settings.WFE_output_params_file");
43
44
// Getting input file path from WFE input_files
45
path_substring = lastIndexOf(INPUTFILES, "/");
46
IMAGEDIR_WFE = substring(INPUTFILES, 0, path_substring+1);
47
48
main();
49
50
function main() {
51
call("CallLog.shout", "Starting opening files, time: " + currentTime());
52
53
if (BATCHMODE=="true") {
54
setBatchMode(true);
55
}
56
57
importData();
58
savingStack();
59
jsonOut();
60
61
call("CallLog.shout", "DONE! " + currentTime());
62
run("Close All");
63
call("CallLog.shout", "Closed");
64
eval("script", "System.exit(0);");
65
}
66
67
function importData() {
68
call("CallLog.shout", "Importing Data");
69
70
if (PREFIX == "no-filter") {
71
call("CallLog.shout", "opening sequence in: "+ IMAGEDIR_WFE + " with no filter");
72
run("Image Sequence...", "open=" +IMAGEDIR_WFE +" sort use");
73
}
74
else {
75
call("CallLog.shout", "opening sequence in: "+ IMAGEDIR_WFE + " with filter: " + PREFIX);
76
run("Image Sequence...", "open=" +IMAGEDIR_WFE +" file="+ PREFIX +" sort use");
77
}
78
}
79
80
function savingStack() {
81
if (STACKNAME=="output") {
82
call("CallLog.shout", "writing tif stack with default name: output.tif");
83
saveAs("Tiff", "/output/output.tif");
84
}
85
else {
86
call("CallLog.shout", "writing tif stack with user name: " + STACKNAME + ".tif");
87
saveAs("Tiff", "/output/" + STACKNAME + ".tif");
88
}
89
}
90
91
// Generate output.json for WFE
92
function jsonOut() {
93
call("CallLog.shout", "Starting JSON Output");
94
jsonout = File.open(RESULTSPATH + "json_out.txt");
95
call("CallLog.shout", "File open: JSON Output");
96
97
print(jsonout,"{");
98
print(jsonout,"\"RESULTSDATA\": [");
99
100
if (STACKNAME=="output") {
101
print(jsonout,"\t\"/output/output.tif\"");
102
}
103
else {
104
print(jsonout,"\t\"/output/"+ STACKNAME + ".tif\"");
105
}
106
print(jsonout,"\t]");
107
print(jsonout,"}");
108
File.close(jsonout);
109
File.rename(RESULTSPATH + "json_out.txt", RESULTSPATH + WFEOUTPUT);
110
111
call("CallLog.shout", "Done with JSON Output");
112
}
113
114
/*
115
* functions for support tasks
116
*/
117
// Get SystemTimer
118
function currentTime() {
119
MonthNames = newArray("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");
120
DayNames = newArray("Sun", "Mon","Tue","Wed","Thu","Fri","Sat");
121
122
getDateAndTime(year, month, dayOfWeek, dayOfMonth, hour, minute, second, msec);
123
124
timeString = DayNames[dayOfWeek]+" ";
125
126
if (dayOfMonth<10) {timeString = timeString + "0";}
127
timeString = timeString+dayOfMonth+"-"+MonthNames[month]+"-"+year+" @ ";
128
129
if (hour<10) {timeString = timeString + "0";}
130
timeString = timeString+hour+":";
131
132
if (minute<10) {timeString = timeString + "0";}
133
timeString = timeString+minute+":";
134
135
if (second<10) {timeString = timeString + "0";}
136
timeString = timeString+second;
137
138
return timeString;
139
}
Copied!
Each module on the platform is packaged into what is called a "Docker Container". Introducing Docker would far exceed this tutorial so if you don't know what Docker is we recommend that you start with reading up some resources online to get familiar with the technology.
Below you see the Dockerfile for this ImageJ module. In this file we specify what the container for our module should include.
Dockerfile
1
# czsip/fiji with additional xvfb support
2
# Author: Robert Kirmse
3
# Version: 0.1
4
5
# Pull base CZSIP/Fiji.
6
FROM czsip/fiji_linux64_baseimage:latest
7
# FROM czsip/fiji
8
9
#get additional stuff
10
RUN apt-get update
11
RUN apt-get install -y apt-utils software-properties-common
12
RUN apt-get upgrade -y
13
14
# get Xvfb virtual X server and configure
15
RUN apt-get install -y xvfb x11vnc x11-xkb-utils xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic x11-apps
16
RUN apt-get install -y libxrender1 libxtst6 libxi6
17
18
# Install additional Fiji Plugins
19
COPY ./CallLog.class /Fiji.app/plugins
20
COPY ./generate_stack.ijm /
21
COPY ./JSON_Read.js /
22
COPY ./start.sh /
23
COPY ./font.conf /etc/fonts/fonts.conf
24
25
VOLUME [ "/input", "/output", "/params" ]
26
27
# Setting ENV for Xvfb and Fiji
28
ENV DISPLAY :99
29
ENV PATH $PATH:/Fiji.app/
30
31
# Entrypoint for Fiji script has to be added below!
32
ENTRYPOINT ["sh","/start.sh"]
Copied!
  • First we pull the container image we want to build our module on from the main Docker website (i.e. the Docker Hub) this is accomplish by using the FROM command and specifying the container image. Here we are using a container we already build based on a Linux image, adding Java and ImageJ
  • next we start a couple of RUNcommands that will execute linux command in the Docker container to add additional software packages we need for the Xvfb environment
  • Using COPYwe then add all additional files we need into the Docker container here these are all files discussed during the tutorial. The logic similar to the standard cpcommand where you need to supply the local file and the location where it should be copied to. Here, you can also add your plugins, e.g. to use the additional ImageJ plugin "Extended Depth of Field", you need to download the plugin package from the home page of the plugin, unzip it, place the folder in the module folder and add this line to the Dockerfile: COPY ./EDF /Fiji.app/plugins/EDF.
  • with the VOLUME command we specify the additional directories that are going to be mounted later by the for exposing files to and from the container. This is WFE convention and will always be the same
  • Next we run some additional commands for setting up the Xvfb virtual screen port
  • Finally we need to define the ENTRYPOINT for the Docker container. The entrypoint is the command executed during the start of the container. Here we execute a shell script once the container starts
As discussed above when a container is executed it will run the command supplied by the ENTRYPOINT parameter. On our example we execute a shell script start.sh that contains the following command below. In essence we start ImageJ with the argument to run our macro script In addition we start the virtual frame environment so that ImageJ can output potential graphical elements into the virtual screen.
start.sh
1
#!/bin/bash
2
xvfb-run -a /Fiji.app/ImageJ-linux64 --ij2 –mem=1024M -macro generate_stack.ijm
Copied!
The last file you need before you can upload and generate a new module on platform is the module_specification.json file. The filename is WFE convention. Without this file the platform will not generate you module. Below is the JSON for this module:
module_specification.json
1
{
2
"spec": {
3
"inputs": {
4
"input_files": {
5
"type:list[file]": {}
6
},
7
"prefix": {
8
"type:string": {},
9
"default": "no-filter"
10
},
11
"name": {
12
"type:string": {},
13
"default": "output"
14
}
15
},
16
"outputs": {
17
"RESULTSDATA": {
18
"type:list[file]": {}
19
}
20
}
21
},
22
"ui": {
23
"inputs": {
24
"prefix": {
25
"index": 1,
26
"label": "Filter files (e.g .png)",
27
"widget:textbox": {}
28
},
29
"name": {
30
"index": 2,
31
"label": "Filename (default output.tif)",
32
"widget:textbox": {}
33
}
34
}
35
},
36
"image": "camodules.azurecr.io/stackgen:latest"
37
}
Copied!
  • the module_specification.json consists of two part the spec block and the ui block. You will recognize that the parameters in the spec part are the same we saw in the initial workflow engine file (WFE_INPUT_JSON.json) at the beginning of this tutorial. The reason is that the WFE takes your module specification. If a workflow runs this information is used to send the input parameters you specified to your module so it is up to you which parameters your module accepts, how they are called and which types they are.
  • spec: Here you define the input parameters that your module accepts and also generates. You need to define the key:values according to the json syntax.
    • for example for this module we defined input_files and give it the type list[files]. This means the module can accept a list of filenames and the WFE will send the path to all files generated by the previous module with the variable.
    • next we defined the key prefix as a string and also supply a default parameter. If no other information is send during workflow execution the prefix will contain the string "no-filter".
    • you can define as many inputs as you like After you defined all inputs you also need to tell the WFE what type of outputs your module generates. Here we have only one output i.e. the Tiff stack the module produces So here outputs only contains one key RESULTSDATA, as type we again use list[file]
  • ui: This blocks defines the UI of your module display on the platform. If you don't want or need any options to change parameters manually by the user you can leave this block empty. However here we want the user to be able to select a certain filter string to e.g. only open png files generated by the previous module and also choose a custom name for the output stack, so we define two UI elements for the parameters above like so:
    • our first element in the UI is the filter string, we need to use the same name for the key we define under spec (i.e. prefix). Since it is the first element in the UI the index is 0. Next we define the label. This is the text displayed in the UI for this element. With widget: we define the UI element for the user input. In this case a simple textbox.
    • our second element follows the first to again supply a textbox for entering a file name. Please note that this is index 1 now
    • You can use other elements such as radio buttons, sliders and drop downs. Please refer to the Module Specification.
  • finally you supply the image name of your module
At this point We finished all files necessary for the module and you could upload these now to the platform the generate a new module, please refer to the Git section to learn how to upload your code to the platform.

Testing the module

It is highly recommended use a local Docker installation to test and debug your module before uploading. Especially for more extensive modules you will save yourself a lot of time if you can do local testing before running your code on the platform
If you are developing modules for the platform it is a good idea to install a local Docker environment. Docker is available for all major OS. For this tutorial we assume that you already have a local Docker installation and some basic knowledge how to use it. Please refer to online resource for in depth Docker tutorials.
With a working local Docker installation you just need a few extra preparations to build and run the container we prepared during the tutorial. Below are some suggestions assuming a Windows operating system.
First you should create some testing folder for convenience we basis this directly on C: and create:
  • C:\Test\input\module_1_1\
  • C:\Test\output\
  • C:\Test\params\
  • We then need to fill the folders with our test files. For the stack generator module we ideally need a number of image file greater then two. Copy these to C:\Test\input\module_1_1\
  • We need a mock WFE_INPUT_JSON.json file similar to what the WFE on the platform would send. Copy this file to C:\Test\params\, like so:
1
{
2
"WFE_output_params_file": "wfe_module_params_2_2.json",
3
"input_files": [
4
"/input/module_1_1/SomeImage_01.tif",
5
"/input/module_1_1/SomeImage_02.tif",
6
"/input/module_1_1/SomeImage_03.tif",
7
"/input/module_1_1/SomeImage_04.tif",
8
"/input/module_1_1/SomeImage_05.tif",
9
"/input/module_1_1/SomeImage_06.tif"
10
],
11
"prefix": ".tif",
12
"name": "test1"
13
}
Copied!
You are set and can now build and run the container, simulating the WFE environment.
To build your module container locally navigate to the folder containing all your files and run
1
docker build --rm -t camodules.azurecr.io/stack_generator:latest .
Copied!
This commands tells Docker to execute a build
  • --rm: will remove intermediary containers after a successful build
  • -t: specifies the name of your container
  • .: tells docker to look for a file name Dockerfile and use it to build the container accordingly.
If you are running the build for the first time this may take a while because Docker needs to download the base container as well as executing all commands you specified in your Dockerfile. Once the build completed you can use the following command to display all local available docker containers.
1
docker images
Copied!
With the build completed you now have the container available and can start it locally to see if everything is working as intended. For running the container use the following command:
1
docker run -v C:\Test\input:/input -v C:\Test\output:/output -v C:\Test\params:/params camodules.azurecr.io/stack_generator:latest
Copied!
  • -v: this parameter tells docker to use a local folder and map it to a path inside the docker container. Here we are making three
    local folders available inside of the docker container as \input, \output, and \params just like you would expect on the platform.
    If all works out you should see the container starting and log output appearing on the command line. After the container completes
    the results can be found in C:\Test\output in this example.
You can also store all local files in one folder and map this location to the three different paths inside of the docker
You might need to enable local file system access for the Docker client so it has permission to access the local folders (e.g Windows you need to open the "Setting > Sharing" in the Docker client and share the respective drive)

Fast lane for impatient coders ;)

Of course we have prepared the zipped project folder and some ready to run files for copy and paste:
CallLog.java
Dockerfile
font.conf
generate_stack.ijm
JSON_Read.js
module_specification.json
start.sh
1
import java.io.PrintStream;
2
3
public class CallLog { public CallLog() {}
4
public static String shout(String paramString) { System.out.println("[LOG]: " + paramString);
5
return null;
6
}
7
}
Copied!
1
# czsip/fiji with additional xvfb support
2
# Author: Robert Kirmse
3
# Version: 0.1
4
5
6
# Pull base CZSIP/Fiji.
7
# FROM czsip/fiji_linux64_baseimage:latest
8
FROM czsip/fiji
9
10
#get additional stuff
11
RUN apt-get update
12
RUN apt-get install -y apt-utils software-properties-common
13
RUN apt-get upgrade -y
14
15
16
# get Xvfb virtual X server and configure
17
RUN apt-get install -y xvfb x11vnc x11-xkb-utils xfonts-100dpi xfonts-75dpi xfonts-scalable xfonts-cyrillic x11-apps
18
RUN apt-get install -y libxrender1 libxtst6 libxi6
19
20
# Install additional Fiji Plugins
21
COPY ./CallLog.class /Fiji.app/plugins
22
COPY ./generate_stack.ijm /
23
COPY ./JSON_Read.js /
24
COPY ./start.sh /
25
COPY ./font.conf /etc/fonts/fonts.conf
26
27
28
VOLUME [ "/input", "/output", "/params" ]
29
30
# Setting ENV for Xvfb and Fiji
31
ENV DISPLAY :99
32
ENV PATH $PATH:/Fiji.app/
33
34
# Entrypoint for Fiji script has to be added below!
35
ENTRYPOINT ["sh","/start.sh"]
36
Copied!
1
<?xml version="1.0"?>
2
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
3
<!-- /etc/fonts/fonts.conf file to configure system font access -->
4
<fontconfig>
5
6
<!--
7
DO NOT EDIT THIS FILE.
8
IT WILL BE REPLACED WHEN FONTCONFIG IS UPDATED.
9
LOCAL CHANGES BELONG IN 'local.conf'.
10
11
The intent of this standard configuration file is to be adequate for
12
most environments. If you have a reasonably normal environment and
13
have found problems with this configuration, they are probably
14
things that others will also want fixed. Please submit any
15
problems to the fontconfig bugzilla system located at fontconfig.org
16
17
Note that the normal 'make install' procedure for fontconfig is to
18
replace any existing fonts.conf file with the new version. Place
19
any local customizations in local.conf which this file references.
20
21
Keith Packard
22
-->
23
24
<!-- Font directory list -->
25
26
<dir>/usr/share/fonts</dir>
27
<dir>/usr/X11R6/lib/X11/fonts</dir>
28
<dir>/usr/local/share/fonts</dir>
29
<dir prefix="xdg">fonts</dir>
30
<!-- the following element will be removed in the future -->
31
<dir>~/.fonts</dir>
32
33
<!--
34
Accept deprecated 'mono' alias, replacing it with 'monospace'
35
-->
36
<match target="pattern">
37
<test qual="any" name="family">
38
<string>mono</string>
39
</test>
40
<edit name="family" mode="assign" binding="same">
41
<string>monospace</string>
42
</edit>
43
</match>
44
45
<!--
46
Accept alternate 'sans serif' spelling, replacing it with 'sans-serif'
47
-->
48
<match target="pattern">
49
<test qual="any" name="family">
50
<string>sans serif</string>
51
</test>
52
<edit name="family" mode="assign" binding="same">
53
<string>sans-serif</string>
54
</edit>
55
</match>
56
57
<!--
58
Accept deprecated 'sans' alias, replacing it with 'sans-serif'
59
-->
60
<match target="pattern">
61
<test qual="any" name="family">
62
<string>sans</string>
63
</test>
64
<edit name="family" mode="assign" binding="same">
65
<string>sans-serif</string>
66
</edit>
67
</match>
68
69
<!--
70
Load local system customization file
71
-->
72
<include ignore_missing="yes">conf.d</include>
73
74
<!-- Font cache directory list -->
75
76
<cachedir>/var/cache/fontconfig</cachedir>
77
<cachedir prefix="xdg">fontconfig</cachedir>
78
<!-- the following element will be removed in the future -->
79
<cachedir>~/.fontconfig</cachedir>
80
81
<config>
82
<!--
83
These are the default Unicode chars that are expected to be blank
84
in fonts. All other blank chars are assumed to be broken and
85
won't appear in the resulting charsets
86
-->
87
<int>0x0020</int>
88
<!-- SPACE -->
89
<int>0x00A0</int>
90
<!-- NO-BREAK SPACE -->
91
<int>0x00AD</int>
92
<!-- SOFT HYPHEN -->
93
<int>0x034F</int>
94
<!-- COMBINING GRAPHEME JOINER -->
95
<int>0x0600</int>
96
<!-- ARABIC NUMBER SIGN -->
97
<int>0x0601</int>
98
<!-- ARABIC SIGN SANAH -->
99
<int>0x0602</int>
100
<!-- ARABIC FOOTNOTE MARKER -->
101
<int>0x0603</int>
102
<!-- ARABIC SIGN SAFHA -->
103
<int>0x06DD</int>
104
<!-- ARABIC END OF AYAH -->
105
<int>0x070F</int>
106
<!-- SYRIAC ABBREVIATION MARK -->
107
<int>0x115F</int>
108
<!-- HANGUL CHOSEONG FILLER -->
109
<int>0x1160</int>
110
<!-- HANGUL JUNGSEONG FILLER -->
111
<int>0x1680</int>
112
<!-- OGHAM SPACE MARK -->
113
<int>0x17B4</int>
114
<!-- KHMER VOWEL INHERENT AQ -->
115
<int>0x17B5</int>
116
<!-- KHMER VOWEL INHERENT AA -->
117
<int>0x180E</int>
118
<!-- MONGOLIAN VOWEL SEPARATOR -->
119
<int>0x2000</int>
120
<!-- EN QUAD -->
121
<int>0x2001</int>
122
<!-- EM QUAD -->
123
<int>0x2002</int>
124
<!-- EN SPACE -->
125
<int>0x2003</int>
126
<!-- EM SPACE -->
127
<int>0x2004</int>
128
<!-- THREE-PER-EM SPACE -->
129
<int>0x2005</int>
130
<!-- FOUR-PER-EM SPACE -->
131
<int>0x2006</int>
132
<!-- SIX-PER-EM SPACE -->
133
<int>0x2007</int>
134
<!-- FIGURE SPACE -->
135
<int>0x2008</int>
136
<!-- PUNCTUATION SPACE -->
137
<int>0x2009</int>
138
<!-- THIN SPACE -->
139
<int>0x200A</int>
140
<!-- HAIR SPACE -->
141
<int>0x200B</int>
142
<!-- ZERO WIDTH SPACE -->
143
<int>0x200C</int>
144
<!-- ZERO WIDTH NON-JOINER -->
145
<int>0x200D</int>
146
<!-- ZERO WIDTH JOINER -->
147
<int>0x200E</int>
148
<!-- LEFT-TO-RIGHT MARK -->
149
<int>0x200F</int>
150
<!-- RIGHT-TO-LEFT MARK -->
151
<int>0x2028</int>
152
<!-- LINE SEPARATOR -->
153
<int>0x2029</int>
154
<!-- PARAGRAPH SEPARATOR -->
155
<int>0x202A</int>
156
<!-- LEFT-TO-RIGHT EMBEDDING -->
157
<int>0x202B</int>
158
<!-- RIGHT-TO-LEFT EMBEDDING -->
159
<int>0x202C</int>
160
<!-- POP DIRECTIONAL FORMATTING -->
161
<int>0x202D</int>
162
<!-- LEFT-TO-RIGHT OVERRIDE -->
163
<int>0x202E</int>
164
<!-- RIGHT-TO-LEFT OVERRIDE -->
165
<int>0x202F</int>
166
<!-- NARROW NO-BREAK SPACE -->
167
<int>0x205F</int>
168
<!-- MEDIUM MATHEMATICAL SPACE -->
169
<int>0x2060</int>
170
<!-- WORD JOINER -->
171
<int>0x2061</int>
172
<!-- FUNCTION APPLICATION -->
173
<int>0x2062</int>
174
<!-- INVISIBLE TIMES -->
175
<int>0x2063</int>
176
<!-- INVISIBLE SEPARATOR -->
177
<int>0x206A</int>
178
<!-- INHIBIT SYMMETRIC SWAPPING -->
179
<int>0x206B</int>
180
<!-- ACTIVATE SYMMETRIC SWAPPING -->
181
<int>0x206C</int>
182
<!-- INHIBIT ARABIC FORM SHAPING -->
183
<int>0x206D</int>
184
<!-- ACTIVATE ARABIC FORM SHAPING -->
185
<int>0x206E</int>
186
<!-- NATIONAL DIGIT SHAPES -->
187
<int>0x206F</int>
188
<!-- NOMINAL DIGIT SHAPES -->
189
<int>0x2800</int>
190
<!-- BRAILLE PATTERN BLANK -->
191
<int>0x3000</int>
192
<!-- IDEOGRAPHIC SPACE -->
193
<int>0x3164</int>
194
<!-- HANGUL FILLER -->
195
<int>0xFEFF</int>
196
<!-- ZERO WIDTH NO-BREAK SPACE -->
197
<int>0xFFA0</int>
198
<!-- HALFWIDTH HANGUL FILLER -->
199
<int>0xFFF9</int>
200
<!-- INTERLINEAR ANNOTATION ANCHOR -->
201
<int>0xFFFA</int>
202
<!-- INTERLINEAR ANNOTATION SEPARATOR -->
203
<int>0xFFFB</int>
204
<!-- INTERLINEAR ANNOTATION TERMINATOR -->
205
<!--
206
Rescan configuration every 30 seconds when FcFontSetList is called
207
-->
208
<rescan>
209
<int>30</int>
210
</rescan>
211
</config>
212
213
</fontconfig>
Copied!
1
/*
2
ImageJ Script to open sequences individual images and safe these as a corresponding TIFF-Stack
3
using virtual stacks to conserver RAM so in theory even large number if images files --> stacks should work
4
*/
5
6
// General global vars
7
RESULTSPATH = "/output/";
8
BATCHMODE = "true";
9
10
// Read JSON Variables
11
call("CallLog.shout", "calllog Trying to read WFE_JSON");
12
13
WFE_file = "/params/WFE_input_params.json";
14
if (!File.exists(WFE_file)) {
15
call("CallLog.shout", "WFE_input_params.json does not exist... exiting...");
16
eval("script", "System.exit(0);");
17
}
18
else {
19
call("CallLog.shout", "WFE_input_params.json found... reading file...");
20
WFE_JSON = File.openAsString(WFE_file);
21
}
22
23
call("CallLog.shout", "WFE_JSON contents: " + WFE_JSON);
24
25
// Read JSON WFE Parameters
26
JSON_READER = "/JSON_Read.js";
27
28
if (!File.exists(JSON_READER)) {
29
call("CallLog.shout", "JSON_Read.js does not exist... exiting...");
30
eval("script", "System.exit(0);");
31
}
32
else {
33
call("CallLog.shout", "JSON_Read.js found... reading file...");
34
}
35
36
call("CallLog.shout", "Reading JSON Parameters");
37
38
// Get WFE Json values as global vars
39
INPUTFILES = runMacro(JSON_READER, "settings.input_files[0]");
40
PREFIX = runMacro(JSON_READER, "settings.prefix");
41
STACKNAME = runMacro(JSON_READER, "settings.name");
42
WFEOUTPUT = runMacro(JSON_READER, "settings.WFE_output_params_file");
43
44
// Getting input file path from WFE input_files
45
path_substring = lastIndexOf(INPUTFILES, "/");
46
IMAGEDIR_WFE = substring(INPUTFILES, 0, path_substring+1);
47
48
main();
49
50
function main() {
51
call("CallLog.shout", "Starting opening files, time: " + currentTime());
52
53
if (BATCHMODE=="true") {
54
setBatchMode(true);
55
}
56
57
importData();
58
savingStack();
59
jsonOut();
60
61
call("CallLog.shout", "DONE! " + currentTime());
62
run("Close All");
63
call("CallLog.shout", "Closed");
64
eval("script", "System.exit(0);");
65
}
66
67
function importData() {
68
call("CallLog.shout", "Importing Data");
69
70
if (PREFIX == "no-filter") {
71
call("CallLog.shout", "opening sequence in: "+ IMAGEDIR_WFE + " with no filter");
72
run("Image Sequence...", "open=" +IMAGEDIR_WFE +" sort use");
73
}
74
else {
75
call("CallLog.shout", "opening sequence in: "+ IMAGEDIR_WFE + " with filter: " + PREFIX);
76
run("Image Sequence...", "open=" +IMAGEDIR_WFE +" file="+ PREFIX +" sort use");
77
}
78
}
79
80
function savingStack() {
81
if (STACKNAME=="output") {
82
call("CallLog.shout", "writing tif stack with default name: output.tif");
83
saveAs("Tiff", "/output/output.tif");
84
}
85
else {
86
call("CallLog.shout", "writing tif stack with user name: " + STACKNAME + ".tif");
87
saveAs("Tiff", "/output/" + STACKNAME + ".tif");
88
}
89
}
90
91
// Generate output.json for WFE
92
function jsonOut() {
93
call("CallLog.shout", "Starting JSON Output");
94
jsonout = File.open(RESULTSPATH + "json_out.txt");
95
call("CallLog.shout", "File open: JSON Output");
96
97
print(jsonout,"{");
98
print(jsonout,"\"RESULTSDATA\": [");
99
100
if (STACKNAME=="output") {
101
print(jsonout,"\t\"/output/output.tif\"");
102
}
103
else {
104
print(jsonout,"\t\"/output/"+ STACKNAME + ".tif\"");
105
}
106
print(jsonout,"\t]");
107
print(jsonout,"}");
108
File.close(jsonout);
109
File.rename(RESULTSPATH + "json_out.txt", RESULTSPATH + WFEOUTPUT);
110
111
call("CallLog.shout", "Done with JSON Output");
112
}
113
114
/*
115
* functions for support tasks
116
*/
117
// Get SystemTimer
118
function currentTime() {
119
MonthNames = newArray("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");
120
DayNames = newArray("Sun", "Mon","Tue","Wed","Thu","Fri","Sat");
121
122
getDateAndTime(year, month, dayOfWeek, dayOfMonth, hour, minute, second, msec);
123
124
timeString = DayNames[dayOfWeek]+" ";
125
126
if (dayOfMonth<10) {timeString = timeString + "0";}
127
timeString = timeString+dayOfMonth+"-"+MonthNames[month]+"-"+year+" @ ";
128
129
if (hour<10) {timeString = timeString + "0";}
130
timeString = timeString+hour+":";
131
132
if (minute<10) {timeString = timeString + "0";}
133
timeString = timeString+minute+":";
134
135
if (second<10) {timeString = timeString + "0";}
136
timeString = timeString+second;
137
138
return timeString;
139
}
Copied!
1
/*
2
* JSON read JSON and return the supplied key
3
* Robert Kirmse
4
*/
5
6
importClass(Packages.ij.IJ);
7
8
function readJSON(call) {
9
10
//Path to the WFE_input_params.json
11
var jsonDir = "/params/WFE_input_params.json";
12
var settings = JSON.parse(IJ.openAsString(jsonDir));
13
// var allPropertyNames = Object.keys(settings); // put JSON keys in new Array
14
// var count = Object.keys(settings).length; // count number of JSON keys