Pillow icon indicating copy to clipboard operation
Pillow copied to clipboard

Pillow cannot read from array gray image?

Open zjplab opened this issue 6 years ago • 6 comments

I have a pic: 391

Do some conversion:

AB = Image.open("./391.jpg").convert('RGB')
arr=np.array(AB)
[email protected]([0.2125, 0.7154, 0.0721]) # gray scale now 

Whatever mode you use, it just looks like fucked up: image

But if you use scipy.misc.toimage it is still usable: image

I don't know how. Just very strange

zjplab avatar Apr 09 '19 00:04 zjplab

Taking your code, I find that fromarray with L looks as you described, but if I omit the mode, it looks fine. Does this look okay for you? If not, what operating system and Pillow version are you using?

import numpy as np
from PIL import Image

AB = Image.open("im.jpg")
arr=np.array(AB)
[email protected]([0.2125, 0.7154, 0.0721])

# I agree that this does not look good
im = Image.fromarray(arr, 'L')

# This looks fine however
im = Image.fromarray(arr)

im.convert('RGB').save('out.jpg')

radarhere avatar Apr 10 '19 03:04 radarhere

@radarhere This is very strange since fromarray by default should read data as RGB. I checked the code from scipy.misc.toimage they use a different way to circumvent this bug:

    if len(shape) == 2:
        shape = (shape[1], shape[0])  # columns show up first
        if mode == 'F':
            data32 = data.astype(numpy.float32)
            image = Image.frombytes(mode, shape, data32.tostring())
            return image
        if mode in [None, 'L', 'P']:
            bytedata = bytescale(data, high=high, low=low,
                                 cmin=cmin, cmax=cmax)
            image = Image.frombytes('L', shape, bytedata.tostring())
            if pal is not None:
                image.putpalette(asarray(pal, dtype=uint8).tostring())
                # Becomes a mode='P' automagically.
            elif mode == 'P':  # default gray-scale
                pal = (arange(0, 256, 1, dtype=uint8)[:, newaxis] *
                       ones((3,), dtype=uint8)[newaxis, :])
                image.putpalette(asarray(pal, dtype=uint8).tostring())
            return image
        if mode == '1':  # high input gives threshold for 1
            bytedata = (data > high)
            image = Image.frombytes('1', shape, bytedata.tostring())
            return image
        if cmin is None:
            cmin = amin(ravel(data))
        if cmax is None:
            cmax = amax(ravel(data))
        data = (data*1.0 - cmin)*(high - low)/(cmax - cmin) + low
        if mode == 'I':
            data32 = data.astype(numpy.uint32)
            image = Image.frombytes(mode, shape, data32.tostring())
        else:
            raise ValueError(_errstr)
        return image

zjplab avatar Apr 12 '19 17:04 zjplab

fromarray by default should read data as RGB

What leads you to this conclusion?

https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.fromarray

mode – Mode to use (will be determined from type if None) See: Modes.

scipy's toimage doesn't even read it as RGB by default, it reads it as L.

>>> import numpy as np
>>> import scipy.misc
>>> from PIL import Image
>>> AB = Image.open("391.jpg").convert('RGB')
>>> arr=np.array(AB)
>>> [email protected]([0.2125, 0.7154, 0.0721]) # gray scale now 
>>> scipy.misc.toimage(gray).mode
'L'

I'm confused. Could you clarify what you are after in this issue? Do you feel that Image.fromarray(arr, 'L') should work, and are asking why it does not?

radarhere avatar Apr 12 '19 23:04 radarhere

@radarhere Yes I used scipy.misc.toimage in my code and I issued this just because I think Image.fromarray(arr, 'L') should work(many users might think this way too) but it does not.

zjplab avatar Apr 14 '19 01:04 zjplab

Here's an even simpler example that triggers this behavior:

from PIL import Image
import numpy as np
x = np.tile(np.arange(0,100).reshape(-1,1),300)
Image.fromarray(x,mode='L').show()

Should get this (with scipy.misc.toimage): image

Instead get this: image

jgodwin avatar Jun 20 '19 21:06 jgodwin

Having been dealing with similar problems for a waay to long time, I realized that the issue was resolved by tacking on .astype(np.uint8) to your array before passing it into Image.fromarray(), as such:

AB = Image.open("im.jpg")
arr=np.array(AB)
[email protected]([0.2125, 0.7154, 0.0721])
im = Image.fromarray(arr.astype(np.uint8), 'L')

or

x = np.tile(np.arange(0,100).reshape(-1,1),300)
Image.fromarray(x.astype(np.uint8), mode='L').show()

Although the most intuitive approach from the PIL library in my opinion would be to cast whatever array that is passed in as an argument to a uint8 since that's what the format mode="L" supports, I guess for now we can simply do it ourselves.

dettmar avatar Jan 28 '20 10:01 dettmar

Regarding the original image, it is an RGB image.

from PIL import Image
AB = Image.open("391.jpg")
print(AB.mode)  # RGB

If you try to apply fromarray() to this using "L",

import numpy as np
from PIL import Image

AB = Image.open("391.jpg")
arr=np.array(AB)
im = Image.fromarray(arr, "L")

you get ValueError: Too many dimensions: 3 > 2. L is a single channel image, with two dimensions, width and height. RGB is a three channel image, and so has a third dimension.

However, you've used the following line.

[email protected]([0.2125, 0.7154, 0.0721]) # gray scale now 

Before this, the "typestr" of the NumPy array is "|u1". The above line turns it into "<f8". If the mode argument is not given when using fromarray, then Pillow observes that the "typestr" of the NumPy array is "<f8", which is an F image. If you would like fromarray() to correctly understand the image as an L mode image, then it should have only one channel and "typestr" should be kept as "|u1"

Pillow is internally calling arr.tobytes(), and then stepping over each row. It's not trying to interpret each value in the array in sequence. So if the data isn't related to the mode in a meaningful way, then the final image will not be meaningful.

In the second image, the array has a "typestr" of "<i8". This isn't a type that Pillow supports.

Image.fromarray(arr.astype(np.uint8), 'L') is certainly a solution, converting the NumPy data to a form that Pillow will understand. However, I stand by my original note on the first image that Image.fromarray(arr) works. If you would like it to be an L mode image afterwards, Image.fromarray(arr).convert('L') will take care of that.

From what I see above, scipy is calling astype internally. So why doesn't Pillow also do that, to match the mode parameter?

https://github.com/python-pillow/Pillow/issues/2856#issuecomment-345688646

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.

If the idea is that it is to override the detected mode, then automatically converting data to match the mode will break backwards compatibility.

radarhere avatar Jan 07 '23 09:01 radarhere