Skip to content

Inconsistent Image.fromarray() behaviour with mode '1' #5723

Description

@dtoniolo

What did you do?

The following code can reproduce the issue:

import numpy as np
import PIL
from PIL import Image

print('Testing for Pillow version', PIL.__version__)


img_size = (2, 3)  # (w, h)
rng = np.random.default_rng()

test = rng.integers(0, 2, dtype=np.bool_, size=img_size[::-1])
no_mode = Image.fromarray(test)
no_mode_arr = np.array(no_mode)
outcome = np.all(no_mode_arr == test)
print('No mode specified. Mode:', no_mode.mode, 'dtype:',
      no_mode_arr.dtype, 'outcome:', outcome)
with_mode = Image.fromarray(test, mode='1')
with_mode_arr = np.array(with_mode)
outcome = np.all(with_mode_arr == test)
print('Mode specified. Mode:', with_mode.mode, 'dtype:',
      with_mode_arr.dtype, 'outcome:', outcome)

print('Input array:\n', test)
print('Output array with no mode specified:\n', no_mode_arr)
print('Output array with mode specified:\n', with_mode_arr)

Output:

Testing for Pillow version 8.3.2
No mode specified. Mode: 1 dtype: bool outcome: True
Mode specified. Mode: 1 dtype: bool outcome: False
Input array:
 [[ True False]
 [ True False]
 [False False]]
Output array with no mode specified:
 [[ True False]
 [ True False]
 [False False]]
Output array with mode specified:
 [[False False]
 [False False]
 [False False]]

What did you expect to happen?

When initialising an image from a boolean NumPy array, it is expected that True values are assigned to white pixels and False values to black ones. The resulting image should have mode '1' and if, converted back to an array, its contents should be equal to the initial input array.

What actually happened?

If Image.fromarray() is called with mode=None, the code works as expected. If instead it is called with mode='1', the array results in all False values. This is an issue for two reasons.

  1. The conversion of the data is incorrect.
  2. The behaviour of Image.fromarray() is inconsistent: given that when called with mode=None it detects the data as having mode='1', passing mode='1' as an input argument should not change the result.

What are your OS, Python and Pillow versions?

I have run the script with two different conda virtual environments. The OS is macOS Big Sur v11.5.2 (20G95).

  • Env 1:
      • Python: 3.8.10
      • Pillow: 8.3.2
  • Env 2:
      • Python: 3.8.10
      • Pillow: 7.1.2

Activity

  1. radarhere commented on Sep 20, 2021

    @radarhere
    Member

    Hi. This seems to be a duplicate of #5465. Try having a read through the comments there.

  2. radarhere commented on Sep 20, 2021

    @radarhere
    Member

    #5465 (comment)

    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.

    By default, the boolean data is inferred to have a mode of 1 and a rawmode of 1;8. Because you are explicitly setting it, they both become 1, and so the output data is different.

    #5465 (comment)

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

  3. dtoniolo commented on Sep 20, 2021

    @dtoniolo
    Author

    Yes, it's definitely the same issue, thanks. I'm closing in order not to duplicate the discussion.

    Edit Before I close, could you explain what is the rawmode and why boolean data is interpreted with a rawmode of 1;8? I can't find any information on the documentation

  4. radarhere commented on Sep 20, 2021

    @radarhere
    Member

    The raw mode is the mode of the incoming data.
    Pillow converts (unpacks) the data from the rawmode to the mode as a way of normalising it, reducing the number of modes that we have to support operations for.

    Here is the unpack method from 1;8 to 1 -

    static void
    unpack18(UINT8 *out, const UINT8 *in, int pixels) {
    /* Unpack a '|b1' image, which is a numpy boolean.
    1 == true, 0==false, in bytes */
    int i;
    for (i = 0; i < pixels; i++) {
    out[i] = in[i] > 0 ? 255 : 0;
    }
    }

    As opposed to our method for just unpacking 1 to 1 -

    static void
    unpack1(UINT8 *out, const UINT8 *in, int pixels) {
    /* bits (msb first, white is non-zero) */
    while (pixels > 0) {
    UINT8 byte = *in++;
    switch (pixels) {
    default:
    *out++ = (byte & 128) ? 255 : 0;
    byte <<= 1;
    case 7:
    *out++ = (byte & 128) ? 255 : 0;
    byte <<= 1;
    case 6:
    *out++ = (byte & 128) ? 255 : 0;
    byte <<= 1;
    case 5:
    *out++ = (byte & 128) ? 255 : 0;
    byte <<= 1;
    case 4:
    *out++ = (byte & 128) ? 255 : 0;
    byte <<= 1;
    case 3:
    *out++ = (byte & 128) ? 255 : 0;
    byte <<= 1;
    case 2:
    *out++ = (byte & 128) ? 255 : 0;
    byte <<= 1;
    case 1:
    *out++ = (byte & 128) ? 255 : 0;
    }
    pixels -= 8;
    }
    }

  5. dtoniolo commented on Sep 21, 2021

    @dtoniolo
    Author

    Ok, now it is starting to get more clear, thanks. Do you know if there's a table of all the possible rawmodes and the implicit conversions that users can read?

  6. radarhere commented on Sep 21, 2021

    @radarhere
    Member

    For fromarray?

    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")

  7. dtoniolo commented on Sep 23, 2021

    @dtoniolo
    Author

    Thanks

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

    AnacondaIssues with Anaconda's PillowNumPy

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions