Skip to content

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

Description

@guillaume-rochette-oxb

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

image_2: Image

image_3: Image

Activity

  1. changed the title [-]Issue with Image.fromarray(..., mode="1") returning a transposed image.[/-] [+]Issue with PIL.Image.fromarray(..., mode="1") returning a transposed image.[/+] on Jun 9, 2025
  2. changed the title [-]Issue with PIL.Image.fromarray(..., mode="1") returning a transposed image.[/-] [+]Issue with PIL.Image.fromarray(..., mode="1") returning a transposed image[/+] on Jun 9, 2025
  3. radarhere commented on Jun 10, 2025

    @radarhere
    Member

    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.

    Pillow/src/PIL/Image.py

    Lines 3405 to 3408 in 05636dc

    _fromarray_typemap = {
    # (shape, typestr) => mode, rawmode
    # first two members of shape are set to one
    ((1, 1), "|b1"): ("1", "1;8"),

    Because you're setting the mode, that is also used as the rawmode.

    See also #5465 (comment)

    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().

  4. guillaume-rochette-oxb commented on Jun 10, 2025

    @guillaume-rochette-oxb
    Author

    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)
  5. radarhere commented on Jun 10, 2025

    @radarhere
    Member

    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 - #5465 (comment)

    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.

  6. guillaume-rochette-oxb commented on Jun 10, 2025

    @guillaume-rochette-oxb
    Author

    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?

  7. radarhere commented on Jun 10, 2025

    @radarhere
    Member

    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.

  8. guillaume-rochette-oxb commented on Jun 11, 2025

    @guillaume-rochette-oxb
    Author

    Okay.

  9. radarhere commented on Jun 12, 2025

    @radarhere
    Member

    To try and get another opinion, @TheRealQuantam, you created #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.

  10. radarhere commented on Jun 14, 2025

    @radarhere
    Member

    I've created #9018 to suggest deprecating mode.

  11. radarhere commented on Sep 4, 2025

    @radarhere
    Member

    #9063 has partially reverted #9018, only deprecating mode for changing data types.

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