Anisotropic Distance Map

knime
imagej-ops
scripting
Tags: #<Tag:0x00007fb882a172b8> #<Tag:0x00007fb882a170d8> #<Tag:0x00007fb882a16ca0>

#1

TL; DR: I’m having issues with the Ops/ImgLib2 implementation of a Distance Transform on anisotropic data:

  • one is concerning the distance computation for objects touching the border,
  • the other is related to running the computation on real data in an IJ2 plugin from within KNIME.

For a current project, I was in the need of generating a Euclidean Distance Map on anisotropic 3D data (i.e. where xy pixel spacing is different from z spacing).

In ImageJ, there are several options:

  • the built-in Process > Binary > Distance works 2D only and is therefore not suitable,
  • the Chamfer Distance Map 3D of MorphoLibJ (by @dlegland and @iarganda) did not respect anisotropic sampling in my trials (I might be wrong though, the documentation doesn’t state anything about anisotropic sampling)
  • the 3D Distance Map plugin of the 3D Suite by @ThomasBoudier takes the image calibration into account

… so using the last one, 3D Distance Map, is perfect as long as you stay in ImageJ.


Now I wanted to do this in KNIME, where the currently available Distance Map node unfortunately assumes isotropic spacing, and using the ImageJ macro node and relying on the presence of the 3D Suite in the local installation would be prone to errors when you aim to share your workflow with others.

Of course, there’s also the possibility to re-sample the image to isotropic spacing, as has been suggested in an answer to a related question by @Ofra_Golani on the mailing list a few years ago. However, I’d like to avoid this, as it increases the processing requirements in both computation and memory, and I have rather large datasets.


So, ImageJ-Ops to the rescue :slight_smile: Thanks to the work of Simon Schmid and @dietzc, a general distance transform is available in imagej-ops since a while:

Based on this, I used a small Groovy script to generate a distance map from a binary image with anisotropic pixel spacing:

#@ OpService ops
#@ Dataset input
#@output result

cal = []
input.numDimensions().times {
	cal << input.averageScale(it)
}

bitImg = ops.run("convert.bit", input);
result = ops.run("image.distancetransform", bitImg, cal as double[])

In order to use this in KNIME, I had to create a small adapter plugin (as long as the SciJava scripting integration for KNIME is not yet released…):


Now to the issues I’m having:

  • The script and the plugin work well in ImageJ, but I observed some strange output in highly anisotropic images when the object touches the border of the image.
    • Open the Bat Cochlea Volume sample image
    • Using Image > Properties…, set the calibration to e.g. x: 10.0, y: 10.0, z: 0.1
    • Compute the distance map using both my script and the 3D Suite plugin, and compare the results:

For illustration, here are the maximum projections of the resulting distance map:

distancetransform Op:
image

3D Distance Map:
image

Overlay of the results of Op (green) and 3D Distance Map (magenta):
image

What puzzles me is that the regions touching the border differ by a factor of 2.0 in the ops result, and it looks wrong. Is this explainable by the out-of-bounds strategy that is used in this implementation? Or is this a real issue with the underlying implementation in imglib2-algorithm? Any idea, @hanslovsky, @dietzc, @gab1one, others?


  • The second issue is related to running this in KNIME:

While the plugin works fine when testing on a small BitType image created using the Structuring Element Creator node, I get a lot of ArrayIndexOutOfBoundsExceptions when running it on a list of images that I previously loaded, cropped and tresholded. This is part of the stack trace:

2018-02-08 12:30:30,105 : DEBUG : KNIME-Worker-900 : KNIPLogService : Distance Map (with Calibration) : 0:698 : publish(
	context = org.scijava.Context@4818f7de
	consumed = false
	progress = -1
	maximum = -1
	status = Running command: Distance Map (with Calibration)
	warning = false,null,null), called from non-EDT Thread:null
2018-02-08 12:30:30,106 : DEBUG : KNIME-Worker-892 : KNIPLogService : Distance Map (with Calibration) : 0:698 : publish(
	context = org.scijava.Context@4818f7de
	consumed = false
	module = ch.fmi.AnisotropicDistanceMap,null,null), called from non-EDT Thread:null
2018-02-08 12:30:30,106 : DEBUG : KNIME-Worker-898 : KNIPLogService : Distance Map (with Calibration) : 0:698 : publish(
	context = org.scijava.Context@4818f7de
	consumed = false
	module = ch.fmi.AnisotropicDistanceMap
	processor = org.scijava.module.process.InitPreprocessor@4a5dcf07,null,null), called from non-EDT Thread:null
2018-02-08 12:30:30,106 : DEBUG : KNIME-Worker-952 : KNIPLogService : Distance Map (with Calibration) : 0:698 : publish(
	context = org.scijava.Context@4818f7de
	consumed = false
	module = ch.fmi.AnisotropicDistanceMap,null,null), called from non-EDT Thread:null
