-
Notifications
You must be signed in to change notification settings - Fork 50
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
New functionality to go to and from numpy arrays. #316
Conversation
Oh, great! I'll have a read.
pyvips does this already I think --- these are zero copy and use the buffer protocol: # numpy array to vips image
def numpy2vips(a):
height, width, bands = a.shape
linear = a.reshape(width * height * bands)
vi = pyvips.Image.new_from_memory(linear.data, width, height, bands,
dtype_to_format[str(a.dtype)])
return vi
# vips image to numpy array
def vips2numpy(vi):
return np.ndarray(buffer=vi.write_to_memory(),
dtype=format_to_dtype[vi.format],
shape=[vi.height, vi.width, vi.bands]) |
My initial investigation suggested that pyvips was making a copy rather than sharing, but I think that's because I just found a bug in which things get out of sync. Check out this, in which things go very pear-shaped: In [1]: import pyvips, torch, numpy as np
In [2]: z = np.zeros((1, 1))
In [3]: t = torch.as_tensor(z)
In [4]: p = pyvips.Image.new_from_memory(z.data, 1, 1, 1, 'double')
In [5]: p(0, 0)
Out[5]: [0.0]
In [6]: z[0, 0] = 1
In [7]: p(0, 0) # from here is looks like pyvips copies
Out[7]: [0.0]
In [8]: t[0, 0]
Out[8]: tensor(1., dtype=torch.float64)
In [9]: wat = p.crop(0,0,1,1) # an identity crop
In [10]: wat(0, 0) # now it sees it!?
Out[10]: [1.0]
In [11]: p(0, 0) # getpoint can't see it
Out[11]: [0.0]
In [12]: p.avg() # but .avg does!
Out[12]: 1.0
In [13]: p.getpoint(0, 0) # still no
Out[13]: [0.0]
In [14]: wat.getpoint(0, 0) #
Out[14]: [1.0]
In [15]: q = p.copy()
In [16]: q(0, 0)
Out[16]: [1.0]
In [17]: z[0, 0] = 2
In [18]: p.avg()
Out[18]: 1.0
In [19]: q.avg()
Out[19]: 2.0
In [20]: q(0, 0)
Out[20]: [1.0]
In [21]: p.max() # .max sees the latest
Out[21]: 2.0
In [22]: p.avg() # but .avg doesn't !?
Out[22]: 1.0
In [23]: p(0, 0) # so, getpoint, avg, and max have 3 different versions!
Out[23]: [0.0]
In [24]: p.width, p.height, p.bands # verify that it really is a single-pixel image.
Out[24]: (1, 1, 1) This doesn't use any of my new PR, so I'm going to file this as a separate bug. |
Further experimentation showed that this strangeness was caused by operation caching, so that doing |
Ah right, yes, that's a mixture of pixel buffer caching and operation caching. You can flush pixel caches with https://www.libvips.org/API/current/VipsImage.html#vips-image-invalidate-all You call that on an image, and all caches on that image and on any downstream images are dropped. It also knocks any operations which depend on that image (directly or indirectly) out of the operation cache. It's not in pyvips (I've never needed it), so I suppose we should add |
I chewed on the bool question a bit more, and I changed my mind. So, bool arrays are now accepted and converted so that they follow the pyvips convention that True becomes 255. This means that round trips from a boolean calculation in pyvips to an array in numpy and then back to pyvips should preserve the 255. I modified the tests to reflect this. I also decided that by default the interpretation shouldn't be changed from whatever libvips would normally do on |
Having an |
OK, I'll make a PR for invalidate. |
useful if you're modifying image memory arrays outside libvips, eg. with numpy see #316
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! I left some small comments.
elif format in ['complex', 'dpcomplex']: | ||
interp = 'fourier' | ||
|
||
return interp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
libvips has something like this too:
https://www.libvips.org/API/current/libvips-header.html#vips-image-guess-interpretation
It looks at the current interpretation and tries to sanity check it. If it looks impossible, it attempts to guess a sane interpretation. If you set interpretation
to "error"
, you should trigger this sanitiser (I think).
Maybe we can combine the two sets of rules? You can see the one libvips uses here:
https://github.com/libvips/libvips/blob/master/libvips/iofuncs/header.c#L555-L721
* add invalidate() method useful if you're modifying image memory arrays outside libvips, eg. with numpy see #316 * add rob's invalidate test
…nto numpy_interface
Ok, here's a summary of the latest changes:
Additional notes:
|
This all looks great Rob. Let's merge and do any polishing in further PRs once we've had time to kick the tyres a bit. Nice job! |
and fix some small issues with the numpy integration PR see #316
I coaxed it through flake8, and it found a couple of small issues which I think I've fixed too. |
This patch pushes the new pyvips interpretation guess rules down into libvips. See libvips/pyvips#316
I've made the libvips interpretation guesser match your improved behaviour: libvips/libvips@57cd942 -- we should probably have the same behaviour everywhere, rather than just in pyvips. So for libvips >= 8.13 we could call into libvips for |
Slight tangent: I noticed the numpy and torch tests fail unless you have pretty recent versions. The ones that ship with ubuntu 21.10, for example, are too old. Perhaps either |
My implementation didn't do anything to ensure against stuff like specifying 'rgb' with single-band images while yours does, so as you mentioned mine could use another round of improvements too. I think at the moment the default behavior, in which |
Ah, interesting! I confess I only tested it with my setup. Is the test at fault or the code it's testing? I'll rework if you can let me know how it's failing. |
Sorry, brainfart, it was PIL and torch that failed. The torch test fails on |
I like the way the libvips guesser leaves interpretation alone unless it's obviously crazy. Perhaps we could add |
I noticed the docs need revising too, we have this section: https://github.com/libvips/pyvips/blob/master/doc/intro.rst#numpy-and-pil Which obviously needs reworking. Would you like to have a stab, or shall I? There's a PIL/numpy example in |
I'll have a go. |
Ok, here's my stab at robust and flexible numpy interoperability.
This PR contains the following:
Image.fromarray
that uses__array_interface__
or, if that's missing,__array__
to ingest numpy-like objects into pyvips. This includes some heuristics that try to set theinterpretation
to something sensible when the user passes'auto'
as theinterpretation
argument (the default; it can be easily set to any libvips interpretation by the user as desired.) This means that we can also ingest lots of foreign array-like objects (PIL images, e.g.).Image.__array__
to enablepyvips.Image
objects to be consumed by numpy. This enables explicit conversion likenp.asarray(image)
ornp.array(image)
, but also implicit conversion by numpy functions, such asnp.diff(image, axis=1)
. This also means that images can beplt.imshow
ed and that kind of stuff since functions in other scientific libraries often accept anything that can act like a numpy array.arr = mytensor.op1().op2().numpy()
, where the method chain ends with an explicit conversion to a numpy array. I have been using this for a couple years on my privatepyvips.Image
subclass and found it to be really handy and compact, so I addedImage.numpy()
too as a convenience function.Some points to note:
dtype
onImage.__array__
.__array_interface__
yet. A consequence is that PIL can't directly ingest pyvips Images without the user explicitly converting the image to a to numpy array first.Image.copy_memory()
and then diddle with the memory directly from numpy or to have a numpy array that gets zero-copy viewed by pyvips as an Image for saving or other operations would be super powerful. Is there a chance that such things would be feasible in the future? (I realize there is complexity associated with avoiding double-freeing memory, etc., but note that__array_interface__
allows offering memory as read-only if needed.)Sorry for the wall of text; adding this bit of functionality was more subtle than I expected.