Converting ArrayImg to ImagePlus

imglib2
jython
imagej

#1

Hi everyone,

Am somewhat new to scripting in ImageJ, so please forgive any miscommunication. I can clarify anything if needed.

I’m trying to use fill a 3D voxelized mesh consisting of vasculature - starting from a confocal image. The voxelization itself is an ArrayImg (which I believe is an ImgLib2 object). To do the filling, I am trying to use the IJ1 version of 3D flood fill, as to my knowledge, there is no equivalent for IJ2.

When I pass my voxelization as the first argument to Flood_Fill.fill(), I get the following error:

TypeError: fill(): 1st arg can’t be coerced to ij.ImagePlus

If I’ve understood correctly - this means that the 1st argument the 3D flood fill is expecting is an ImagePlus object, whereas what is being returned by the voxelization algorithm is a 10x10x10 ArrayImg.

Does anyone know if it is possible to convert between the two?

Cheers.


Running IJ1 Plugins with Ops/Sanity Check
#2

Hi @meadt,

Welcome to the forum.

Is it - you’re exactly correct.

You’re probably right here too.

It is possible, and pretty easy too, just need something like this:

from net.imglib2.img.display.imagej import ImageJFunctions as IJF
myImagePlus = IJF.wrap( myArrayImg, "myArrayImgTitle" )

There are a ton of great Jython scripting examples here by the way. A great place to learn.

Good luck and report back how it goes!
John

Edit: forgot second arg to wrap method


#3

Thanks for the help - wrapping the ArrayImg seems to have worked, but now I’m getting an out-of-bounds error when I provide the new ImagePlus as an argument to the flood fill argument. I’m not sure what it’s expecting to see (on that note, do you happen to know the best place to find documentation for older plugins?), but this is what the ImagePlus object looks like;

img["voxel_image" (-4), 8-bit, 10x10x10x1x1]

And here’s the traceback:

[ERROR] null
Traceback (most recent call last):
  File "Foo/Bar/quantify_one_image.py", line 49, in <module>
	at ij.process.ByteProcessor.get(ByteProcessor.java:251)
	at process3d.Flood_Fill.fill(Flood_Fill.java:70)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
java.lang.ArrayIndexOutOfBoundsException: java.lang.ArrayIndexOutOfBoundsException: 910

	at org.python.core.Py.JavaError(Py.java:552)
	at org.python.core.Py.JavaError(Py.java:543)
	at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:190)
	at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:206)
	at org.python.core.PyObject.__call__(PyObject.java:450)
	at org.python.core.PyObject.__call__(PyObject.java:454)
	at org.python.pycode._pyx0.f$0(Foo/Bar/quantify_one_image.py:72)
	at org.python.pycode._pyx0.call_function(Foo/Bar/quantify_one_image.py)
	at org.python.core.PyTableCode.call(PyTableCode.java:171)
	at org.python.core.PyCode.call(PyCode.java:18)
	at org.python.core.Py.runCode(Py.java:1614)
	at org.python.core.__builtin__.eval(__builtin__.java:497)
	at org.python.core.__builtin__.eval(__builtin__.java:501)
	at org.python.util.PythonInterpreter.eval(PythonInterpreter.java:259)
	at org.python.jsr223.PyScriptEngine.eval(PyScriptEngine.java:57)
	at org.python.jsr223.PyScriptEngine.eval(PyScriptEngine.java:31)
	at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264)
	at org.scijava.script.ScriptModule.run(ScriptModule.java:160)
	at org.scijava.module.ModuleRunner.run(ModuleRunner.java:168)
	at org.scijava.module.ModuleRunner.call(ModuleRunner.java:127)
	at org.scijava.module.ModuleRunner.call(ModuleRunner.java:66)
	at org.scijava.thread.DefaultThreadService$3.call(DefaultThreadService.java:238)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.ArrayIndexOutOfBoundsException: 910
	at ij.process.ByteProcessor.get(ByteProcessor.java:251)
	at process3d.Flood_Fill.fill(Flood_Fill.java:70)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:188)
	... 23 more

Cheers.

EDIT: Even better, I’ve done some rooting around in the VIB.jar file and I’ve managed to extract what I think is the source code, which I’ll post. Only problem is that I’m not at all familiar with Java, so I might need some help identifying where the problem is.


#4
3D Flood Fill Source:

package process3d;

import ij.IJ;
import ij.ImagePlus;
import ij.ImageStack;
import ij.gui.Toolbar;
import ij.plugin.MacroInstaller;
import ij.plugin.PlugIn;
import ij.process.ByteProcessor;
import ij.process.ColorProcessor;
import ij.process.FloatProcessor;
import ij.process.ImageProcessor;
import ij.process.ShortProcessor;
import java.awt.Color;
import java.io.PrintStream;

public class Flood_Fill
  implements PlugIn
{
  private static boolean debug = false;
  private static int tol = 0;
  public static final String MACRO_CMD = "var leftClick=16, alt=9;\nmacro 'Flood Fill Tool - C111O11ffC100T6c0aF' {\n while (true) {\n  getCursorLoc(x, y, z, flags);\n  if (flags&leftClick==0) exit();\n  call('process3d.Flood_Fill.fill', x,y,z);\n  exit(); }\n}\n\n";

  public void run(String arg)
  {
    MacroInstaller installer = new MacroInstaller();
    installer.install("var leftClick=16, alt=9;\nmacro 'Flood Fill Tool - C111O11ffC100T6c0aF' {\n while (true) {\n  getCursorLoc(x, y, z, flags);\n  if (flags&leftClick==0) exit();\n  call('process3d.Flood_Fill.fill', x,y,z);\n  exit(); }\n}\n\n");
  }

  public static synchronized void fill(String x, String y, String z)
  {
    fill(Integer.parseInt(x),
      Integer.parseInt(y),
      Integer.parseInt(z));
  }

  public static synchronized void fill(int sx, int sy, int sz)
  {
    fill(IJ.getImage(), sx, sy, sz,
      Toolbar.getForegroundColor().getRGB());
  }

  public static synchronized void fill(ImagePlus imp, int sx, int sy, int sz, int color)
  {
    IJ.showStatus("Flood fill");
    long start = System.currentTimeMillis();
    int w = imp.getWidth();int h = imp.getHeight();
    int d = imp.getStackSize();
    int wh = w * h;
    ImageProcessor[] b = new ImageProcessor[d];
    for (int z = 0; z < d; z++) {
      b[z] = imp.getStack().getProcessor(z + 1);
    }
    Difference diff = null;
    if ((b[0] instanceof ByteProcessor)) {
      diff = new DifferenceInt();
    } else if ((b[0] instanceof ShortProcessor)) {
      diff = new DifferenceInt();
    } else if ((b[0] instanceof FloatProcessor)) {
      diff = new DifferenceFloat();
    } else if ((b[0] instanceof ColorProcessor)) {
      diff = new DifferenceRGB();
    }
    int colorToFill = b[sz].get(sx, sy);

    Stack stack = new Stack();
    stack.push(sz * wh + sy * w + sx);
    while (!stack.isEmpty())
    {
      int p = stack.pop();
      int pz = p / wh;
      int pi = p % wh;
      int py = pi / w;
      int px = pi % w;

      int by = b[pz].get(px, py);
      if (diff.getDifference(by, colorToFill) <= tol)
      {
        b[pz].set(px, py, color);

        int pzwh = pz * wh;
        if ((px > 0) && (b[pz].get(px - 1, py) != color)) {
          stack.push(pzwh + pi - 1);
        }
        if ((px < w - 1) && (b[pz].get(px + 1, py) != color)) {
          stack.push(pzwh + pi + 1);
        }
        if ((py > 0) && (b[pz].get(px, py - 1) != color)) {
          stack.push(pzwh + pi - w);
        }
        if ((py < h - 1) && (b[pz].get(px, py + 1) != color)) {
          stack.push(pzwh + pi + w);
        }
        if ((pz > 0) && (b[(pz - 1)].get(px, py) != color)) {
          stack.push((pz - 1) * wh + pi);
        }
        if ((pz < d - 1) && (b[(pz + 1)].get(px, py) != color)) {
          stack.push((pz + 1) * wh + pi);
        }
      }
    }
    imp.updateAndDraw();
    long end = System.currentTimeMillis();
    System.out.println("Needed " + (end - start) / 1000L + " seconds");
    IJ.showStatus("");
  }

  static abstract interface Difference
  {
    public abstract float getDifference(int paramInt1, int paramInt2);
  }

  static final class DifferenceInt
    implements Flood_Fill.Difference
  {
    public float getDifference(int p1, int p2)
    {
      return Math.abs(p2 - p1);
    }
  }

  static final class DifferenceRGB
    implements Flood_Fill.Difference
  {
    public final float getDifference(int p1, int p2)
    {
      return

        Math.abs((p1 & 0xFF0000) >> 16 - (p2 & 0xFF0000) >> 16) + Math.abs((p1 & 0xFF00) >> 8 - (p2 & 0xFF00) >> 8) + Math.abs((p1 & 0xFF) - (p2 & 0xFF));
    }
  }

  static final class DifferenceFloat
    implements Flood_Fill.Difference
  {
    public float getDifference(int p1, int p2)
    {
      return Math.abs(Float.intBitsToFloat(p2) -
        Float.intBitsToFloat(p1));
    }
  }

  static final class Stack
  {
    private int[] array;
    private int size;

    Stack()
    {
      this.array = new int[1000000];
      this.size = 0;
    }

    public void push(int n)
    {
      if (this.size == this.array.length)
      {
        int[] tmp = new int[this.array.length + 1000000];
        System.arraycopy(this.array, 0, tmp, 0, this.array.length);
        this.array = tmp;
      }
      this.array[this.size] = n;
      this.size += 1;
    }

    public int pop()
    {
      this.size -= 1;
      return this.array[this.size];
    }

    public int size()
    {
      return this.size;
    }

    public boolean isEmpty()
    {
      return this.size == 0;
    }
  }
}

#5

Hi @meadt,

Thanks for tracking down the source, that’s super helpful.
Can you show how you’re calling Flood_Fill or how you’re calling the thing that calls it?

It looks like the starting point for the flood fill may be out of bounds, since your image is 10 x 10 x 10
if sx or sy (or both) is greater than 9 here, then that would cause the error you’re seeing.

John


#6

Can do. -->

filled_volume = Flood_Fill.fill( voxelization, interior_coord[0], interior_coord[1], interior_coord[2], fill_color)

where fill color is set to 100 and “interior_coord” is specified like so:

interior_coord = [260, 65, 6]

Thanks again for the help - I appreciate we’ve gone off-piste a little, but this is really helpful.


#7

@meadt,

Perfect, thanks.

Is the voxelization variable that 10x10x10 image from above? If so, that’s almost certainly the cause of the issue.

Whatever you pass as the x,y,z arguments has to be within the bounding box of the image, and from what I can tell, Flood_Fill works in pixel not physical units …

If that makes sense, great. But if you need more clarification then it would help me (and others who could help you) if you explained more about your whole pipeline and the context in which you’re using flood fill.

John


#8

Hi again,

Let me address your points and I’ll post the actual code afterwards.

I actually called wrap on voxelization before doing Flood Fill, like so;

voxelization = IJF.wrap(voxelization, "voxel_image")

which I assumed works, considering I don’t get the “coersion” error anymore.

interior_coord should correspond to a location within the blood vessel structure, a voxelized 3D mesh at this stage in the code, such that fill fills the interior of the 3D mesh. These are x, y and z pixel values (where z is the stack number). I can choose any location as long as it is within the mesh itself, if the flood fill is working as advertised.

Here is the code itself:

# @Dataset input
# @OpService opService
# @DatasetService DatasetService
# @UIService uiService
# @OUTPUT Dataset volume_edt

from ij import IJ
from net.imglib2.img.display.imagej import ImageJFunctions as IJF
from process3d import Flood_Fill

print( "Starting script" )

# Minor preprocessing
#blur = opService.filter.mean(input, [2, 2, 2])
#thresholded = opService.threshold.moments(blur)

thresholded = opService.convert().bit(input)

print(thresholded)

# Compute our 3D mesh
mesh = opService.geom().marchingCubes(thresholded)

print( mesh )

# Voxelize the mesh
## Voxelization requires conversion to ImagePlus object - done with IJF.wrap() ##
voxelization = opService.geom().voxelization(mesh)
voxelization = IJF.wrap(voxelization, "voxel_image")

print( voxelization )

# Some coordinate that is interior to our mesh. This is hard coded, and generally needs to be manually determined per image.
interior_coord = [260, 65, 6]

## Call IJ1 flood fill plugin ('process3d.Flood_Fill.fill', x-coord , y-coord, z-coord, fill color) ##
fill_color = 100
filled_volume = Flood_Fill.fill( voxelization, interior_coord[0], interior_coord[1], interior_coord[2], fill_color )

I also had a quick question about the ImagePlus object - using voxel_image as an example…

img["voxel_image" (-4), 8-bit, 10x10x10x1x1]

Can you tell me what the (-4) is indicating? Also, what exactly are the three dimensions in 10x10x10? I didn’t look too closely and assumed it was pixels, but given that the confocal image has more than 10 stacks, I’m now not too sure what to make of it.

In the meantime, I’ll start taking another look at the meshing - I think it might be reducing the size of the object, which causes it to throw an error when I give it “incorrect” pixel coordinates.

If anything is still unclear, please give me a shout.

Cheers.


#9

@meadt,

Thanks for sharing.

It’s the “id” of the ImagePlus and will be a unique value.

I think you’re right here, at first glance it looks like voxelization defaults to making a 10x10x10 output image see here. It should be possible to specify the exact size you want, but I’m not sure how.

John


#10

I didn’t dig into the code examples of this topic, but think this issue might be related:

I posted a small script there that demonstrates the issues, maybe it’s also helpful for debugging the issue here.


#11

I think you’re right here, at first glance it looks like voxelization defaults to making a 10x10x10 output image see here

Yeah, I see it. It looks like the height, width, and depth parameters have been set to 10. I’m unsure of how to change it from within Jython, but I’m in touch with the guy who wrote it, so maybe he can explain what to do with it.

I posted a small script there that demonstrates the issues, maybe it’s also helpful for debugging the issue here.

I’ve not used Groovy before, but it looks like you’re able to change the dimensions of the voxelization. Is that correct?

ops.run("geom.voxelization", convexHull, box[3]-box[0], box[4]-box[1], box[5]-box[2])), [box[0], box[1], box[2]]), img)


#12

Using the Op Finder (Plugins > Utilities > Find Ops…) you can see that the voxelization op has one required and three optional (indicated by the question mark ?) parameters:

RandomAccessibleInterval out  <=  Voxelization(Mesh in, int width?, int height?, int depth?)

The optional parameters default to 10 if not provided:

Does that help?


#13

That does, thanks. I actually just found the documentation using OP finder. My mistake was passing the parameters like so;

height = 512

etc. It seems to be working now. There’s more to the code that I haven’t included, but I think I’ve got enough to work with now.