Fill holes ops stack

Dear all,
I would like to use the imagej-op fill holes as it can work also on a virtual images.

  1. If you start from a binarized image (loaded as UnsignedByteType) one needs to convert it to to a BooleanType in order to pass it to fillHoles. I call again “threshold.apply” but I am sure there are easy ways to do it.

  2. The behavior with a stack is puzzling. I would like to do a 2D fill holes for the whole XYZT stack. Currently it does not seem to fill any holes (it works if I pass a single plane). Does the fillholes apply the operation in 3D by default? If yes how to pass a 2D structuring element? Could you give an example for it or add such an example to the Jupyter notebook (https://nbviewer.jupyter.org/github/imagej/tutorials/blob/master/notebooks/1-Using-ImageJ/Ops/morphology/fillHoles.ipynb) ?

You can find the image at https://owncloud.gwdg.de/index.php/s/hj2ABIrgolWe3Xd t=1 Z_plane= 18

image

forground_binary-2

Thanks

antonio

1 Like

This is what I do in the groovy scipt after loading the image in ImageJ

#@ ImageJ ij
#@ Img input_full

import net.imglib2.type.numeric.integer.UnsignedByteType
import net.imglib2.type.BooleanType

// Need to convert to BooleanType. Workaround apply threshold again
// Direct cast to a differt type ?
a = new UnsignedByteType(1)
binaryInput = ij.op().run("threshold.apply", input_full, a)
output = ij.op().morphology().fillHoles(null, binaryInput)
ij.ui().show(output)

1 Like

You can do this using the convert.bit op:

ij.op().run("convert.bit", input_full)

Yes, by default, the op works with the same dimensionality as the input image.

Have a look at the op signature in the Op Finder (ShiftAltL):

RandomAccessibleInterval out?  <=  FillHoles(RandomAccessibleInterval out?, RandomAccessibleInterval in, Shape structElement?)

Note that you have to fill optional arguments from left to right, i.e. in this case, if you want to provide a structElement argument, you’ll also have to provide a pre-allocated out.

Hope that helps.

2 Likes

Thanks! I saw that it has an additional option.
do you mind to post an example of structElement I can try to google it but which class is it exactly?

You can use the utility class StructuringElements to generate the required Shape objects:

import net.imglib2.algorithm.morphology.StructuringElements

shape = StructuringElements.square(5, 3, false)
println shape

However, it occurs to me that I might have put you on the wrong track, as I’m not sure how fillHoles deals with different dimensionality of input and shape…

It might be a better approach to apply the fillHoles op slice-wise on your input image, using yet another helper op: slice

RandomAccessibleInterval out  <=  Slice(RandomAccessibleInterval out, RandomAccessibleInterval in, UnaryComputerOp op, int[] axisIndices, boolean dropSingleDimensions?)

In this case, you’ll have to create your reusable fillHoles op instance using (sorry, untested):

fillHolesOp = ij.op().op("fillHoles", in)

(Some of the op specialists here on the forum might have better advice: @ctrueden, @gselzer, @bnorthan, …)

1 Like

Dear @imagejan,
thanks for your response.

I tried this

shape = StructuringElements.square(1,2,false) // or square(5,2,false)
print shape
output = ij.op().morphology().fillHoles(null, binaryInput, shape[1]) // shape[0]
ij.ui().show(output)

but it does not really helps and the processing seems to have been performed in 3D. In general the fillHoles ops is very very very slow.
I also tried this

rec = new CenteredRectangleShape([1,1,0] as int[], false)
output = ij.op().morphology().fillHoles(null, binaryInput, rec)
ij.ui().show(output)

which fails with the error

java.lang.ArrayIndexOutOfBoundsException: 3
	at net.imglib2.AbstractInterval.min(AbstractInterval.java:182)
	at net.imglib2.algorithm.neighborhood.RectangleNeighborhoodRandomAccess.setPosition(RectangleNeighborhoodRandomAccess.java:171)

For the reusable ops I tried something like you said but I did not managed. I tried to follow the example in https://nbviewer.jupyter.org/github/imagej/tutorials/blob/master/notebooks/1-Using-ImageJ/Ops/slice.ipynb

fillHolesOp = Computers.unary(ij.op(), Ops.Morphology.FillHoles.class, output, input)
ij.op().run("slice", output, binaryInput, fillHolesOp, [0,0,1] as int)
ij.ui().show(output)

this does not work as fillholes is not a class but interface and honestly is just getting too complicated.
I try to use features from imageJ2 but it is at times a little frustrating. I guess I will go back to imageJ1 for this step

Thanks for your help
Antonio

1 Like

Dear Forum,
a final add on after a few trials.

In fact the imageJ1 “Fill Holes” and “Open” work on a virtual stack when calling the function from the macro (confusing is that from the GUI it complains).

#@ ImageJ ij
#@ ImagePlus input_full


//uses SciFo import Options -> ImageJ2 -> SciFo when Openining Files

ij.IJ.run(input_full, "Open", "stack")
ij.IJ.run(input_full, "Fill Holes", "stack")

Interestingly if you load the image using bioformats and virtual stack than it fails the binary opening.

Converting imglib2 image to imagej1 virtual stack and then to a non-virtual stack

Finally what would be the best way of converting a virtual imgJ2 stack to a non-virtual one, i.e. duplicate (This is required for some imageJ1 plugins.)

I did:

img = ImageJFunctions.wrap(input_full, 'imageplus')
img2 = img.duplicate()
img2.setDimensions(1, 25, 2)
ij.IJ.run(img2, "Fill Holes", "stack")
ij.IJ.run(img2, "Open", "stack")
img2.show()

This works but messes with the dimensions in the wrap step (Z -> C this is why I call setDimensions ). See also https://github.com/imglib/imglib2-ij/issues/29

1 Like

Here is an example that shows how to fill holes slicewise on a multi-dimensional dataset. I admit it’s a bit complicated (if anyone has ideas on how to make it more concise let me know).

To make things easier, this type of logic could be part of the ops framework. When a user calls a 2D algorithm (filtering with a 2D kernel, morphology with a 2D shape) on a ND image, the algorithm should be applied to each 2D slice automatically.

2 Likes

Thanks. Yes I think that a simpler logic would make it easier. It would be enough if the operation is applied according to the structuring element passed to the ops. Currently I don’t have the impression it is the case for fillHoles but I may be wrong.
For other ops i tried (e.g. Gauss filtering) the kernel is applied correctly.

The following Groovy script works for me with binary XYZ or XYZC images and produces two outputs,

  • a slice-wise 2d-filled version, and
  • a 3d-filled version of the input image:
#@ Img input
#@ OpService ops
#@output filled2d
#@output filled3d

// convert input image to BitType
binary = ops.run("convert.bit", input)

// fill holes slice-wise 2d in the first two dimensions (usually x and y)
filled2d = ops.run("create.img", binary)
fillOp = ops.op("fillHoles", filled2d)

ops.run("slice", filled2d, binary, fillOp, [0,1] as int[])

// fill holes 3d
filled3d = ops.run("fillHoles", binary)

Hope that helps.

3 Likes

@imagejan thank you for writing that script, it seems to work well.

@apoliti I am glad you were able to find the tutorial for fillHoles. Taken from the fillHoles tutorial:

Shape structElement : an optional parameter that defines how large of a space the Op searches when filling the holes. When the Op finds an “off” value that it determines is not an edge it will attempt to fill that “off” pixel and any neighboring values (where the Neighborhood is defined by this Shape ) that are also “off” and not separated from the current value by an “on” pixel. Most of the time no value needs to be passed through, so thus we leave this parameter out of the notebook.

One of the reasons that fillHoles might be running slowly for you is through the shape that you defined. fillHoles works by applying floodFill, which in turn calls this method here. The parameter shape in this method will be the shape you pass to fillHoles, and thus the larger your shape is the more computations you are doing. See the above quote about recommended Shape.

I do agree, however, that the Ops implementation seems slower than the legacy plugin (@imagejan do you notice this too?). I don’t know enough about Fiji to recommend a solution here, however naively looking at the code of DefaultFillHoles it does not seem like the ops code is any less efficient than the plugin, other than that the plugin always uses a single pixel at the shape (which is the default used by the op). I unfortunately have no more time today however I might be able to take another look next week.

Best,

Gabe

1 Like

I don’t have an answer to the performance issue either without digging in (and I don’t have time in the immediate future to do that). But I did want to quickly mention that the convert.bit op, as currently written, makes a copy in memory. If you are working with a huge image (since you said you want to use virtual images) you might to consider using the Converters utility class to do a lazy wrapping. Happy to elaborate if that’s interesting to you. In the future, we will have lazy conversion ops as well, but not yet.

2 Likes

Hi Curtis,
I compared the 2D and 3D ops for the filling (DefaultFillHoles) using the ops. For the same imageStack:

  • 1.5 sec for 2D (slice wise)
  • 34 sec for 3D

Quite surprised why it is so much longer in 3D. The Converters utility class looks still a little cryptic to me, but I think I can manage in case I need it.

Thanks
Antonio