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
PyDIP, where should it go from here? (pt 2) #105
Comments
|
One issue I found confusing when trying out example 1Assume I want to do Gaussian filtering of a 3D volume via
example 2Suppose I want to perform morphological operations with a rectangular structuring element of shape (5, 9). In scikit-image, I could define the structuring element via However, in DIPlib, to get identical output with a rectangular structuring element I would need to create the structuring element using proposed solutionShould these function's wrappers detect if the input is a NumPy array and reverse the entries of tuples like this automatically? That would seem to be a more user-friendly approach. Otherwise, I suspect it is likely that many users of the library will make these types of mistakes. I hope this feedback is helpful to the team! |
In case it is of interest, I came across the examples above when I was prototyping possibilities for offloading There are some examples and benchmarks for a small number of functions in this demo project's README. You will see that |
@grlee77 Thanks for your thoughts! I see your point, and agree it would be convenient if things matched up. I'm not sure if we can make your proposed solution work without significant changes to the bindings. We currently take as input any object with a buffer protocol, the bindings automatically create a Though there might be some simple solution that I can't think of right now. Will keep thinking about this! Here's my reasons for reversing the dimensions of the NumPy arrays: |
It seems the main problem is import/export and visualization. Can we make a global option that instructs those functions to interpret the axes differently? For the viewer at least it’s just a matter of changing the default axes and their names. Of course, the fact that numpy arrays would have non-normal strides when converted to images and vice-versa would probably affect performance. |
There are some DIPlib functions that process the image in order of coordinates (i.e. increasing x first), but most functions should work equally fast no matter what the dimension order is. Remember that in MATLAB we are always dealing with images with non-standard strides, so this has always been important to get right. So this would require:
Am I missing something? This seems like a very doable proposition. |
Good to know about the performance! I was hoping that would be the case. Do you want the option to be specific to PyDIP, or also available for users of the C++ library? In the latter case, the C++ functions themselves would do the translation. |
Thanks for looking into the possibilities here. It sounds like perhaps there is a solution that won't be too difficult from an implementation and maintenance standpoint. If it turns out to be harder than expected, just making an explanation of the current behavior more explicit in the PyDIP docs would help. |
@wcaarls I was thinking of having this specifically in the Python interface. Didn't think about C++. Is there a use case for it in C++? Also, that would require adding a global variable to DIPlib, which I'm not keen on (I removed all the globals that DIPlib 2 had, we only have the OpenMP number of threads as a per-thread global variable right now). |
@crisluengo No use case that I know of, but it impacts how I implement it in the viewer. If it is PyDIP only it needs to be a viewer option, whereas if it is a DIPlib variable the viewer can query it by itself. I'll make a viewer option then. |
@wcaarls It should be easy to reorder dimensions in the function in the bindings, see dip::Image tmp = input; // because input is likely a const&, we can't and don't want to change it
dip::uint ndims = tmp.Dimensionality();
dip::UnsignedArray order(ndims);
std::generate(order.begin(), order.end(), [& ndims]{ return --ndims; });
tmp.PermuteDimensions(order); Maybe we can add this as a member function for |
@wcaarls I have pushed the changes for @grlee77 In the next release, calling |
I'd like to change the I guess we can tell people to do dip.Image.__mul__ = dip.Image.__matmul__
dip.Image.__imul__ = dip.Image.__imatmul__ to revert to the old behavior? Like I did with the The Python bindings have lots of awkward things that are hard to fix without breaking existing scripts... @wcaarls What do you think? |
@crisluengo Although inconvenient, I think pydip should be treated as a moving target for now, since we don't have a clear idea of where it should go. So I don't think we should get hung up too much on breaking changes, as long as they are a definite improvement and are announced clearly. I think the proposed change qualifies. |
@wcaarls I think that's a good policy, it will make it easier to make improvements. I've been doing version numbers like this:
Obviously this refers to the C++ library only, not anything else. Would it be OK to increase only It's kinda awkward having a single version number for all these different components. I thought it was convenient putting it all in the same project, but maybe it's not? |
Do you see the 3.x.y as major.minor.patch (semver.org) or epoch.major.minor? In general, people will assume the first. Theoretically, breaking changes require a major version bump, but we can treat PyDIP as a de facto development release and only bump the minor version. I do think the patch version (if any) should be reserved for bugfixes. Separate component versions will probably be more confusing. I don't think 3.42.0 is too problematic. |
I've been doing {incompatible}.{API compatible},{ABI compatible}, but that is explicitly related to the DIPlib binary. And we haven't really had a 3.x.1 yet, so I don't think I got to explain the version scheme anywhere yet.
Let's do it that way then. Whatever is least surprising. :) I'll add some words on the website and README to indicate PyDIP is still in development and might change in incompatible ways. People should pin their version number and read release notes before upgrading. Thanks! |
I've been pointed to the I've made a simple implementation (it's not yet preserving pixel sizes, but does preserve tensor information), this is what it can do: >>> import diplib as dip
>>> import numpy as np
>>> a = dip.ImageRead('examples/DIP.tif')
>>> a
<Color image (3x1 column vector, 3 elements, sRGB), UINT8, sizes {256, 256}>
>>> np.sum(a)
<Scalar image, UINT64, 0D>
>>> np.sum(a, axis=1)
<Color image (3x1 column vector, 3 elements, sRGB), UINT64, sizes {256}>
>>> np.sum(a, axis=2)
<Scalar image, UINT64, sizes {256, 256}> Maybe I can get it to return the input object if it's a scalar, I don't yet know how to do this. Implementation: Lines 296 to 312 in 92071b2
@wcaarls What do you think of this? What behavior should it have? Maybe this is not even desirable? |
Interesting! As an implementer I would not have immediately expected this behavior, but I could easily see a user wanting this. I'm a bit worried about the guessing of array dimensions, though. Would it be possible to use |
Yeah, I have been thinking about that. I'm guessing you'd need to know about all the ufuncs, and possibly check their parameters, to know what the output sizes will be. I don't think that is something we want to maintain. I agree I don't like guessing at dimensions. A 3-channel image that has one dimension of size 3 might produce totally unexpected and wrong results with the code I wrote. I'm not sure this can ever be done properly, but it was fun playing with for a while. :) I actually don't mind the symmetry of the current situation, where calling a NumPy function on an image returns an array, and calling a DIPlib function on an array returns an image. Your "I would not have immediately expected this behavior" cements that. Thanks! |
On the other hand, this will work: a = dip.ImageRead('examples/trui')
b = dip.Image((256,), 1, "SFLOAT")
np.sum(a, out=np.asarray(b), axis=1) BTW: I'm working to add bindings for the version of each function that has b = np.zeros((256,256), dtype=np.float64)
dip.Gauss(a, out=b, sigmas=1) (And of course with a It's a bit of work, and it adds a lot of duplication to the PyDIP source files, but it is very useful being able to specify an output buffer, or being able to some functions in-place. |
I'm not sure we need to know this on a per-ufunc basis. From a cursory glance, it seems their arguments that affect the output size are quite well-defined, particularly their methods. |
Unrelated to the above. I have just found these two projects:
|
Issue #25 was getting too long, I added a recap at the end. Here's a clean slate to continue the process.
We need lots of usage, lots of feedback from users, and lots of time polishing the interface.
But ideally we'd take this to a different level, in a way similar to what DIPimage is. For example, we could write a GUI as exists in DIPimage. And maybe make the module more "Pythonic" by changing function names.
We also need to find a way to automatically generate documentation from Doxygen. One approach is here.
The text was updated successfully, but these errors were encountered: