pyimagej
pyimagej copied to clipboard
Make original ImageJ commands work as SciJava modules
[@ctrueden Edit] If we create a script wrapper for each original ImageJ command, they will be easier to use in SciJava-friendly contexts, such as napari-imagej's search bar, because the types of inputs and outputs will be properly declared. Once the script wrappers exist, we could also customize each command as needed, such as checking if headless and failing fast for commands that don't support it. The write-up below describes one possible path to automating the generation of these scripts, so that we can get an initial version of them all in existence as quickly as possible with a minimum of human error.
Goal: create a RecorderPlus
object in ImageJA that operates in parallel to the IJ1 Recorder
. This class would be a static singleton. This work can live on a branch in IJA as it's not meant for distribution, just a simple workflow to generate scripts.
This object should have a number of fields based on the strings that we are harvesting (e.g. a list of String
s corresponding to GenericDialog#getNextString()
).
We will need to modify GenericDialog
to populate our RecorderPlus
object since not all information is being passed to the IJ1 Recorder
.
Points of modification:
Each of these methods should be updated to populate a field of RecorderPlus
with the information that would be sent to the Recorder
.
Then in Recorder.saveCommand we would call into RecorderPlus to write a *.py
script based on its current state (and also noting the ImagePlus parameter if present). Then we will create a new repository to house these scripts (or add it to imagej-legacy
, or something..)
Finally, I think tapping into Recorder.resetCommandOptions to also clear RecorderPlus
would be sufficient.
Code skeleton:
public class RecorderPlus {
public static final RecorderPlus recorder = new RecorderPlus();
//TODO add some fields, with setters, based on GenericDialog.getNextXXXXX
//TODO add a reset method that clears the current fields
//TODO add a "writeScript" method that outputs a script based on RecorderPlus current state
}
Before working on this automation, I suggest wrapping a couple of commands manually, to make sure that the result delivers on our requirements. (What are our requirements? We should list them.)
@ctrueden and I worked on this more earlier today and made some good progress on this. Instead of hacking getNextString
, getNextNumber
and the rest of the getNext
methods we edited:
- addCheckbox
- addCheckboxGroup
- addChoice
- addDirectoryField
- addFileField
- addNumericField
- addRadioButtonGroup
- addSlider
- addStringField
In each of these methods we generate the "script line" that we want as the command creates the GUI and passes all the options. For example in addChoice
we added two lines to extract this data:
java.util.List<String> choices = Arrays.asList(items).stream().map(item -> "\"" + item + "\"").collect(Collectors.toList()); // convert item array to list
String label2 = label;
if (label2.indexOf('_')!=-1)
label2 = label2.replace('_', ' ');
scriptLines.add(String.format("#@ String (label=%s, choices={%s}, value=%s) %s", label2, String.join(", ", choices), defaultItem, labelToName(label))); // create script string
...
Now when I run the Analyze Particles command the console outputs all the info we want (without even running the recorder):
#@ String (label=Size (pixel^2):, value=0-Infinity) size
#@ String (label=Circularity:, value=0.00-1.00) circularity
#@ String (label=Show:, choices={"Nothing", "Overlay", "Overlay Masks", "Outlines", "Bare Outlines", "Ellipses", "Masks", "Count Masks"}, value=Nothing) show
#@ Boolean (label=Display results, value=true) display
#@ Boolean (label=Exclude on edges, value=true) exclude
#@ Boolean (label=Clear results, value=true) clear
#@ Boolean (label=Include holes, value=true) include
#@ Boolean (label=Summarize, value=false) summarize
#@ Boolean (label=Overlay, value=false) overlay
#@ Boolean (label=Add to Manager, value=true) add
#@ Boolean (label=Composite ROIs, value=false) composite
From here we just need to grab scriptLines
and save it a file. So far it works great (printing to console)! I'm still working on addRadioButtonGroup
and addSlider
and should be done soon. I created a new branch "scriptlines" with the changes: https://github.com/imagej/ImageJA/tree/scriptlines
Notes from talk with @ctrueden:
- The script argline can be generated at the same time as the parameters
- For boolean parameters use a ternary operator since the string values should be omitted when the value is false
- Output language should be JavaScript. Note that JS uses backticks for interpolation, which is a nice way to reference the variable names
- Need to grab the command name.. is there a place to do this in GD or does it have to come when the plugin is actually executed?
- Name the output script file for the command, with each whitespace character replaced by an underscore
- Folder structure under needs to match the menu structure of each command
- Everything will be committed to
imagej-legacy
insrc/main/resources/scripts
- These script wrappers will collide with the original ImageJ menu items—we will probably need to tune the ImageJ Legacy menu augmentation logic not to overwrite existing built-in ImageJ 1.x commands with SciJava ones
- Once these script wrappers are complete, we should consider removing the logic to create
LegacyCommand
objects based on IJ1 commands. There is a related question though about whether to worry about non-core IJ1 plugins, e.g. Fiji plugins
One other note about the menu structure for scripts, which I forgot to mention: scripts are indeed placed in the menu structure at a position matching their folder structure. So e.g. src/main/resources/scripts/Plugins/My_Awesome_Stuff/Best_Plugin_Ever.js
will appear in the menus at Plugins › My Awesome Stuff › Best Plugin Ever. But with @imagejan I was also working on a new script directive #@script
which would allow to declare the same script-wide pieces of metadata that you can do with @Plugin(...)
in Java. So then it would become possible to write:
#@script(menuPath="Plugins>My Awesome Stuff>Best Plugin Ever")
at the top of a script and the declaration there would override the menu path.
The relevant issues are scijava/scijava-common#294 and scijava/scijava-common#344. It is past time we finish this work, and I had discussed having a pair programming session some time in September with him to do so anyway. If this work gets finished in time, then you could group all the ImageJ 1.x script wrappers in the same folder e.g. src/main/resources/scripts/legacy
in imagej-legacy. On the other hand, maybe it is nice to divide the scripts into folders by their ImageJ 1.x menu structure anyway? It would be less overwhelming. What do you think?
@gselzer Since this issue was written, you also explored wrapping the ImageJ commands as scripts, no? How far did you get? Is any work pushed anywhere? Any comments on the issue here?
@ctrueden I don't think I ever really tried, sorry.
@gselzer No worries, I must have misremembered! We can circle back to this issue again in another 5 years or so :sweat_smile: