Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pillow destroys image by incorrectly binarizing grayscale image during show() #6765

Closed
apacha opened this issue Nov 29, 2022 · 9 comments
Closed

Comments

@apacha
Copy link

apacha commented Nov 29, 2022

What did you do?

I've probably discovered a severe bug in PIL. I'm trying to open an image but when opening and showing it with PIL, the image is incorrectly binarized and visually completely destroyed.

0026

What did you expect to happen?

That the image opens without being destroyed.

What actually happened?

The image gets destroyed:
0026_destroyed

What are your OS, Python and Pillow versions?

  • OS: MacOS
  • Python: 3.9
  • Pillow: 9.3.0
from PIL import Image
Image.open("0026.png").show()

when reading the very same image with opencv, it works just fine:

import cv2
img_cv2 = cv2.imread("0026.png")
Image.fromarray(img_cv2).show()
@radarhere
Copy link
Member

It's not actually during Image.open(). If you try to resave the image, you should find that it is perfectly fine.

from PIL import Image
Image.open("0026.png").save("out.png")

It is during show() that the problem is occurring. The image has a rawmode of I;16B, which is opened as I instead (see #3041 or #3796). Pillow tries to convert this to L.

base = Image.getmodebase(image.mode)
if image.mode != base:
image = image.convert(base)

@radarhere radarhere changed the title Pillow destroys image by incorrectly binarizing grayscale image during Image.open() Pillow destroys image by incorrectly binarizing grayscale image during show() Nov 29, 2022
@radarhere
Copy link
Member

I've created PR #6766 to resolve this.

@apacha
Copy link
Author

apacha commented Nov 30, 2022

I don't believe that the provided PR is sufficient to fix the problem. I think the problem goes much further and is a systematic bug in the way how a PNG image that is encoded with I-mode is being handled:

path = Path("0026.png")
image = Image.open(path)
image = image.convert("RGB")
image.save("0026_corrupt.png")
image.show()

creates the exact same corrupted image. In the case of saving the image after conversion, the patched ImageShow() class is not even involved.

@radarhere
Copy link
Member

Regarding converting an I;16B rawmode PNG to RGB, that has been previously reported as #5642.

I'm guessing you're not interested in a workaround. #3159 is the general issue for this. There is actually not a consensus that it should be fixed.

@apacha
Copy link
Author

apacha commented Nov 30, 2022

I'm very much interested in a workaround, but right now I don't know how I could process the given image with PIL without corrupting it.

By the way, it also happens when converting to grayscale: image = image.convert("L"), not just RGB.

The only workaround I can currently think of is:

import cv2
from PIL import Image

image = Image.open("0026.png")
if image.mode == "I":
  cv2_img = cv2.imread("0026.png")
  cv2.imwrite("0026_rgb.png", cv2_img)

# Now I can open and work with the image in PIL:
image = Image.open("0026_rgb.png")
image.show()

@radarhere
Copy link
Member

Ok, the workaround would be to scale down the pixel values in the image. The following code can be used for converting to either RGB or L.

from PIL import Image
im = Image.open("in.png")
if im.format == "PNG" and im.tile[0][-1] == "I;16B":
	im = im.point(lambda x: x / 256)
	im = im.convert("RGB")
	# Or convert to L instead
	#im = im.convert("L")
im.save("out.png")

or to show the image.

from PIL import Image
im = Image.open("in.png")
if im.format == "PNG" and im.tile[0][-1] == "I;16B":
	im = im.point(lambda x: x / 256)
im.show()

@radarhere
Copy link
Member

Closing as part of #3159

@apacha
Copy link
Author

apacha commented Dec 8, 2022

@radarhere one small question regarding the proposed workaround: Does a conversion to RGB even make sense? As far as I can tell, I;16 images are always grayscale images, see also

((1, 1), ">u2"): ("I", "I;16B"),

So:

from PIL import Image
im = Image.open("in.png")
if im.format == "PNG" and im.tile[0][-1] == "I;16B":
	im = im.point(lambda x: x / 256)
	im = im.convert("L")
im.save("out.png")

would make the most sense. At least when I tried to create a synthetic I;16B color image, I failed both with PIL as well as with OpenCV.

Another small thing: im.tile behaves a bit funky: PyCharm is giving me this warning

Screenshot 2022-12-08 at 14 29 45

and when I try to access the value in the debugger, it is always None. Only when executing Image.open() and im.tile directly after each other, then it is available. What kind of black magic is this? I have a bit of a bad feeling when using this call. And is there a guarantee that the operation im.tile[0][-1] will always succeed, or could there be images where this will throw an IndexOutOfRange exception?

@radarhere
Copy link
Member

Converting the image to RGB will work, but it's not space-efficient, no. I;16 images are always grayscale, yes, so sure, you can convert to L.

You could

  • try changing your code so that image is an ImageFile.ImageFile, or
  • instead of checking tile, check im.png.im_rawmode

but I wouldn't be surprised if those doesn't help, since they're still not class attributes. Pillow hasn't done much work on typing, and #2625 is an open issue.

When an image is loaded, tile is set to []. So yes, my intention was for you to run the code immediately after opening it.
Perhaps tile could be [] after opening a PNG, but such an image would treated by Pillow as empty, and is most likely broken. If it is a concern to you, you could just use if im.format == "PNG" and im.tile and im.tile[0][-1] == "I;16B": instead.

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

Successfully merging a pull request may close this issue.

2 participants