Skip to content

Image.fromarray silently fails with floating-point input #2856

Description

@jakevdp

When passing a floating-point array to Image.fromarray, the output does not match the input:

from PIL import Image
import numpy as np
x = np.ones((100, 100), dtype='float64')
x[40:60, 40:60] = 0.0
Image.fromarray(x, mode='L')

download

This happens is because when mode is specified, the array's bytes are passed directly to frombuffer without any type check:

Pillow/PIL/Image.py

Lines 2394 to 2438 in c82f9fe

def fromarray(obj, mode=None):
"""
Creates an image memory from an object exporting the array interface
(using the buffer protocol).
If obj is not contiguous, then the tobytes method is called
and :py:func:`~PIL.Image.frombuffer` is used.
:param obj: Object with array interface
:param mode: Mode to use (will be determined from type if None)
See: :ref:`concept-modes`.
:returns: An image object.
.. versionadded:: 1.1.6
"""
arr = obj.__array_interface__
shape = arr['shape']
ndim = len(shape)
strides = arr.get('strides', None)
if mode is None:
try:
typekey = (1, 1) + shape[2:], arr['typestr']
mode, rawmode = _fromarray_typemap[typekey]
except KeyError:
# print(typekey)
raise TypeError("Cannot handle this data type")
else:
rawmode = mode
if mode in ["1", "L", "I", "P", "F"]:
ndmax = 2
elif mode == "RGB":
ndmax = 3
else:
ndmax = 4
if ndim > ndmax:
raise ValueError("Too many dimensions: %d > %d." % (ndim, ndmax))
size = shape[1], shape[0]
if strides is not None:
if hasattr(obj, 'tobytes'):
obj = obj.tobytes()
else:
obj = obj.tostring()
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)

It would be helpful for users if the dtype of the input were checked, and an appropriate warning or error raised.

I'm using Python 3.6.1 and PIL v4.2.1

Activity

  1. wiredfool commented on Nov 20, 2017

    @wiredfool
    Member

    My impression of this parameter (which, tbh, is not well documented) is that it's for overriding the mode that's detected from the dtype. Raising an error where it doesn't match is going to be problematical for the intended use case. On the other hand, since it's assigned directly to the rawmode as well as the mode, there are only a handful of valid modes that will work, generally the ones where the mode and rawmode match in the unpackers table in libImaging/Unpack.c

  2. added this to the Future milestone on Apr 1, 2018
  3. radarhere commented on May 1, 2021

    @radarhere
    Member

    #4887 encountered this as well.

    import numpy as np
    from PIL import Image
    
    array = np.full((32,32,3),255)
    Image.fromarray(array, "RGB")

    And #5227

    import numpy as np
    from PIL import Image
    
    vector = np.ones((5, 5, 3)).astype("uint8") * np.array([3])
    Image.fromarray(vector, mode="RGB")

    And #5465

    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))
  4. radarhere commented on Nov 23, 2021

    @radarhere
    Member

    If you remove the mode parameter from the original post's fromarray call,

    from PIL import Image
    import numpy as np
    x = np.ones((100, 100), dtype='float64')
    x[40:60, 40:60] = 0.0
    im = Image.fromarray(x)
    im.save("out.tiff")

    then you get the expected result - out.tiff.zip
    (Here is it converted to PNG since GitHub won't allow TIFFs)
    out

    I'm going to suggest resolving this by just improving the documentation in #5849

  5. maoshi7 commented on Dec 11, 2024

    @maoshi7
    from PIL import Image
    import numpy as np
    
    a = np.zeros((200,200))
    a[0:2,0:2] = 1 
    a[99:101,99:101] = 1
    img = Image.fromarray(a,mode='L')
    img1 = np.array(img)

    I find that a is a matrix with lots of zeros and 8 ones at the right position, but img1 is a wrong matrix. Wrong numbers 240 and 63 appears at strange positions:
    image
    There is no warning, I didn`t understand why this change happened until I see this issue.
    I would like to know What data types and ranges are required for the different modes of Image.fromarray? I don't see this in the official documentation of pillow, can someone tell me? Did I mislocate the official documentation?
    https://pillow-docs-cn.readthedocs.io/zh-cn/latest/reference/Image.html#PIL.Image.fromarray
    Thanks!

  6. radarhere commented on Dec 11, 2024

    @radarhere
    Member

    https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.fromarray is the official documentation.

    The 'mode' parameter is optional. If you do not wish to tell Pillow to treat the data as a different mode, then you can just omit it.

    You might also be interested in

    Pillow/src/PIL/Image.py

    Lines 3356 to 3380 in d66c51a

    _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:
    ((1, 1), f"{_ENDIAN}i4"): ("I", "I"),
    ((1, 1), f"{_ENDIAN}f4"): ("F", "F"),
    }

  7. maoshi7 commented on Apr 3, 2025

    @maoshi7

    Thank you ! Do you mean that if I use img = Image.fromarray(a) instead of img = Image.fromarray(a,mode='L'), I will get correct results?

  8. radarhere commented on Apr 3, 2025

    @radarhere
    Member

    I expect so, yes.

  9. maoshi7 commented on Apr 3, 2025

    @maoshi7

    Thank you very very much. Although Python's documentation isn't as comprehensive as MATLAB's and bugs do occur from time to time, I believe it's the presence of enthusiastic individuals like you – who actively help others by answering questions and solving problems – that keeps Python thriving with such vibrant vitality!

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