Pillow icon indicating copy to clipboard operation
Pillow copied to clipboard

Converting this 16-bit grayscale image to 'L' mode destroys it

Open ExplodingCabbage opened this issue 7 years ago • 8 comments

Here is a 16-bit grayscale image:

test

It clearly contains a gradient of grays.

If I open it with Pillow, convert it to 8-bit grayscale, and save it, like so...

>>> from PIL import Image
>>> test_img = Image.open('test.png')
>>> test_img.mode
'I'
>>> test_img.convert('L').save('out.png')

... then I get this, which is mostly completely white:

out

This conversion should work; according to http://pillow.readthedocs.io/en/4.2.x/handbook/tutorial.html#converting-between-modes

The library supports transformations between each supported mode and the “L” and “RGB” modes.

and according to http://pillow.readthedocs.io/en/latest/handbook/concepts.html#modes, I is a supported mode.

Notably, I don't see this same problem if I start by loading an 8-bit RGB image from a JPG and then do .convert('I').convert('L'), so it's not simply the case that I->L conversion is broken in general. I'm not what specifically leads to the breakage in this case.

ExplodingCabbage avatar Feb 20 '18 22:02 ExplodingCabbage

There's a longstanding behavioral issue with Pillow where conversions don't intelligently use the range of the target mode.

So, in this case, you're taking an image with values from 0-65k and converting it to 0-255, with clipping. Most of the values are > 255, so they're all white. When you start with an 8 bit image, you convert 0-255 to 0-65k, but since there's no promotion, it's still just a 0-255 image. Converting back is then not a problem.

wiredfool avatar Feb 21 '18 07:02 wiredfool

This looks related to #3159

radarhere avatar Apr 13 '19 07:04 radarhere

I've created PR #3838 to resolve this.

radarhere avatar May 09 '19 01:05 radarhere

Resolved by #3838

radarhere avatar Jun 05 '19 20:06 radarhere

It turns out that this situation is more complicated. See https://github.com/python-pillow/Pillow/pull/3838#discussion_r292114051

radarhere avatar Jun 11 '19 09:06 radarhere

FYI, my work around for my use case (large gray-scale images):

x = np.linspace(0, 65535, 1000, dtype=np.uint16)
image = np.tile(x, (1000, 1)).T
plt.imshow(image)
plt.show()

im32 = image.astype(np.int32)
pil = Image.fromarray(im32, mode='I')

Shouldn't lose precision.

jamesjjcondon avatar Jul 17 '19 10:07 jamesjjcondon

For I to L conversion, this works for me:

def convert_I_to_L(img)
    array = np.uint8(np.array(img) / 256)
    return Image.fromarray(array)

machin3io avatar Nov 09 '19 21:11 machin3io

I wanted a non-numpy based solution and came up with:

def convert_I_to_L(im: Image):
    return ImageMath.eval('im >> 8', im=im.convert('I')).convert('L')

comparing this to machin3io's numpy code, ImageMath is faster for smaller images while numpy wins for larger ones:

           conversion time in ms
dimensions   ImageMath  Numpy
 640x 480          1.0    1.3
1280x 960          4.1    3.5
1920x1440          9.3    8.4

smason avatar Apr 06 '21 11:04 smason

Closing as part of #3159

radarhere avatar Dec 30 '22 05:12 radarhere