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

imageio.v3.imread returns read-only array #877

Closed
hookxs opened this issue Sep 12, 2022 · 7 comments · Fixed by #976
Closed

imageio.v3.imread returns read-only array #877

hookxs opened this issue Sep 12, 2022 · 7 comments · Fixed by #976

Comments

@hookxs
Copy link

hookxs commented Sep 12, 2022

Hi
Under certain circumstances (in particular, happened during reading a normal png image from a local disk, I can investigate more if necessary) the function imageio.v3.imread returns a read-only image (ie a np.ndarray with flags.writeable==False). Is this expected behavior or a bug? This never happened to me before v3 and there is nothing in the docs indicating that this might happen. I don't think most users would expect or appreciate anything like that and it can break downstream tasks, as it did in my case.

@FirefoxMetzger
Copy link
Contributor

Hard to say what is happening without more context; do you have an example image or snippet that produces this behavior?

Just to utter a guess: If you are reading a PNG using the pillow backend with a somewhat recent version of pillow then you may get a read-only view into pillow's image buffer instead of getting a full copy of the image data. This type of conversion (sharing the data buffer between numpy and pillow) is much faster than doing an extra copy of the buffer. Numpy is mindful of such read-only views and will create a copy as needed unless you explicitly call for in-place operations which will fail on read-only data. Is this what is happening for you?

@hookxs
Copy link
Author

hookxs commented Sep 12, 2022

Thanks for your response. The image itself is nothing special I think (an 8bit PNG, 1 grayscale channel actually saved by imageio.imwrite) and the code snippet is nothing more than img = imageio.imread("c:/img.png") so there is not much to share. This happens for all png images I have of this kind, not just one particular.
I am using imageio 2.21.2 and pillow 9.2.0. In some cases it is possible to specify img.flags.writeable=True after reading, in some cases this fails (ValueError) and there is no obvious solution.

This seems really weird, I don't think this can be called expected behavior even if it were marginally faster. I must say I've never encountered a case where imread-like function in any library (opencv, matlab,...) produced read-only data in memory. Is there a workaround you could suggest?
There are other mildly annoying compatibility breaking issues in v3, for example previously it was possibly to specify "jpg" as format in imwrite, now this fails and I must specify "jpeg".

@hookxs
Copy link
Author

hookxs commented Sep 12, 2022

Further info, of the available plugins for PNG images the plugin "pillow" produces read-only image as described above (I'd consider that a bug) while plugins "PNG-PIL" or "opencv" work as expected (return writeable np.ndarray). Unfortunately the behavior of different plugins is otherwise not exactly equivalent so this workaround is not ideal.

@FirefoxMetzger
Copy link
Contributor

Interesting. This is a new behavior introduced by Pillow 9.2.

I can do the following on imageio 2.21.2 and pillow 9.1.0:

>>> import imageio.v3 as iio
>>> import numpy as np
>>> rng = np.random.default_rng()
>>> img = rng.integers(0, 255, (128, 128), dtype=np.uint8)
>>> buffer = iio.imwrite("<bytes>", img, extension=".png") 
>>> frame = iio.imread(buffer)
>>> frame.flags
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY : False

but if I upgrade to pillow to 9.2.0 I get the behavior you describe:

>>> import imageio.v3 as iio
>>> import numpy as np
>>> rng = np.random.default_rng()
>>> img = rng.integers(0, 255, (128, 128), dtype=np.uint8)
>>> buffer = iio.imwrite("<bytes>", img, extension=".png")
>>> frame = iio.imread(buffer)
>>> frame.flags
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : False
  WRITEABLE : False
  ALIGNED : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY : False

The same will not happen with PNG-PIL (legacy pillow), because we perform at least one buffer copy internally; hence you will get a writable buffer. For OpenCV (also available in ImageIO via plugin="opencv" 🎉) I'm not 100% sure. My guess is that its policy is to make the caller the owner of the buffer which gives you write access in the process.

In some cases it is possible to specify img.flags.writeable=True after reading, in some cases this fails (ValueError) and there is no obvious solution.

One solution here is to call img = np.array(img) in the image at the beginning of your in-place operations. This will copy the data into a contiguous, writable buffer that is owned by you. Especially if you don't mind the extra copy this is a quite smooth solution.

This seems really weird, I don't think this can be called expected behavior even if it were marginally faster.

I'm not sure if this behavior is intentional or if it is a bug in pillow actually. I like it, but I see that it can be confusing in some cases. We can always check with the pillow devs if this was intentional and what their reasoning is.

There are other mildly annoying compatibility breaking issues in v3, for example previously it was possibly to specify "jpg" as format in imwrite, now this fails and I must specify "jpeg".

In the v2 API format= was used to select the plugin, format, or a combination of the two. This was messy, e.g. you had PNG-FI and PNG-PIL, but no PNG-ITK. Similarily, you had ITK and FFMPEG, but no PILLOW or FREEIMAGE. To make this less heterogeneous, we split the kwarg into plugin= (which selects plugins only) and extension= (which selects formats only).

We removed format= and it is now bundled with other **kwargs and forwarded to the backend that does the encoding/decoding. What you are calling is pillow's format= argument, which only accepts jpeg and not jpg.

Instead, try the following:

iio.imwrite("some/path.png", img, extension=".jpg")
iio.imwrite("some/path.png", img, extension=".jpeg")

Thinking about it, I've commented about format=, extension=, and plugin= multiple times recently. I will check if I can add it to the Caveats and Notes section of our migration guide.

@FirefoxMetzger
Copy link
Contributor

@hookxs FYI, I've asked about this behavior change in the pillow repo. If you want to follow the discussion, it lives here: python-pillow/Pillow#6581

@hookxs
Copy link
Author

hookxs commented Sep 15, 2022

Hey, thanks for all the investigation into the issue and link to the pillow point of view. As a user I can say that I consider this situation rather unfortunate. Because as of now, reading a .jpg image produces a normal writeable array (as was always the norm) (edit: it actually doesn't) while reading a .png is read-only because I guess a different plugin is used in the background. And having a read-only array can break downstream tasks (as it did for me). For the end user (of imageio) this is far from ideal because I think that most people use imageio as a sort of interface that deals with peculiarities of particular plugins but presents a unified front when possible. Now simply because somebody sent me a .png instead of a .jpg my code breaks even though both are fairly standard formats and I am using imageio library which supposedly can handle both. As a result I need to include some hack like if(not img.flags.writeable): img=np.array(img) which I think should be imageio's job:-)

Regarding the format keyword - that was my fault for not properly reading the docs, thanks for the pointers, but I must say that I looked into the docs (of v3) and would be hard-pressed to explain the difference between extension (which you suggested) and format_hint;-)

Edit: Actually, my bad, reading a .jpg also produces a read-only array, it just didn't matter in my case so I was under the impression that a different plugin is being used for .jpg. So at least this is consistent.

@FirefoxMetzger
Copy link
Contributor

FirefoxMetzger commented Sep 15, 2022

would be hard-pressed to explain the difference between extension (which you suggested) and format_hint;-)

That's easy. format_hint is deprecated and more or less the same as extension. It exists for backward compatibility and will be removed in a future version of ImageIO :)

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