2018-02-08 12:30:30,108 : DEBUG : KNIME-Worker-991 : KNIPLogService : Distance Map (with Calibration) : 0:698 : Selected 'net.imagej.ops.convert.ConvertImages$Bit' op: net.imagej.ops.convert.ConvertImages$Bit
2018-02-08 12:30:30,108 : DEBUG : KNIME-Worker-991 : KNIPLogService : Distance Map (with Calibration) : 0:698 : Selected 'net.imagej.ops.Ops$Convert$Bit/net.imagej.ops.special.computer.UnaryComputerOp' op: net.imagej.ops.convert.ConvertTypes$IntegerToBit
2018-02-08 12:30:30,110 : DEBUG : KNIME-Worker-991 : KNIPLogService : Distance Map (with Calibration) : 0:698 : Selected 'net.imagej.ops.Ops$Map/net.imagej.ops.special.computer.UnaryComputerOp' op: net.imagej.ops.map.MapUnaryComputers$IIToIIParallel
2018-02-08 12:30:30,111 : DEBUG : KNIME-Worker-991 : KNIPLogService : Distance Map (with Calibration) : 0:698 : Selected 'net.imagej.ops.Ops$Create$Img' op: net.imagej.ops.create.img.CreateImgFromDimsAndType
2018-02-08 12:30:30,112 : DEBUG : KNIME-Worker-991 : KNIPLogService : Distance Map (with Calibration) : 0:698 : Selected 'net.imagej.ops.Ops$Create$ImgFactory' op: net.imagej.ops.create.imgFactory.CreateImgFactoryFromImg
2018-02-08 12:30:30,113 : DEBUG : KNIME-Worker-991 : KNIPLogService : Distance Map (with Calibration) : 0:698 : Selected 'net.imagej.ops.thread.chunker.ChunkerOp' op: net.imagej.ops.thread.chunker.DefaultChunker
2018-02-08 12:30:31,919 : DEBUG : KNIME-Worker-991 : KNIPLogService : Distance Map (with Calibration) : 0:698 : Selected 'net.imagej.ops.Ops$Image$DistanceTransform' op: net.imagej.ops.image.distancetransform.DistanceTransform3DCalibration
2018-02-08 12:30:31,920 : DEBUG : KNIME-Worker-991 : KNIPLogService : Distance Map (with Calibration) : 0:698 : Selected 'net.imagej.ops.create.img.CreateImgFromDimsAndType/net.imagej.ops.special.function.UnaryFunctionOp' op: net.imagej.ops.create.img.CreateImgFromDimsAndType
2018-02-08 12:30:31,920 : DEBUG : KNIME-Worker-991 : KNIPLogService : Distance Map (with Calibration) : 0:698 : Selected 'net.imagej.ops.Ops$Create$ImgFactory' op: net.imagej.ops.create.imgFactory.DefaultCreateImgFactory
2018-02-08 12:30:31,958 : ERROR : KNIME-Worker-909 : ThreadPool :  :  : An exception occurred while executing a runnable.
2018-02-08 12:30:31,958 : ERROR : KNIME-Worker-929 : ThreadPool :  :  : An exception occurred while executing a runnable.
2018-02-08 12:30:31,958 : DEBUG : KNIME-Worker-929 : ThreadPool :  :  : An exception occurred while executing a runnable.
java.lang.ArrayIndexOutOfBoundsException

I’ll try to create a minimal example workflow and follow up on this, but @dietzc, @gab1one if you have any pointers or could help debugging, I’d be grateful :slight_smile:


#2

The minimal example workflow would be very helpful, I think something might go wrong with the conversion of the image, leading to your problem. I will take a look into it once you can send me the example.


#3

Thanks @gab1one for your offer.

I was able to figure out that it’s related to the presence of an Image Cropper node in my workflow, so I’ve created this example workflow:

image

Cropper and Distance Map Issue.knwf (48.5 KB)

So I assume it might be related to this issue:

Or do I have to take care myself (in my plugin) that I’m working within the boundaries of the correct Interval?

Anyways, let me know if you can reproduce the issue. I’m going to release I just released the plugin to our FMI update site (https://community.knime.org/download/ch.fmi.knime.plugins.update/) soon, but you might also be able to build it yourself from the code linked above :slight_smile:


#4

@imagejan I got good news for you, it is quite easy to work around this bug. I assume the problem originates from the fact that the either the DistanceMap or our execution of it assumes that the input image has a zero minimum in all dimension, but the Image Cropper does not produce such images. In fact in your example, the min in the z dimension is 5. You can fix this, either in the code of the distance map, or by using a Java Snippet node (see attached workflow) with the following code:

outImage = new ImgPlus(ImgView.wrap(Views.zeroMin(inImage), inImage.factory()), inImage);

Cropper and Distance Map Workaround.knwf (45.9 KB)


#5

@imagejan I am not familiar with the ops distance transform implementation. It seems to me that it does not delegate to the implementation in imglib2-algorithm. I cannot find net.imglib2.algorithm.morphology.distance.DistanceTransform in the ops pull request nor in the imagej ops repository on master:

$ ~/git/imagej-ops ag 'net.imglib2.algorithm.morphology.distance.DistanceTransform' src
$ echo $?
1

#6

Oh, you’re right, this seems to be an ops-only implementation. Sorry for involving you then… I somehow thought it’s delegating to imglib2.

But if you happen to have an example of how to call ImgLib2’s DistanceTransform on any 3D image, I’d be very grateful and include into my comparison :slight_smile:


#7

@imagejan
I made this notebook for you, showing both isotropic and anisotropic distance transform for EUCLIDIAN distance. You can also choose L1 instead of EUCLIDIAN. The examples are 2D, but it works in 3D just the same.

Keep in mind, that this

  • implements distance transform for sampled functions, and
  • is equivalent to distance transform on binary images when setting points of interested to zero, and everything else to infinity (i.e. >> than any possible spatial distance in the image). That is the reason why the input is converted in the example.