Issue with PIL.Image.fromarray(..., mode="1") returning a transposed image
What did you do?
I have an np.ndarray with shape=(h, w) and dtype=bool that I want to convert to a PIL.Image.Image (and later save on disk disk, although that is not relevant).
I have noticed that PIL.Image.fromarray(array, mode="1") does not work correctly, while PIL.Image.fromarray(array, mode=None) does behave correctly.
What did you expect to happen?
Referring to the sample code below, I expect that all the 3 arrays would be equal for all modes.
What actually happened?
It does not work for mode="1".
What are your OS, Python and Pillow versions?
Linux:
--------------------------------------------------------------------
Pillow 11.2.1
Python 3.12.9 | packaged by Anaconda, Inc. | (main, Feb 6 2025, 18:56:27) [GCC 11.2.0]
--------------------------------------------------------------------
Python executable is /home/***/miniconda3/envs/pil-env/bin/python3
System Python files loaded from /home/***/miniconda3/envs/pil-env
--------------------------------------------------------------------
Python Pillow modules loaded from /home/***/miniconda3/envs/pil-env/lib/python3.12/site-packages/PIL
Binary Pillow modules loaded from /home/***/miniconda3/envs/pil-env/lib/python3.12/site-packages/PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 11.2.1
--- TKINTER support ok, loaded 8.6
--- FREETYPE2 support ok, loaded 2.13.3
--- LITTLECMS2 support ok, loaded 2.17
--- WEBP support ok, loaded 1.5.0
*** AVIF support not installed
--- JPEG support ok, compiled for libjpeg-turbo 3.1.0
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.3
--- ZLIB (PNG/ZIP) support ok, loaded 1.2.13, compiled for zlib-ng 2.2.4
--- LIBTIFF support ok, loaded 4.7.0
--- RAQM (Bidirectional Text) support ok, loaded 0.10.1, fribidi 1.0.8, harfbuzz 11.0.1
*** LIBIMAGEQUANT (Quantization method) support not installed
--- XCB (X protocol) support ok
--------------------------------------------------------------------
macOS:
--------------------------------------------------------------------
Pillow 11.1.0
Python 3.12.9 | packaged by conda-forge | (main, Mar 4 2025, 22:44:42) [Clang 18.1.8 ]
--------------------------------------------------------------------
Python executable is /Users/***/miniconda3/envs/pil-env/bin/python3
System Python files loaded from /Users/***/miniconda3/envs/pil-env
--------------------------------------------------------------------
Python Pillow modules loaded from /Users/***/miniconda3/envs/pil-env/lib/python3.12/site-packages/PIL
Binary Pillow modules loaded from /Users/***/miniconda3/envs/pil-env/lib/python3.12/site-packages/PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 11.1.0
--- TKINTER support ok, loaded 8.6
--- FREETYPE2 support ok, loaded 2.13.2
--- LITTLECMS2 support ok, loaded 2.16
--- WEBP support ok, loaded 1.5.0
--- JPEG support ok, compiled for libjpeg-turbo 3.1.0
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.3
--- ZLIB (PNG/ZIP) support ok, loaded 1.3.1.zlib-ng, compiled for zlib-ng 2.2.2
--- LIBTIFF support ok, loaded 4.6.0
*** RAQM (Bidirectional Text) support not installed
*** LIBIMAGEQUANT (Quantization method) support not installed
--- XCB (X protocol) support ok
--------------------------------------------------------------------
Sample code
from PIL import Image
from urllib.request import urlopen
import numpy as np
def main():
url = "https://python-pillow.github.io/assets/images/pillow-logo.png"
base_image = Image.open(urlopen(url))
print(base_image.size, base_image.mode)
modes = [
"RGBA",
"RGB",
"L",
"I",
"F",
"1",
]
for mode in modes:
image_1 = base_image.convert(mode=mode)
array_1 = np.array(image_1)
image_2 = Image.fromarray(array_1, mode=mode)
array_2 = np.array(image_2)
image_3 = Image.fromarray(array_1)
array_3 = np.array(image_3)
size_equality = image_1.size == image_2.size == image_3.size
mode_equality = image_1.mode == image_2.mode == image_3.mode
shape_equality = array_1.shape == array_2.shape == array_3.shape
dtype_equality = array_1.dtype == array_2.dtype == array_3.dtype
print(f"image_1.size = {image_1.size}, image_2.size = {image_2.size}, image_3.size = {image_3.size}")
print(f"image_1.mode = {image_1.mode}, image_2.mode = {image_2.mode}, image_3.mode = {image_3.mode}")
print(f"array_1.shape = {array_1.shape}, array_2.shape = {array_2.shape}, array_3.shape = {array_3.shape}")
print(f"array_1.dtype = {array_1.dtype}, array_2.dtype = {array_2.dtype}, array_3.dtype = {array_3.dtype}")
print(f"size_equality = {size_equality}")
print(f"mode_equality = {mode_equality}")
print(f"shape_equality = {shape_equality}")
print(f"dtype_equality = {dtype_equality}")
array_1_and_array_2_equality = (array_1 == array_2).all()
array_1_and_array_3_equality = (array_1 == array_3).all()
array_2_and_array_3_equality = (array_2 == array_3).all()
print(f"array_1_and_array_2_equality = {array_1_and_array_2_equality}")
print(f"array_1_and_array_3_equality = {array_1_and_array_3_equality}")
print(f"array_2_and_array_3_equality = {array_2_and_array_3_equality}")
# uncomment to display images
# if not all([array_1_and_array_2_equality, array_1_and_array_3_equality, array_2_and_array_3_equality]):
# image_1.show()
# image_2.show()
# image_3.show()
print()
if __name__ == "__main__":
main()
image_1:
image_2:
image_3:
Hi. This question has been raised before as #5723. I suggest having a read of that.
In terms of our code, when determining a mode and rawmode to use for the array, mode 1 is the only one of your scenarios that uses a different rawmode. https://github.com/python-pillow/Pillow/blob/05636dca172e5b2cf7add8ae85fe22ae71a87e0b/src/PIL/Image.py#L3405-L3408
Because you're setting the mode, that is also used as the rawmode.
See also https://github.com/python-pillow/Pillow/issues/5465#issuecomment-831871276
I think that the mode to use parameter is a footgun. It’s not an implicit conversion, it’s a cast.
It seems like you already have a solution, simply to not set the mode in fromarray().
Hi,
Thank you for your response, however I do not think this is a satisfying solution.
Why not make a special case for it?
Implicit is not better than explicit, c.f. PEP 20.
Do you think it would be good to create fromarray2() then?
def fromarray2(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
if mode == "1":
mode = None
return fromarray(obj, mode)
Why not make a special case for it?
There is often a reluctance to change things, for the sake of backwards compatibility. It looks like this behaviour has been in place since before we forked from PIL, about 15 years ago.
I can understand that the mode argument doesn't work how you'd expect. However, I'm still not clear on why you need to pass it in.
Implicit is not better than explicit, c.f. PEP 20.
I don't personally think that's a sufficient reason to for the user to provide a mode. The array being passed in has a shape and a type. It more or less has a mode, in the same way that images have modes.
There might actually be a stronger appetite for deprecating the argument altogether - https://github.com/python-pillow/Pillow/issues/5465#issuecomment-831871276
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.
Pardon me, but this seems like you're implying that "it's not a bug, it's a feature" :/
I disagree, there is no bijection between mode and dtype, for example both modes RGB and HSV are represented with uint8, maybe instead we should have instead `fromarray(..., array_mode, image_mode) instead?
I'm trying to say that regardless of whether it is a bug, it's long-standing behaviour, and it's possible that users have workarounds that rely on it.
Feel free to disagree with me and create a PR, if you'd like to move forward and see what others think.
Okay.
To try and get another opinion, @TheRealQuantam, you created https://github.com/python-pillow/Pillow/discussions/6561 where you were using both L and P mode with fromarray(). Do you have any thoughts on this issue or on deprecating the mode parameter altogether?
We've had #2856 (with 12 likes), #3781, #4887, #5227, #5465 and #5723 - all issues were this parameter has caused confusion. Since then, #5849 improved the documentation, but as per this issue, it's not done causing problems.
I've created #9018 to suggest deprecating mode.
https://github.com/python-pillow/Pillow/pull/9063 has partially reverted #9018, only deprecating mode for changing data types.