Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Saving 16-bit grayscale PNG image doesn't work #6997

Closed
3h4 opened this issue Mar 8, 2023 · 4 comments
Closed

Saving 16-bit grayscale PNG image doesn't work #6997

3h4 opened this issue Mar 8, 2023 · 4 comments

Comments

@3h4
Copy link

3h4 commented Mar 8, 2023

Hi!

I'm trying to wrap my head around this, I read some issues have been fixed, but perhaps there still are some problems:
#2970
#3566

I'm trying to save a grayscale PNG image with compression, either uint16 or uint32, steps to reproduce:

import numpy as np
from PIL import Image
import os

maxuint = {
    'uint16':2**16-1,
    'uint32':2**32-1
}

for dtyp in ['uint16', 'uint32']:
    for ext in ['tif', 'png']:
        maxval = maxuint[dtyp]
        o = np.random.rand(1470,50)*maxval
        o = o.astype(dtyp)
        im = Image.fromarray((o))
        name = f'test-{dtyp}.{ext}'
        im.save(name, optimize=True)
        im = Image.open(name)
        a = np.asarray(im)
        size = os.stat(name).st_size
        print(f'{name}: {a.shape} {a.dtype} {size:,}')
    print('- '*3)
    

Output:

test-uint16.tif: (1470, 50) uint16 147,122
test-uint16.png: (1470, 50) int32 148,640
- - - 
test-uint32.tif: (1470, 50) int32 294,134
test-uint32.png: (1470, 50) int32 18,733
- - - 
  • Why does 16bit PNG and TIF have the same filesize, shouldn't PNG be compressed?
  • Why has 16-Bit PNG has datatype int32 when loaded?
  • Why are the 32-bit images not unsigned ints when loaded?
  • Why is 32-bit PNG many times smaller than 16-bit PNG?

Expected behaviour:
test-uint16.png should load as uint16
test-uint32.tif and test-uint32.png should load as uint32

@radarhere radarhere changed the title Saving 16-bit grayscale PNG image doesn't work. Saving 16-bit grayscale PNG image doesn't work Mar 8, 2023
@radarhere
Copy link
Member

  1. Why does 16bit PNG and TIF have the same filesize, shouldn't PNG be compressed?

https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#png-saving states that

When optimize option is True compress_level has no effect (it is set to 9 regardless of a value passed).

So, yes, it should be compressed.

You are generating random numbers, which sounds like the the nightmare scenario when trying to compress data.

  1. Why has 16-Bit PNG has datatype int32 when loaded?

There is an open issue for this - #3796. If you are looking for an explanation, I can offer #3041 (comment)

There's better support for image operations on int32 images than int16, especially for signed 16 bit images.

  1. Why are the 32-bit images not unsigned ints when loaded?

For one thing, Pillow doesn't have a mode that corresponds to every NumPy mode. We have I (32-bit signed integer pixels) and I;16*. We don't have a mode for 32-bit unsigned pixels. We convert various complicated modes into a smaller number of modes that are easier to deal with.

For another, the PNG format has a maximum of 16 as the bit depth - http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html. So even if we added a mode for unsigned 32-bit integers, it wouldn't made sense to request that the PNG be opened as such.

  1. Why is 32-bit PNG many times smaller than 16-bit PNG?

The 16-bit image has values that range from 0-65534 and the 32-bit image has values that range from 0-2147473565. It seems reasonable to think that the PNG found a way to turn the smaller amount of information into a smaller file.

If I turn off the compression by replacing optimize=True with compress_level=0, the files become a similar size.

@3h4
Copy link
Author

3h4 commented Mar 8, 2023

@radarhere
Thank you for you explanations and pointing out #3041 (comment) and the maximum of 16 as the bit depth in PNG.

The 16-bit image has values that range from 0-65534 and the 32-bit image has values that range from 0-2147473565. It seems reasonable to think that the PNG found a way to turn the smaller amount of information into a smaller file.

I see, so when 32-bit uint array is saved as PNG all values above 2**16-1 are simply discarded (set to zero?) which reduces the information in test-uint32.png? Perhaps there can be a warning message for this?

This is perhaps a better showcase of the compression using a checkerboard:

import numpy as np
from PIL import Image
import os

maxuint = {
    'uint16':2**16-1,
    'uint32':2**32-1
}

imgs = []

for dtyp in ['uint16', 'uint32']:
    for ext in ['tif', 'png']:
            maxval = maxuint[dtyp]
            #o = np.random.rand(1470,50)
            o = np.zeros((1470, 50))
            o[::10] = 0.5
            o[:,::10] = 1
            o = o*maxval
            o = o.astype(dtyp)
            im = Image.fromarray((o))
            name = f'test-{dtyp}.{ext}'
            im.save(name, compress_level=1)
            im = Image.open(name)
            a = np.asarray(im)
            size = os.stat(name).st_size
            print(f'{name}: {a.shape} {a.dtype} {size:,}   Max:{a.max()}')
            imgs.append(im)
    print('- '*3)

for im in imgs:
    display(im)
    

Output:

test-uint16.tif: (1470, 50) uint16 147,122   Max:65535
test-uint16.png: (1470, 50) int32 1,091   Max:65535
- - - 
test-uint32.tif: (1470, 50) int32 294,134   Max:2147483647
test-uint32.png: (1470, 50) int32 1,142   Max:65535
- - - 

The tiff can still not be saved as uint32, max is not 2**32-1 and the vertical stripes in the checkerboard are gone?

@radarhere
Copy link
Member

I see, so when 32-bit uint array is saved as PNG all values above 2**16-1 are simply discarded (set to zero?) which reduces the information in test-uint32.png? Perhaps there can be a warning message for this?

I now think you're talking about #3159

The tiff can still not be saved as uint32, max is not 2**32-1

As I said,

We have I (32-bit signed integer pixels) and I;16*. We don't have a mode for 32-bit unsigned pixels.

You might be interested in the F mode - https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes

the vertical stripes in the checkerboard are gone?

The vertical stripes aren't gone in the saved TIFF files, only when they are opened and shown - Pillow saves images to be shown as temporary PNG files.

@3h4
Copy link
Author

3h4 commented Mar 8, 2023

@radarhere Thank you!

@3h4 3h4 closed this as completed Mar 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants