Skip to content

Values change with implicit conversion from mode F to L #5465

Description

@meyerjo

Might be somewhat related to for instance #3011

What did you do?

import numpy as np
from PIL import Image
tgt = np.ones(shape=(2, 2)) # float32
tgt_im = Image.fromarray(tgt)
tgt_im_np = np.asarray(tgt_im)
final_img = Image.fromarray(tgt_im_np, mode='L')

print(np.asarray(final_img))

What did you expect to happen?

I assumed that the resulting matrix values still have the value of 1 but are implictly converted to 8-bit range.

What actually happened?

>>> print(np.asarray(final_img))
[[  0   0]
 [128  63]]

None of the resulting values keep the original value of 1 and (for me even more surprising) they change to different values at different places in the matrix.

What are your OS, Python and Pillow versions?

  • OS: Ubuntu 20.10
  • Python: Python 3.8.8
  • Pillow:
pip list
Package    Version
---------- -------------------
certifi    2020.12.5
numpy      1.20.2
Pillow     8.2.0
pip        21.0.1
setuptools 52.0.0.post20210125
wheel      0.36.2

Activity

  1. radarhere commented on May 4, 2021

    @radarhere
    Member

    Mode for fromarray is the "Mode to use". I think this is the mode to use when reading the numpy data - if so, then it makes sense that the values change when it is told to interpret the input data directly.

    You might be interested in .convert('L') instead of , mode='L',

    import numpy as np
    from PIL import Image
    tgt = np.ones(shape=(2, 2)) # float32
    tgt_im = Image.fromarray(tgt)
    tgt_im_np = np.asarray(tgt_im)
    final_img = Image.fromarray(tgt_im_np).convert('L')
    
    print(np.asarray(final_img))

    gives

    [[1 1]
     [1 1]]
    
  2. meyerjo commented on May 4, 2021

    @meyerjo
    Author

    Okay thanks for the workaround.

    However, is it an expected behavior that random numbers appear in the matrix after doing the implicit conversion?
    I assumed that the fromarray method either

    • calls the conversion script, if the input is np.float32, or,
    • raises an exception if the array.dtype does not match the specified mode.
  3. wiredfool commented on May 4, 2021

    @wiredfool
    Member

    I think that the mode to use parameter is a footgun. It’s not an implicit conversion, it’s a cast. In this case, you’re interpreting an array of 32 bit floats as 8 bit ints.

    I’m not sure if there is a good use case at all for the mode parameter here, as we need to be pretty sure what we’re getting from numpy to interpret the array correctly.

  4. dtoniolo commented on Sep 20, 2021

    @dtoniolo

    I had a similar issue in #5723. So, if mode is given to Image.fromarray(), then what happens is:

    1. the image is created interpreting the array in a predefined manner (e.g. F if the dtype is np.float32 and the values all reside in the [0, 1].
    2. The image is then converted to the requested mode, changing the dtype and value range of the data array if necessary.

    Is this correct?

  5. radarhere commented on Nov 22, 2021

    @radarhere
    Member

    To respond to the previous comment,

    1. fromarray sets the mode according to

      Pillow/src/PIL/Image.py

      Lines 2877 to 2902 in d76fd93

      _fromarray_typemap = {
      # (shape, typestr) => mode, rawmode
      # first two members of shape are set to one
      ((1, 1), "|b1"): ("1", "1;8"),
      ((1, 1), "|u1"): ("L", "L"),
      ((1, 1), "|i1"): ("I", "I;8"),
      ((1, 1), "<u2"): ("I", "I;16"),
      ((1, 1), ">u2"): ("I", "I;16B"),
      ((1, 1), "<i2"): ("I", "I;16S"),
      ((1, 1), ">i2"): ("I", "I;16BS"),
      ((1, 1), "<u4"): ("I", "I;32"),
      ((1, 1), ">u4"): ("I", "I;32B"),
      ((1, 1), "<i4"): ("I", "I;32S"),
      ((1, 1), ">i4"): ("I", "I;32BS"),
      ((1, 1), "<f4"): ("F", "F;32F"),
      ((1, 1), ">f4"): ("F", "F;32BF"),
      ((1, 1), "<f8"): ("F", "F;64F"),
      ((1, 1), ">f8"): ("F", "F;64BF"),
      ((1, 1, 2), "|u1"): ("LA", "LA"),
      ((1, 1, 3), "|u1"): ("RGB", "RGB"),
      ((1, 1, 4), "|u1"): ("RGBA", "RGBA"),
      }
      # shortcuts
      _fromarray_typemap[((1, 1), _ENDIAN + "i4")] = ("I", "I")
      _fromarray_typemap[((1, 1), _ENDIAN + "f4")] = ("F", "F")

    So F mode is the "typestr" has "f" (if it is float, see https://numpy.org/doc/stable/reference/arrays.interface.html#object.__array_interface__ for more information)

    1. I wouldn't say that it is converted - the data is not changed - rather the same data is interpreted differently. This is not what you might expect.

    Some notes on the original post.

    1. The initial post states that
    tgt = np.ones(shape=(2, 2)) # float32

    This is not correct. tgt.dtype is "float64".

    import numpy as np
    from PIL import Image
    tgt = np.ones(shape=(2, 2))
    tgt_im = Image.fromarray(tgt)
    tgt_im_np = np.asarray(tgt_im)

    gives tgt_im_np of

    [[1. 1.]
     [1. 1.]]
    

    You might assume this is equal to np.ones(shape=(2, 2)), but tgt.dtype is "float64" and tgt_im_np.dtype is "float32". So the dtype changes, but I'm not convinced it's a problem that Pillow doesn't have a different mode for all of numpy's different modes.

    1. So if we remove that roundtrip from the original code, then
    import numpy as np
    from PIL import Image
    tgt = np.ones(shape=(2, 2))
    final_img = Image.fromarray(tgt, mode="L")
    print(np.asarray(final_img))

    gives

    [[0 0]
     [0 0]]
    

    This seems more reasonable than the "random numbers" that were initially described.

    import numpy as np
    from PIL import Image
    tgt = np.ones(shape=(2, 2))
    final_img = Image.fromarray(tgt)
    print(np.asarray(final_img))

    gives

    [[1. 1.]
     [1. 1.]]
    
  6. radarhere commented on Nov 23, 2021

    @radarhere
    Member

    Closing unless there are further questions. #2856 is also about this situation.

  7. radarhere commented on Nov 23, 2021

    @radarhere
    Member

    I've created PR #5849 to clarify this in the documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions