Converting macro into plugin-type code to hide intermediate results

micro-manager
java
macro
Tags: #<Tag:0x00007fb87d58c770> #<Tag:0x00007fb87d58c478> #<Tag:0x00007fb87d58c310>

#1

I’m trying to do some image processing on a 4D/5D datasets from within a Micro-Manager plugin. I recorded an macro to perform the steps and copied the resulting Java code into my Micro-Manager plugin. It’s a bunch of calls to IJ.run() with appropriate string parameters. These operate on the top-most image window (each Micro-Manager image window contains an ImageJ window if I understand correctly, I know how to get the ImagePlus object from it). That approach works but is annoying, and probably not very efficient, because all the intermediate results are being displayed.

What is the best approach for converting my code into something more like an ImageJ plugin where the image manipulations are done in the background and only the final result is displayed? I may be missing something obvious as I haven’t done much programming with ImageJ so far.

I want this code to live within the (Java) Micro-Manager plugin, not as an ImageJ plugin or macro, because the settings for the image manipulations depend on Micro-Manager metadata values and settings that live in the Micro-Manager plugin.

I found some information about setBatchMode but I couldn’t figure out how it would apply in this situation.

If it matters, I’m using Micro-Manager 1.4.x which is distributed with ImageJ 1.48v.

Thanks!


#2

Hello @jondaniels and welcome to the ImageJ Forum!

You might want to try scripting your macro code, what would be in between macros and plain java code. Have a look at the scripting documentation in the ImageJ wiki.

You might also post your macro code here so other forum users can help you translating it into proper java code.

Cheers!


#3

Thanks @iarganda for that.

I have converted my macro into a Java script (running within Micro-Manager) and it is working but I would like suggestions on streamlining my script. I think my aims would be achieved by operating on the underlying ImageJ objects in Java instead of making a calls to IJ.run() which operate on the top-most window and show all the intermediate results.

Here is a somewhat simplified version of my Java script. It translates each slice by an amount that depends on both channel index and slice index. Because I couldn’t find another way, the script splits each channel into its own window, operates on each channel one at a time after selecting the appropriate window, and then recombines the channels by merging the relevant windows. Splitting and merging channels is a good example of an operation that could probably be avoided if the script were to operate on the underlying image objects instead of relying on IJ.run().

final ImagePlus ip = IJ.getImage();  // gets ImagePlus of top-most window
final String windowTitle = ip.getTitle();
final double dx =  12;  // dummy value, actual value depends on image metadata
final int nrChannels = ip.getNChannels();
final int width = ip.getWidth();
final int height = ip.getHeight();
final int nrSlices = ip.getNSlices();
final String title = windowTitle + "-deskewed";
final int width_deskewed = width + (int) dx*(nrSlices-1);

// create duplicate stack to avoid manipulating original data
// split into separate channels because have to treat each channel a bit differently
if (nrChannels > 1) {
   IJ.run("Duplicate...", "title=" + title + " duplicate");
   IJ.run("Split Channels");
} else {
   IJ.run("Duplicate...", "title=C1-" + title + " duplicate");  // make it named as 1st channel would be
}

String mergeCmd = "";
for (int c=0; c<nrChannels; c++) {  // loop over channels
   IJ.selectWindow("C" + (c+1) + "-" + title);
   final int dir = ((c % 2) == 0) ? 1 : -1;  // in reality more complex code goes here to determine direction of shift
   IJ.run("Canvas Size...", "width=" + width_deskewed + " height=" + height + " position=Center-" + (dir < 0 ? "Right" : "Left") + " zero");
   for (int s=0; s<nrSlices; s++) {  // loop over slices in stack and shift each by an appropriate amount
      IJ.setSlice(s+1);
      IJ.run("Translate...", "x=" + (dx*s*dir) + " y=0 interpolation=Bilinear slice");
   }
   mergeCmd += ("c" + (c+1) + "=C" + (c+1) + "-" + title + " ");   // keep track of file names so we can merge channels again
}

