ij.plugin.Scaler.resize() returns Inf calibration

Found a straightforward bug. If you directly call Scaler.resize() with a calibrated image, the result has a calibration of Inf x Inf. Uncalibrated images are not affected.

Looking at the java source, it seems the result calibration is computed by dividing the input calibration by the values xscale and yscale, but these values are only set if the function is invoked via the Image > Scale... dialog (or via Macro). Otherwise they are initialized as 0.0.

Python code to reproduce:

from ij import IJ
from ij.plugin import Scaler
imp = IJ.getImage()  # Must be calibrated.
Scaler.resize(imp, imp.getWidth(), imp.getHeight(), 1, '').show()

Simple workaround is to set correct calibration afterwards.

FIJI/ImageJ version 1.53c

Hello Muniak -

I can reproduce your issue.

I agree with your diagnosis.

It looks to me as if this can be fixed by having Scaler.resize()
set scaler.xscale and scaler.yscale (where scaler is the
instance of Scaler instantiated by the static Scaler.resize()
method) to the appropriate values before calling
scaler.createNewStack().

I’ve implemented this fix in a Scaler2 subclass of Scaler.

You can test this by placing this jar file:

scaler_2.jar (2.3 KB)

in your <path_to_fiji_install>/Fiji.app/jars
directory (restarting Fiji as necessary) and running this test
jython script:

from ij import IJ
import Scaler2
imp = IJ.createImage ('ramp', '8-bit ramp', 256, 256, 1)
imp.show()
Scaler2.resize(imp, imp.getWidth(), imp.getHeight(), 1, '').show()
Scaler2.resize(imp, imp.getWidth() / 2, imp.getHeight() / 2, 1, '').show()
imp.getCalibration().setUnit ('furlong')
Scaler2.resize(imp, imp.getWidth(), imp.getHeight(), 1, '').show()
Scaler2.resize(imp, imp.getWidth() / 2, imp.getHeight() / 2, 1, '').show()

Here is the source for Scaler2.java:

// Scaler2.java

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import ij.ImagePlus;
import ij.gui.Roi;
import ij.plugin.Scaler;
import ij.process.ImageProcessor;

/** Subclass of ij.plugin.Scaler to fix static resize() method
*/
public class Scaler2 extends Scaler {

    // use reflection to set various private super-class fields
    static void setInterpolationMethod (Scaler s, int m) {
        Field intF = null;
        try {
            intF = Scaler.class.getDeclaredField ("interpolationMethod");
        }
        catch (NoSuchFieldException e) {
            throw new RuntimeException (e);
        }
        intF.setAccessible (true);
        try {
            intF.set (s, m);
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException (e);
        }
    }
    static void setDoZScaling (Scaler s, boolean b) {
        Field dozF = null;
        try {
            dozF = Scaler.class.getDeclaredField ("doZScaling");
        }
        catch (NoSuchFieldException e) {
            throw new RuntimeException (e);
        }
        dozF.setAccessible (true);
        try {
            dozF.set (s, b);
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException (e);
        }
    }
    static void setProcessStack (Scaler s, boolean b) {
        Field proF = null;
        try {
            proF = Scaler.class.getDeclaredField ("processStack");
        }
        catch (NoSuchFieldException e) {
            throw new RuntimeException (e);
        }
        proF.setAccessible (true);
        try {
            proF.set (s, b);
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException (e);
        }
    }
    static void setXscale (Scaler s, double x) {
        Field xscF = null;
        try {
            xscF = Scaler.class.getDeclaredField ("xscale");
        }
        catch (NoSuchFieldException e) {
            throw new RuntimeException (e);
        }
        xscF.setAccessible (true);
        try {
            xscF.set (s, x);
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException (e);
        }
    }
    static void setYscale (Scaler s, double y) {
        Field yscF = null;
        try {
            yscF = Scaler.class.getDeclaredField ("yscale");
        }
        catch (NoSuchFieldException e) {
            throw new RuntimeException (e);
        }
        yscF.setAccessible (true);
        try {
            yscF.set (s, y);
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException (e);
        }
    }
    // use reflection to invoke private Scaler.createNewStack() super-class method
    static ImagePlus invokeCreateNewStack (
        Scaler s,
        ImagePlus imp,
        ImageProcessor ip,
        int newWidth,
        int newHeight,
        int newDepth
    ) {
        ImagePlus retval = null;
        Method creM = null;
        try {
            creM = Scaler.class.getDeclaredMethod (
                "createNewStack",
                ImagePlus.class,
                ImageProcessor.class,
                int.class,
                int.class,
                int.class
            );
        }
        catch (NoSuchMethodException e) {
            throw new RuntimeException (e);
        }
        creM.setAccessible (true);
        try {
            retval = (ImagePlus) creM.invoke (
                s,
                imp,
                ip,
                newWidth,
                newHeight,
                newDepth
            );
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException (e);
        }
        catch (InvocationTargetException e) {
            throw new RuntimeException (e);
        }
        return retval;
    }

    // replacement resize() method with xscale/yscale fix
    public static ImagePlus resize(ImagePlus imp, int dstWidth, int dstHeight, int dstDepth, String options) {
        if (options==null)
            options = "";
        Scaler scaler = new Scaler();
        if (options.contains("none"))
            setInterpolationMethod (scaler, ImageProcessor.NONE);
        if (options.contains("bicubic"))
            setInterpolationMethod (scaler, ImageProcessor.BICUBIC);
        boolean processStack = imp.getStackSize()>1 && !options.contains("slice");
        Roi roi = imp.getRoi();
        ImageProcessor ip = imp.getProcessor();
        if (roi!=null && !roi.isArea())
            ip.resetRoi();
        boolean myDoZScaling = dstDepth!=1;
        setDoZScaling (scaler, myDoZScaling);
        if (myDoZScaling)
            setProcessStack (scaler, true);
        setXscale (scaler, (double) dstWidth / imp.getWidth());
        setYscale (scaler, (double) dstHeight / imp.getHeight());
        return invokeCreateNewStack (scaler, imp, ip, dstWidth, dstHeight, dstDepth);
    }
}

The fix itself is quite small – most of the code is java-reflection
tedium required because the relevant fields and methods of
Scaler are private.

Thanks, mm

This bug is fixed in the ImageJ 1.53g5 daily build.

Hello Wayne and Muniak -

I can confirm that ImageJ 1.53g5 works as expected with the
following version of Muniak’s test script:

from ij import IJ
from ij.plugin import Scaler
print (IJ.getFullVersion())
imp = IJ.createImage ('ramp', '8-bit ramp', 256, 256, 1)
imp.show()
Scaler.resize(imp, imp.getWidth(), imp.getHeight(), 1, '').show()
Scaler.resize(imp, imp.getWidth() / 2, imp.getHeight() / 2, 1, '').show()
imp.getCalibration().setUnit ('furlong')
Scaler.resize(imp, imp.getWidth(), imp.getHeight(), 1, '').show()
Scaler.resize(imp, imp.getWidth() / 2, imp.getHeight() / 2, 1, '').show()

Thanks, mm

Thanks Wayne and MM for the rapid response!