Skip to content

Conversation

@radarhere
Copy link
Member

@radarhere radarhere commented Jun 14, 2025

Resolves #9006

#2856 (with 12 likes), #3781, #4887, #5227, #5465 and #5723 are all issues where the Image.fromarray() mode parameter has caused confusion.

#5465 (comment)

I think that the mode to use parameter is a footgun. It’s not an implicit conversion, it’s a cast.

Since those, #5849 improved the documentation, but it's not done causing problems, as #9006 objects to the fact that one type of array can't be correctly read using an explicit mode argument, but mode=None can handle it without a problem.

This PR suggests just deprecating the argument altogether.

There is a small loss of functionality in that P mode images can no longer be directly created using fromarray(). Currently, this can be done with Image.fromarray(a, "P"), but because array data does not contain palette information, this would no longer be possible.

@radarhere radarhere added the Deprecation Feature that will be removed in the future label Jun 14, 2025
@hugovk hugovk merged commit c704f43 into python-pillow:main Jun 25, 2025
77 of 78 checks passed
@radarhere radarhere deleted the fromarray_mode branch June 25, 2025 09:45
@neutrinoceros
Copy link
Contributor

neutrinoceros commented Jun 27, 2025

How should Image.fromarray(a, mode='P') be replaced ? there's one occurrence of this in matplotlib

@radarhere
Copy link
Member Author

from PIL import Image
import numpy
a = numpy.ones((256, 256), dtype=numpy.uint8)

# Before
p = Image.fromarray(a, "P")  # This has an empty palette
print(p, p.palette.mode, len(p.palette.colors))

# After
l = Image.fromarray(a)
p = l.convert("P")  # This has a full palette
p.putpalette([])  # If you would like to keep an empty palette, this will do so
print(p, p.palette.mode, len(p.palette.colors))

@manthey
Copy link

manthey commented Jul 1, 2025

I have some uint8 YCbCr data that I access via imgycbcr = PIL.Image.fromarray(arr, 'YCbCr'). What is the correct equivalent with this deprecation?

@radarhere
Copy link
Member Author

Could you put together a simple example to demonstrate what you're doing?

from PIL import Image
import numpy
a = numpy.ones((1, 1), dtype=numpy.uint8)
Image.fromarray(a, "YCbCr")

gives

ValueError: not enough image data

@manthey
Copy link

manthey commented Jul 2, 2025

I have a numpy array of shape (h, w, 3) which contains YCbCr data. For instance, a single blue pixel might be

a = np.array([[[30, 255, 107]]], dtype=np.uint8)

I want to convert this to RGB:

ycbcrimg = PIL.Image.fromarray(a, 'YCbCr')
ycbcrimg.convert('RGB').getpixel((0, 0))

yields (0, 1, 255) (our blue pixel).

It looks like I can do

ycbcrimg = PIL.Image.frombuffer('YCbCr', (a.shape[1], a.shape[0]), a.tobytes(), 'raw', 'YCbCr', 0, 1)

but that seems cumbersome compared to the now deprecated PIL.Image.fromarray(a, 'YCbCr'), and I was wondering if there was an alternative preferred idiom for this.

@radarhere
Copy link
Member Author

What do you think of this?

from PIL import Image
import numpy as np
a = np.array([[[30, 255, 107]]], dtype=np.uint8)
img = Image.new("YCbCr", (1, 1))
img.putdata([tuple(b[0]) for b in a])
print(img.convert('RGB').getpixel((0, 0)))

@manthey
Copy link

manthey commented Jul 2, 2025

I assume that would be inefficient for large images, since we are constructing a tuple for every pixel. I used a 1x1 pixel image as the smallest working example; my actual images are larger and can be numerous.

@radarhere
Copy link
Member Author

Ok. If the tuple construction is the only problem, I could put together a PR to allow lists to be used, meaning that

from PIL import Image
import numpy as np
a = np.array([[[30, 255, 107]]], dtype=np.uint8)
img = Image.new("YCbCr", (1, 1))
img.putdata(a.tolist()[0])
print(img.convert('RGB').getpixel((0, 0)))

would work. How does that sound?

@wiredfool
Copy link
Member

We really go out of our way to tell people not to do bulk pixel manipulation in python, so deprecating something that does the right thing in 0 time in C and then suggesting that level of pixel pushing in Python seems wrong.

When this was proposed, there was the one legitimate use case mentioned (P), this is definitely another. Specifying that the uint8 pixels represent YCbCr instead of RGB is the sort of cast that is safe and useful for importing -- it's essentially the same operation as specifying a colorspace -- rather than specifying a bit layout which is what trips people up.

I can see two options here:

  1. We allow the mode parameter, but only for modes that are compatible with the dtype. This allows for the matrix of RGB(XxA)/Lab/YCbCr to be specified properly. We explicitly error on things like bool->1 and float->int modes. This makes it explicitly cast only, which should remove the footgun aspect of this parameter.

  2. We provide an interface to change the im->mode between bit compatible modes, so that one can take any RGB image and interpret it as YCbCr.

@radarhere
Copy link
Member Author

radarhere commented Jul 3, 2025

Ok, I've created #9063 for the first option.

Pinging @guillaume-rochette-oxb just to make sure he is aware of this potential step in the other direction.

@manthey
Copy link

manthey commented Jul 3, 2025

Thank you.

@mara004
Copy link

mara004 commented Jul 10, 2025

Also ran into this deprecation with pypdfium2.
Problem is, I think this would lose the distinction between RGBA and RGBX, won't it? The former cannot be saved as JPEG, but the latter can.
AFAICS, this case is not included in the revert list in #9063 (comment) yet.

@radarhere
Copy link
Member Author

Ok, I've added RGBX to the list of RGBA alternatives in the PR.

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

Labels

Deprecation Feature that will be removed in the future

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Issue with PIL.Image.fromarray(..., mode="1") returning a transposed image

7 participants