Save PNG with pixel values 0-1 only does not work

Hi guys,

I have an PNG in Fiji that has all 0-1 pixel values. But every time I save it and reopen it as PNG the values are now 0-255 … And so far I have no idea how to avoid this problem. Any hints?

I had the same issue recently. Ensuring the display range is 0-255 before saving worked (at least in terms of preserving the values… pity it looks all black anywhere else I view it).

Actually, a 2-step conversion to 8-bit indexed color seems to work as well, subject to brightness/contrast settings:

  • Image → Type → RGB Color
  • Image → Type → 8-bit Color

So presumably there is a scriptable path to getting there.

Perfect. That worked.

Hello Sebi and Peter -

At issue is the fact that the PNG format does not store the image’s
“Display range” metadata, and therefore ImageJ has to make a
choice when storing an image with a display range as PNG.

As you see, ImageJ chooses to store an image with rescaled pixel
values so that when you reopen it, you get the same displayed
appearance, but lose the original pixel values.

If your use case permits you to use TIFF format, that would be the
cleanest way to go. TIFF stores the display range, so when you
reopen it you get both the original appearance and the original
pixel values.

Note that the “8-bit Color” conversion does work for pixel values
of 0 and 1, but more or less by happenstance. In general,
converting color images that are in fact grayscale to 8-bit Color
is quite lossy. (If your use case is restricted to 0-1 grayscale
images you should be fine.)

Here is an IJ Macro that illustrates saving as TIFF and also the
artifacts introduced by using 8-bit Color. It starts with creating
a “half-ramp” 8-bit image whose pixel values range up to 127
instead of 255. (Actually up to 128, but never mind that.)

newImage ("hramp", "8-bit ramp", 256, 256, 1);
run ("Divide...", "value=2");
saveAs ("png", "hramp_png_raw.png");
setMinAndMax (0, 127);
saveAs ("png", "hramp_png.png");
saveAs ("tiff", "hramp_tif.tif");
run ("RGB Color");
run ("8-bit Color", "number=256");
saveAs ("png", "hramp_8c.png");

Here is the original half-ramp image before setting the Display
range, save as PNG. So far, so good.

hramp_png_raw.png:

hramp_png_raw

Here is the PNG version after setting the Display range:

hramp_png.png:

hramp_png

It displays “correctly” but the pixel values have changed.

Here is the TIFF version. You will probably have to download
the TIFF version and open it in ImageJ to see it:

hramp_tif.tif:

hramp_tif.tif (64.2 KB)

When opened in ImageJ it should display correctly and have
the correct pixel values.

Lastly, here is the 8-bit Color version:

hramp_8c.png:

hramp_8c

It displays with the correct display range, but you can see the
grayscale quantization artifacts due to the lossy 8-bit-color
conversion algorithm. (Also, pre-LUT, the pixel values are totally
garbled, and post-LUT, they range up to 255 instead of 127.)
Again, the 8-bit-color approach really only works by happenstance
for the 0-1 pixel-value case.

Thanks, mm

Thx a lot. Always amazing that an issue (that looks simple) has so much depth and details. I did follow what @petebankhead said and struggled with the results later because:

  • in Fiji my PNG shows only two values 0 or 1 (under the cursor)
  • on Python the same image has the values 0 or 247 (which was as least surprising to me)

So I ended up writing a little script in python to convert my 0-247 images to 0-1 images. And my taring pipeline is much happier …

import os
from skimage import io
import numpy as np

directory = r"D:\Train_Models\membranes\train\label"

value2one = 247

for fname in os.listdir(directory):
    print(fname)
    image = io.imread(os.path.join(directory, fname))
    new_image = image/value2one
    io.imsave(os.path.join(directory, fname), new_image.astype(np.uint8))
    
print('Done.')

@mountain_man, I understand the issues of converting to indexed color losing information – the trouble is that 8-bit grayscale images are a special case here. One might expect ImageJ’s export to an 8-bit PNG would preserve the original pixel values and write an indexed color image with a LUT (since PNG supports this), but in fact it rescales the values (similar to if ‘Apply’ was pressed in the brightness/contrast dialog first).

Upon closer inspection, it seems that ImageJ will rescale the pixel values when writing a single-channel 8-bit PNG if the following are true:

  • the LUT is grayscale, and not inverted
  • the minimum and maximum are not 0 and 255

In all other cases I’ve checked (inverted LUT, non-grayscale) ImageJ will write out the original pixel values and the corresponding LUT to the PNG. This feels to me the ‘right’ behavior; changing the pixel values unnecessarily feels wrong.

It seems possible to work around this and write an 8-bit grayscale image without changing pixel values if I request the image to write in a different way, i.e. using getImage() rather than getBufferedImage(). This works because the two methods provide images that have different color models.

/**
 * Write 8-bit PNG with ImageIO.
 */

import javax.imageio.ImageIO

def imp = ij.IJ.getImage()

// getImage() appears to preserve pixel values & use an IndexedColorModel
def img = imp.getImage()

// getBufferedImage() appears to apply the color model under some circumstances, changing pixel values in the output
//def img = imp.getBufferedImage()

ImageIO.write(img, "PNG", new File("/path/to/file.png"))

Converting to an indexed color image in ImageJ is a hack that can work in simple cases, but I agree it isn’t a reliable solution generally.

@sebi06 in the end I did something similar, accepting that values would be changed and thresholding my image on 0 later… I didn’t want to count on the non-zero being anything specific (255, 247 or anything else) :slight_smile:

Hello Peter -

Your observation about the LUT suggests another work-around,
namely to use a LUT instead of a Display range to get the 8-bit
image to display with “full contrast.”

Unfortunately, I couldn’t find any way to create the desired LUT
other than to build it by hand.

In any event, here is an illustrative script:

newImage ("hramp", "8-bit ramp", 256, 256, 1);
run ("Divide...", "value=2");
// build min-max LUT
lut = newArray (256);
for (i = 0; i < 256; i++) {
	lut[i] = minOf (2 * i, 255);
}
setLut (lut, lut, lut);
saveAs ("PNG", "hramp_lut.png");

And here is the resulting image:

hramp_lut.png:

hramp_lut

The image both displays with and re-opens with full 0–255
display intensities (thanks to the LUT), but with the correct
0–127 (well, 0–128, but never mind) pixel values.

Thanks, mm