Skip to content

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

Description

@ExplodingCabbage

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.

Activity

  1. changed the title [-]Converting this 16-bit image to grayscale destroys it[/-] [+]Converting this 16-bit grayscale image to 'L' mode destroys it[/+] on Feb 20, 2018
  2. wiredfool commented on Feb 21, 2018

    @wiredfool
    Member

    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.

  3. added
    BugAny unexpected behavior, until confirmed feature.
    on Apr 1, 2018
  4. added this to the Future milestone on Apr 1, 2018
  5. added and removed
    BugAny unexpected behavior, until confirmed feature.
    on Apr 2, 2018
  6. radarhere commented on Apr 13, 2019

    @radarhere
    Member

    This looks related to #3159

  7. radarhere commented on May 9, 2019

    @radarhere
    Member

    I've created PR #3838 to resolve this.

  8. radarhere commented on Jun 5, 2019

    @radarhere
    Member

    Resolved by #3838

  9. radarhere commented on Jun 11, 2019

    @radarhere
    Member

    It turns out that this situation is more complicated. See #3838 (comment)

  10. jamesjjcondon commented on Jul 17, 2019

    @jamesjjcondon

    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.

  11. machin3io commented on Nov 9, 2019

    @machin3io

    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)
  12. smason commented on Apr 6, 2021

    @smason
    Contributor

    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
    
  13. radarhere commented on Dec 30, 2022

    @radarhere
    Member

    Closing as part of #3159

  14. added a commit that references this issue on Jan 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions