Pillow
Pillow copied to clipboard
Converting this 16-bit grayscale image to 'L' mode destroys it
Here is a 16-bit grayscale image:

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:

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.
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.
This looks related to #3159
I've created PR #3838 to resolve this.
Resolved by #3838
It turns out that this situation is more complicated. See https://github.com/python-pillow/Pillow/pull/3838#discussion_r292114051
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.
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)
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
Closing as part of #3159