diff --git a/pom.xml b/pom.xml index 86ff253..b0dfa83 100644 --- a/pom.xml +++ b/pom.xml @@ -144,5 +144,17 @@ csbdeep 0.3.5-SNAPSHOT + + com.google.code.gson + gson + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-text + diff --git a/src/main/java/de/csbdresden/CommandFromMacro.java b/src/main/java/de/csbdresden/CommandFromMacro.java new file mode 100644 index 0000000..de4b09c --- /dev/null +++ b/src/main/java/de/csbdresden/CommandFromMacro.java @@ -0,0 +1,267 @@ +package de.csbdresden; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ExecutionException; + +import org.apache.commons.lang3.ClassUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.scijava.ItemVisibility; +import org.scijava.Named; +import org.scijava.command.Command; +import org.scijava.command.CommandInfo; +import org.scijava.command.CommandModule; +import org.scijava.command.CommandService; +import org.scijava.log.LogService; +import org.scijava.module.ModuleItem; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.service.Service; +import org.scijava.ui.UIService; + +import com.google.gson.Gson; + +import de.csbdresden.stardist.StarDist2D; +import ij.ImagePlus; +import ij.WindowManager; +import ij.plugin.frame.Recorder; +import net.imagej.Dataset; +import net.imagej.ImageJ; +import net.imglib2.RandomAccessibleInterval; + + +@Plugin(type = Command.class, label = "Command From Macro", menuPath = "Plugins > StarDist > Other > Command From Macro") +public class CommandFromMacro implements Command { + + private static final List SKIP_VISIBILITY = Arrays.asList(ItemVisibility.MESSAGE, ItemVisibility.INVISIBLE); + + @Parameter + private String command; + + @Parameter + private String args; + + @Parameter + private boolean process; + + // --------- + + @Parameter + private UIService ui; + + @Parameter + private CommandService cmd; + + @Parameter + private LogService log; + + // --------- + + @Override + public void run() { + + CommandInfo info = cmd.getCommand(command); + if (info == null) { + for (CommandInfo c: cmd.getCommands()) { + try { + if (command.equals(c.getMenuPath().getLeaf().getName())) { + info = c; + command = info.getClassName(); + break; + } + } catch (NullPointerException e) {} + } + if (info == null) { + log.error(String.format("Command \"%s\" not found.", command)); + return; + } + } + + final Map params = new LinkedHashMap<>(); + final List outputs = new ArrayList<>(); + + Map argsMap = new Gson().fromJson("{"+args+"}", Map.class); + // System.out.println(argsMap); + + for (Object keyO : argsMap.keySet()) { + final String key = String.valueOf(keyO); + final String value = String.valueOf(argsMap.get(keyO)); + ModuleItem item = null; + + item = info.getInput(key); + if (item != null) { + Class clazz = item.getType(); + if (clazz.isPrimitive()) + clazz = ClassUtils.primitiveToWrapper(clazz); + params.put(key, toParameter(value, clazz)); + } else { + item = info.getOutput(key); + if (item != null) { + outputs.add(key); + } else { + log.warn(String.format("Ignoring argument \"%s\" since neither an input or output of this command.", key)); + } + } + } + + try { + final CommandModule result = cmd.run(command, process, params).get(); + for (String name : outputs) { + final Object output = result.getOutput(name); + if (output != null) ui.show(output); + } + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + + + private Object toParameter(final String value, final Class clazz) { + // some special classes (TODO: incomplete) + if (clazz == String.class) + return value; + if (clazz == File.class) + return new File(value); + // all typical number types and boolean are covered by this + if (clazz.getName().startsWith("java.lang.")) { + try { + return clazz.getDeclaredMethod("valueOf", String.class).invoke(null, value); + } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + e.printStackTrace(); + } + } + // ImagePlus and typical imagej2 image types (all that implement RAI) + if (clazz == ImagePlus.class || RandomAccessibleInterval.class.isAssignableFrom(clazz)) { + final ImagePlus imp = WindowManager.getImage(value); + if (imp == null) + log.error(String.format("Could not find input image with name/title \"%s\".", value)); + return imp; + } + log.error(String.format("Cannot process arguments of class \"%s\".", clazz.getName())); + return null; + } + + + public static boolean record(final Command command, final CommandService commandService) { + return record(command, commandService, false); + } + + public static boolean record(final Command command, final CommandService commandService, final boolean process) { + if (Recorder.getInstance() == null) + return false; + final String recorded = Recorder.getCommand(); + final CommandInfo info = commandService.getCommand(command.getClass()); + // only proceed if this command is being recorded + final String name = info.getMenuPath().getLeaf().getName(); + // System.out.printf("RECORDED: %s, COMMAND: %s\n", recorded, name); + if (recorded==null || !recorded.equals(name)) + return false; + // prevent automatic recording + Recorder.setCommand(null); + // record manually + Recorder.recordString(getMacroString(command, commandService, process)); + return true; + } + + + private static String getMacroString(final Command command, final CommandService commandService, final boolean process) { + final Class commandClass = command.getClass(); + final CommandInfo info = commandService.getCommand(command.getClass()); + final Map args = new LinkedHashMap<>(); + + // add input parameters as arguments + for (final ModuleItem item : info.inputs()) { + final String name = item.getName(); + final Class clazz = item.getType(); + if (SKIP_VISIBILITY.contains(item.getVisibility()) || // skip items that shouldn't be recorded + Service.class.isAssignableFrom(clazz)) // skip all (injected) services + continue; + try { + final Field field = commandClass.getDeclaredField(name); + if (!field.isAccessible()) field.setAccessible(true); + final Object value = field.get(command); + // skip unassigned items (includes buttons) + if (value == null) + continue; + if (Named.class.isAssignableFrom(clazz)) // ImgPlus and Dataset + args.put(name, ((Named)value).getName()); + else if (clazz == ImagePlus.class) + args.put(name, ((ImagePlus)value).getTitle()); + else + args.put(name, String.valueOf(value)); + } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + // designate assigned outputs to be shown + for (final ModuleItem item : info.outputs()) { + final String name = item.getName(); + try { + final Field field = commandClass.getDeclaredField(name); + if (!field.isAccessible()) field.setAccessible(true); + final Object value = field.get(command); + // skip unassigned outputs + if (value == null) + continue; + args.put(name, ""); + } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + // convert to json and remove curly braces + String argsStr = new Gson().toJson(args); + argsStr = argsStr.substring(1, argsStr.length()-1); + // to make the macro string look nicer (otherwise have to escape all double quotes): + // replace double quotes around json keys/values with single quotes + // technically not correct json, but can be parsed by Gson + final StringBuilder sb = new StringBuilder(argsStr); + int p = 0; + for (Entry arg: args.entrySet()) { + final int k = StringEscapeUtils.escapeJava(arg.getKey()).length(); + final int v = StringEscapeUtils.escapeJava(arg.getValue()).length(); + sb.replace(p, p+1, "'"); p+=1+k; + sb.replace(p, p+1, "'"); p+=2; + sb.replace(p, p+1, "'"); p+=1+v; + sb.replace(p, p+1, "'"); p+=2; + } + argsStr = sb.toString(); + + final String execName = commandService.getCommand(CommandFromMacro.class).getMenuPath().getLeaf().getName(); + return String.format("run(\"%s\", \"command=[%s], args=[%s], process=[%s]\");\n", + execName, commandClass.getName(), argsStr, String.valueOf(process)); + } + + + public static void main(final String... args) throws Exception { + + final ImageJ ij = new ImageJ(); + ij.launch(args); + + Dataset input = ij.scifio().datasetIO().open(StarDist2D.class.getClassLoader().getResource("yeast_crop.tif").getFile()); + ij.ui().show(input); + +// Dataset input2 = ij.scifio().datasetIO().open(StarDist2D.class.getClassLoader().getResource("yeast_timelapse.tif").getFile()); +// ij.ui().show(input2); + + Recorder recorder = new Recorder(); + recorder.show(); + + final Map params = new LinkedHashMap<>(); + // params.put("input", input); + ij.command().run(StarDist2D.class, true, params); + + +// IJ.run("CommandFromMacro", "args=[\"input\":\"yeast_crop.tif\", \"label\":\"\"], process=[false], command=[de.csbdresden.stardist.StarDist2D]"); + } + + +} diff --git a/src/main/java/de/csbdresden/stardist/StarDist2D.java b/src/main/java/de/csbdresden/stardist/StarDist2D.java index 5c6fd11..8c490d3 100644 --- a/src/main/java/de/csbdresden/stardist/StarDist2D.java +++ b/src/main/java/de/csbdresden/stardist/StarDist2D.java @@ -29,6 +29,7 @@ import org.scijava.widget.ChoiceWidget; import org.scijava.widget.NumberWidget; +import de.csbdresden.CommandFromMacro; import ij.IJ; import ij.ImagePlus; import net.imagej.Dataset; @@ -47,7 +48,7 @@ @Menu(label = MenuConstants.PLUGINS_LABEL, weight = MenuConstants.PLUGINS_WEIGHT, mnemonic = MenuConstants.PLUGINS_MNEMONIC), @Menu(label = "StarDist"), @Menu(label = "StarDist 2D", weight = 1) -}) +}) public class StarDist2D extends StarDist2DBase implements Command { @Parameter(label="", visibility=ItemVisibility.MESSAGE, initializer="checkForCSBDeep") @@ -317,6 +318,9 @@ public void run() { final Future futureNMS = command.run(StarDist2DNMS.class, false, paramsNMS); label = (Dataset) futureNMS.get().getOutput("label"); } + // call at the end of the run() method + CommandFromMacro.record(this, this.command); + } catch (InterruptedException | ExecutionException | IOException e) { e.printStackTrace(); } finally { @@ -384,6 +388,9 @@ public static void main(final String... args) throws Exception { // Dataset input = ij.scifio().datasetIO().open(StarDist2D.class.getClassLoader().getResource("yeast_timelapse.tif").getFile()); Dataset input = ij.scifio().datasetIO().open(StarDist2D.class.getClassLoader().getResource("yeast_crop.tif").getFile()); ij.ui().show(input); + +// Recorder recorder = new Recorder(); +// recorder.show(); final HashMap params = new HashMap<>(); ij.command().run(StarDist2D.class, true, params); diff --git a/src/main/java/de/csbdresden/stardist/StarDist2DBase.java b/src/main/java/de/csbdresden/stardist/StarDist2DBase.java index 7f97d5e..21d5cc7 100644 --- a/src/main/java/de/csbdresden/stardist/StarDist2DBase.java +++ b/src/main/java/de/csbdresden/stardist/StarDist2DBase.java @@ -163,7 +163,6 @@ protected Dataset labelImageToDataset(String outputType) { return Utils.raiToDataset(dataset, Opt.LABEL_IMAGE, labelImg, axes); } else { return null; - } - + } } } diff --git a/src/main/java/de/csbdresden/stardist/StarDist2DNMS.java b/src/main/java/de/csbdresden/stardist/StarDist2DNMS.java index ed371f4..f50bec0 100644 --- a/src/main/java/de/csbdresden/stardist/StarDist2DNMS.java +++ b/src/main/java/de/csbdresden/stardist/StarDist2DNMS.java @@ -16,6 +16,7 @@ import org.scijava.widget.ChoiceWidget; import org.scijava.widget.NumberWidget; +import de.csbdresden.CommandFromMacro; import ij.IJ; import ij.ImagePlus; import net.imagej.Dataset; @@ -117,6 +118,9 @@ public void run() { } label = labelImageToDataset(outputType); + + // call at the end of the run() method + CommandFromMacro.record(this, this.command); }