pyimagej icon indicating copy to clipboard operation
pyimagej copied to clipboard

Make original ImageJ commands work as SciJava modules

Open hinerm opened this issue 3 years ago • 7 comments

[@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 Strings 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
}

hinerm avatar Aug 24 '21 20:08 hinerm

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 avatar Aug 25 '21 04:08 ctrueden

@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:

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

elevans avatar Aug 30 '21 21:08 elevans

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 in src/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

hinerm avatar Sep 01 '21 15:09 hinerm

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?

ctrueden avatar Sep 01 '21 15:09 ctrueden

@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 avatar Jun 23 '23 17:06 ctrueden

@ctrueden I don't think I ever really tried, sorry.

gselzer avatar Jun 23 '23 17:06 gselzer

@gselzer No worries, I must have misremembered! We can circle back to this issue again in another 5 years or so :sweat_smile:

ctrueden avatar Jun 23 '23 18:06 ctrueden