Stats.max Op returns wrong result type

imagej-ops
bug
Tags: #<Tag:0x00007fa30b306410> #<Tag:0x00007fa30b3060f0>

#1

The stats.max op seems to have a bug which I have tried to illustrate with the following code snippets. Both the code snippets should ideally return the same result but Snippet 1 works as expected while snippet 2 returns an error which I have also posted below. The result of op in Snippet 2 returns a DoubleType for some reason. Can anybody please take a look into this?

Snippet 1:

FloatType maxVal = new FloatType();
ij.op().stats().max( maxVal, Views.iterable( imgSmooth ) );
float inverse = 1.0f / maxVal.getRealFloat();

Snippet 2:

FloatType maxVal = ij.op().stats().max( Views.iterable( imgSmooth ) );
float inverse = 1.0f / maxVal.getRealFloat();
java.lang.ClassCastException: net.imglib2.type.numeric.real.DoubleType cannot be cast to net.imglib2.type.numeric.real.FloatType
	at com.mycompany.imagej.TemplateMatchingPlugin.runThrowsException(TemplateMatchingPlugin.java:54)
	at com.mycompany.imagej.TemplateMatchingPlugin.run(TemplateMatchingPlugin.java:33)
	at com.mycompany.imagej.TemplateMatchingPlugin.main(TemplateMatchingPlugin.java:27)

#2

Hello mp007 -

Disclaimer: I do not understand the ops matcher system, and
I am very confused by the imglib2 numeric type hierarchy
(RealType<>, DoubleType, etc.). So I probably don’t know
what I am talking about.

Short answer (some details, below):

I don’t this that this is a bug, per se, but is how ops is
expected to work.

ops.stats().max(IterableInterval), as called in your
Snippet 2, returns a DoubleType, which cannot be cast
to a FloatType, so you get the ClassCastException.

However, in your Snippet 1 you tell max() to return a
FloatType by calling it with an additional argument,
ops.stats().max(FloatType, IterableInterval).

Longer answer:

Java does use the (types of the) arguments to a function to
determine which version of a function to call, and therefore
what type (that version of) the function returns.

But Java does NOT use the type on the left-hand side of an
assignment statement to choose which version of a function
on the right-hand side to use.

So in your Snippet 2, using a FloatType on the left-hand
side:


   FloatType maxVal = ij.op().stats().max(...);

doesn’t suffice to tell java to use a version of max() that
returns a FloatType, but calling max() (on the right-hand
side) with the additional FloatType argument, as you do
in Snippet 1, does tell the ops matcher to use a version of
max() that returns a FloatType.

The ops system (which I don’t understand) does a bunch of
stuff to figure out which actual version of a function to use, but
also doesn’t (can’t) look at the left-hand side of an assignment.

I believe that it is purposely designed to let you use an argument
of a specific type to give it a hint about which return type you
want.

The following IJ1-style plugin illustrates this behavior:

import ij.*;
import ij.gui.*;
import ij.plugin.*;

import net.imagej.legacy.IJ1Helper;
import net.imagej.ops.OpService;
import net.imglib2.img.Img;
import net.imglib2.img.display.imagej.ImageJFunctions;
import net.imglib2.type.numeric.real.DoubleType;
import net.imglib2.type.numeric.real.FloatType;

public class My_Plugin implements PlugIn {
  public void run (String arg) {

    // some boiler plate to get Ops ...
    OpService ops = IJ1Helper.getLegacyContext().getService(OpService.class);

    Img img32 = ImageJFunctions.wrap (NewImage.createImage ("img32", 512, 512, 1, 32, NewImage.FILL_RAMP));
    FloatType floatTypeVar = new FloatType();

    // this returns a DoubleType
    IJ.log ("ops.stats().max (img32).getClass().getName() = " + ops.stats().max (img32).getClass().getName());

    // this returns a FloatType
    IJ.log ("ops.stats().max (floatTypeVar, img32).getClass().getName() = " + ops.stats().max (floatTypeVar, img32).getClass().getName());

    // this works -- you can use a DoubleType for your result
    DoubleType dbl = (DoubleType) ops.stats().max (img32);

    // this fails -- it throws the ClassCastException you see
    // java.lang.ClassCastException: net.imglib2.type.numeric.real.DoubleType cannot be cast to net.imglib2.type.numeric.real.FloatType
    // FloatType flt = (FloatType) ops.stats().max (img32);
    
    // this works -- you've told ops to use a version of max that returns a FloatType
    FloatType flt = (FloatType) ops.stats().max (floatTypeVar, img32);

    // this works -- ops converts (doesn't just cast) the DoubleType to a FloatType
    flt = ops.convert().float32 (dbl);


    // *******
    // stop reading now -- you have been warned!

    // but just for fun ...

    Img img64 = ops.convert().float64 (img32);

    // max() still returns a DoubleType
    IJ.log ("ops.stats().max (img64).getClass().getName() = " + ops.stats().max (img64).getClass().getName());

    // but!  we can no longer use floatTypeVar to tell max() to return a FloatType
    // java.lang.ClassCastException: net.imglib2.type.numeric.real.DoubleType cannot be cast to net.imglib2.type.numeric.real.FloatType
    // IJ.log ("ops.stats().max (floatTypeVar, img64).getClass().getName() = " + ops.stats().max (floatTypeVar, img64).getClass().getName());
    
  }
}

Java has a useful facility called “reflection” (the getClass()
calls in the example plugin) that let you query an Object (an
instance of a Class) for its actual runtime type. I find this can
be very useful to help track what the ops matcher is doing.

I’ve commented out the lines that throw the ClassCastExceptions,
but you can uncomment them and run the plugin to see what happens.

Thanks, mm


#3

@mp007 Please post an entire minimal working example (MWE) That way, we know the generic parameter of ImgSmooth as well.

Update: I think this minimum working example replicates the behavior you observed:

import net.imagej.ImageJ;
import net.imglib2.img.array.ArrayImg;
import net.imglib2.img.array.ArrayImgs;
import net.imglib2.img.basictypeaccess.array.FloatArray;
import net.imglib2.type.numeric.real.FloatType;

public class Dummy
{

	public static void main( final String[] args )
	{
		final ImageJ ij = new ImageJ();
		final ArrayImg< FloatType, FloatArray > floats = ArrayImgs.floats( 1, 2, 3 );
		final FloatType o = ij.op().stats().max( floats );
		ij.context().dispose();
	}

}

I am not an imagej-ops expert by any means but here are my 2 cents: The culprit is probably AbstractStatsOp.createOutput: It always creates a DoubleType. IterableMax could override createOutput, e.g. something like this:

@Override
public T createOutput( final Iterable< T > input )
{
	return input.iterator().next().createVariable();
}

The problem here is that this will throw an exception if input has zero elements.

Update: I created an issue (imagej/imagej-ops#559) so hopefully the imagej-ops experts can find a way to solve this bug.


#4

Hi @hanslovsky

I just created a branch that implements your solution. If people think this is an acceptable solution I could also modify the min, and median op and open a pull request for review.

Brian