Image creation by linear scaling

Hi,

I inspected the source code of the function FloatProcessor.create8BitImage() and saw that the ‘scale’ variable is assigned the value of ‘256.0/(max2-min2)’. Shouldn’t that be ‘255.0/(max2-min2)’ ? It seems to me that using 256 is incorrect.

Thanks in advance,
G. E.

Hello G.E. -

I agree* with your conclusion that the constant in the scale
factor should be 255. I give my reasoning below.

To make the analysis simpler and more extreme, let’s consider
converting a 32-bit floating-point image to a 2-bit image whose
pixels take on the values (0, 1, 2, 3). For simplicity, let the
pixel values in the floating point image run from [0.0, 1.0].
(This is irrelevant to the analysis – the minimum and maximum
values drop out of the calculation.)

Here is the relevant code from FloatProcessor.java:

        double value;
        int ivalue;
        double min2 = getMin(), max2=getMax();
        int maxValue = 255;
        double scale = 256.0/(max2-min2);
        // if (thresholding) {
        //     maxValue = 254;
        //     scale = 255.0/(max2-min2);
        // }
        for (int i=0; i<size; i++) {
            value = pixels[i]-min2;
            if (value<0.0) value=0.0;
            ivalue = (int)((value*scale)+0.5f);
            if (ivalue>maxValue) ivalue = maxValue;
            pixels8[i] = (byte)ivalue;
        }

I’ve commented out the if (thresholding) {} part to avoid
confusion.

For our 2-bit case, scale == 4.0 (and maxValue == 3), and

    ivalue = (int) ( (4.0 * value) + 0.5 );

Here is how the (nearly) continuous value maps to the discrete
ivalue, and the fraction, f, of [0, 1] that maps to each ivalue:

   [0,   1/8)  -->  0;  f = 1/8
   [1/8, 3/8)  -->  1;  f = 1/4
   [3/8, 5/8)  -->  2;  f = 1/4
   [5/8,   1]  -->  3;  f = 3/8

This is asymmetric. There seems to be no good reason that three
times as much of the range [0, 1] maps to ivalue == 3 than
maps to ivalue == 0.

Note, it’s equivalent, but it might help the explanation to break
the range [5/8, 1] up into two pieces:

   [5/8, 7/8)  -->  3;  f = 1/4
   [7/8,   1]  -->  3;  f = 1/8

If, instead, we use G.E.'s suggestion to use (in our example case)
scale == 3.0, we get:

   [0,   1/6)  -->  0;  f = 1/6
   [1/6, 1/2)  -->  1;  f = 1/3
   [1/2, 5/6)  -->  2;  f = 1/3
   [5/6,   1]  -->  3;  f = 1/6

This is symmetric, and I would say clearly more satisfactory than
the choice of scale == 4.0.

*) However, even using scale == 3.0 still seems imperfect;
the two end bins get only half the love that the two central bins
do. This can be remedied by getting rid of the “offset” of 0.5
(and using the original scale == 4.0). That is, using (for our
2-bit case):

     ivalue = (int) (4.0 * value);

we get:

   [0,   1/4)  -->  0;  f = 1/4
   [1/4, 1/2)  -->  1;  f = 1/4
   [1/2, 3/4)  -->  2;  f = 1/4
   [3/4,   1)  -->  3;  f = 1/4

This seems better still.

Thanks, mm

1 Like

Dear mountain_man,

Thank you very much for your time and thorough explanation. The question now then, becomes, why this isn’t implemented in ImageJ as you suggest at the end, i.e. why is rounding used at all ?

G. E.

Hello G.E. -

I don’t why FloatProcessor has its conversion to 8-bit implemented
this way – perhaps the reason lost in the mists of antiquity. There
may be some sound image-processing reason for it, but I guess it
just seemed like a good idea at the time.

It is true that because of floating-point round-off error, the calculation
that maps [min2, max2] to [0.0, 1.0] will sometimes map to
values (just slightly) less than zero and (just slightly) greater than one.
But the code also has logic that (in effect) clips the final 8-bit result,
ensuring that it ranges from [0, 255].

Thanks, mm

Dear mountain_man,

Thanks again.

G. E.

Interesting observations. I suspect this is the reason behind the irregularity in the histogram of an 8-bit image converted from 32-bit shown by the following macro:

newImage(“Untitled”, “32-bit ramp”, 512, 512, 1);
run(“Histogram”, “bins=256 use x_min=0 x_max=1 y_max=Auto”);
selectWindow(“Untitled”);
run(“8-bit”);
run(“Histogram”);

The histogram of the 32-bit is flat, but the 8-bit version is not (50% fewer 0 pixels and 50% more of 255 than all other values in between).

Regards, David

1 Like

FWIW, the commit introducing this code is here:

I guess only @Wayne can tell us the rationale, since the commit message is lacking any explanation.

Hello David -

I believe that you are exactly right about this. (At first glance, the
histogram code appears correct.)

Here’s a related bit of fun. Create an 8-bit ramp image (512 x 512).
Convert it to a 32-bit image. You can confirm that the floating-point
value pixels are exactly the integers [0, 255]. Convert it back to
8-bit, and run the histogram. When I do this I get a “hole” (a count
of zero) for 128, with a double count (2048 instead of 1024) for 255.

Thanks, mm

1 Like

Hello Jan -

Thanks for finding that – so the change is from just last year. (I just
assumed that it had been this way “forever”.)

I note that the previous version used scale = 255.0 and
offset = 0.5, rather than scale = 256.0 and offset = 0.0`.

Thanks, mm

This v1.52e regression is fixed in the latest ImageJ daily build (1.52k70).

This macro reproduces the problem:

  setBatchMode(true);
  newImage("Untitled", "8-bit ramp", 512, 512, 1);
  getRawStatistics(nPixels, mean, min, max, std);
  print("8-bit:", mean, min, max, std);
  run("32-bit");
  getRawStatistics(nPixels, mean, min, max, std);
  print("32-bit:", mean, min, max, std);
  run("8-bit");
  getRawStatistics(nPixels, mean, min, max, std);
  print("8-bit:", mean, min, max, std);

With ImageJ 1.52j, it outputs:

  8-bit: 127.5  0  255  73.9004
  32-bit: 127.5  0  255  73.9004
  8-bit: 127.9961  0  255 74.3271

With ImageJ 1.52k70, it outputs:

  8-bit: 127.5  0  255  73.9004
  32-bit: 127.5  0  255  73.9004
  8-bit: 127.5  0  255  73.9004

Update
Float to 8-bit conversion does not work as expected without rounding so the 1.52k71 daily build rounds using

ivalue = (int)(value*scale+0.5);

This JavaScript code shows the problem:

  img = IJ.createImage("Untitled", "32-bit black", 512, 512, 1);
  ip = img.getProcessor();
  ip.setf(0,0,255);
  ip.setf(0,1,10.9);
  ip.setMinAndMax(0,255);
  IJ.log("before conversion: "+ip.getf(0,1));
  useScaling = true;
  ip2 = ip.convertToByte(useScaling);
  IJ.log("after conversion: "+ip2.getf(0,1));

With the 1.52k70 daily build, it outputs:

  before conversion: 10.899999618530273
  after conversion: 10

With the 1.52k71 daily build, it outputs:

before conversion: 10.899999618530273
after conversion: 11

Source code changes are at

1 Like