// merge the channels back together to display final image
if (nrChannels > 1) {
   IJ.run("Merge Channels...", mergeCmd + "create");
} else {
   IJ.run("Rename...", "title=" + title);
}

This operates correctly on the following dummy 2-channel z-stack dataset to produce an image that says “Hello world!” in both channels after the deskew operation. (I couldn’t figure out how to upload the 60k tif stack here, but have posted it at https://www.dropbox.com/s/j0o4l4rt4df4v13/Composite.tif).

I am aware that much of my script could be accomplished using TransformJ, but I cannot use it because I am stuck with pre-1.50 ImageJ. Furthermore I want to understand how to approach this sort of problem in general, independent of the specifics of this particular example.

The best idea I have is to find the Java code implementing each command I am using via IJ.run() via Plugins->Utilities->Find Commands… to find the class, then dig in the source code. Then I could trace through the source code to find the relevant tidbits and paste into my Java script after wiring up the correct variables. But this seems pretty tedious and not a good reuse of existing code. I’m hoping there is a better way to make my script operate more efficiently and without showing all the intermediate results.
Thanks for your help!


#4

Exactly, that is your next step. For that, you need to find in the ImageJ API the classes and methods behind your menu commands. One easy way to find the right class consists on using the Command Finder, where the corresponding class of each command is shown.

That being said, you can also make calls to IJ.run() with an extra parameter which is the input ImagePlus. For example:

IJ.run( ip, "Canvas Size...", "width=" + width_deskewed + " height=" + height + " position=Center-" + (dir < 0 ? "Right" : "Left") + " zero" );

#5

Thanks @iarganda.

To restate the answer I was originally seeking, there is not an easy way to convert from IJ.run() calls to underlying Java code, only by tracing through the Java code implementing each used ImageJ menu item and integrating the relevant low-level snippets into your own Java code.

By following that process I arrived at the following implementation of the snippet I posted earlier. If anyone spots further optimizations please speak up (e.g. maybe there isn’t a need to split and merge channels anymore?). I haven’t fully tested it but it seems to work and roughly halves the execution time on a realistic dataset and also doesn’t show any intermediate results. There is no indication of progress visible to the user but I imagine if I dug into the documentation I could find out how to display that on the status bar.

final ImagePlus ip = IJ.getImage();  // gets ImagePlus of top-most window
final String newTitle = ip.getTitle() + "-deskewed";
final double dx =  12;  // dummy value, actual value depends on image metadata
final int nrChannels = ip.getNChannels();
final int width = ip.getWidth();
final int height = ip.getHeight();
final int nrSlices = ip.getNSlices();
final int width_expansion = (int) dx*(nrSlices-1);
 
// create duplicate stack to avoid manipulating original data
// split into separate channels because have to treat each channel a bit differently
ImagePlus[] channels = new ImagePlus[nrChannels];
if (nrChannels > 1) {
  channels = ij.plugin.ChannelSplitter.split(ip.duplicate());
 } else {
  channels[0] = ip.duplicate();
}

ij.Prefs.set("resizer.zero", true);
ij.plugin.CanvasResizer resize = new ij.plugin.CanvasResizer();
for (int c=0; c<nrChannels; c++) {  // loop over channels
  ImagePlus i = channels[c];
  final int dir = ((c % 2) == 0) ? 1 : -1;  // in reality more complex code goes here to determine direction of shift
  i.setStack(resize.expandStack(i.getImageStack(), width + width_expansion, height, (dir < 0 ? width_expansion : 0), 0));
  for (int s=0; s<nrSlices; s++) {  // loop over slices in stack and shift each by an appropriate amount
    i.setSlice(s+1);
    ImageProcessor proc = i.getProcessor();
    proc.setInterpolate(false);
    proc.translate(dx*s*dir, 0);
  }
}

// merge the channels back together to display final image
ImagePlus deskewed;
if (nrChannels > 1) {
  deskewed = ij.plugin.RGBStackMerge.mergeChannels(channels, false);
} else {
  deskewed = channels[0];
}
deskewed.setTitle(newTitle);
deskewed.show();