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
Standardize function signature of compare_images. #7322
Conversation
I'm a little embarrassed because, with this change, existing code written as |
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.
Thanks for tackling this @mkcor. Especially the somewhat complex documentation of the deprecation. 😅
skimage/util/tests/test_compare.py
Outdated
img1[3:8, 3:8] = 255 | ||
img2 = np.zeros_like(img1) | ||
img2[3:8, 0:8] = 255 | ||
with pytest.warns(FutureWarning): |
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.
Might at least use match=".*image[01]
here and elsewhere.
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.
I'm not sure... I removed the case where input images are passed as kwargs following the new API (e0d5929), so we're not quite cycling over all image0-image1 combinations when expecting a warning. Or maybe my command of regular expressions is not up to the mark 😉
Co-authored-by: Lars Grüter <lagru+github@mailbox.org>
skimage/util/compare.py
Outdated
|
||
|
||
def compare_images(image1, image2, method='diff', *, n_tiles=(8, 8)): | ||
@_rename_image_params("image2", start_version="0.23", stop_version="0.25") | ||
def compare_images(image0, image1, image2=DEPRECATED, method='diff', *, n_tiles=(8, 8)): |
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.
I'm sorry but unfortunately I think looking at deprecate_parameter
has lead you on a wrong path and over-complicated things. And also breaks
a = np.ones(10)
b = np.zeros(10)
ski.util.compare_images(a, b, "blend")
# warns but still uses method="diff"
which used to work before and should continue working during the deprecation. I think we can't use an argument with =DEPRECATED
here.
Except for that I think you got quite close, especially with the idea to first turn all args
into kwargs
to more easily handle them. What do you think about the draft and decorator below? It should work for all cases unless I've forgotten a a way to abuse the signature. It also turns method
into a keyword-only parameter.
def _rename_image_params(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Turn all into keyword parameters
for i, (value, param) in enumerate(
zip(args, ["image0", "image1", "method", "n_tiles"])
):
if i >= 2:
warnings.warn("method will become keyword-only")
if param in kwargs:
raise ValueError(
f"{param} given as positional and keyword argument"
)
else:
kwargs[param] = value
args = tuple()
# Account for `image2` if given
if "image2" in kwargs:
warnings.warn("used deprecated image2")
# Safely move `image2` to `image1` if that's empty
if "image1" in kwargs:
# Safely move `image1` to `image0`
if "image0" in kwargs:
raise ValueError(
"three images given, `image0`, `image1` and `image2`"
)
kwargs["image0"] = kwargs.pop("image1")
kwargs["image1"] = kwargs.pop("image2")
return func(*args, **kwargs)
return wrapper
Of course it doesn't have the nice convenience stuff of modifying the docstring and warnings and error messages are drafts and need to be polished. But I think this deprecation is so special that we should just edit the docstring by hand.
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.
I'm sorry but unfortunately I think looking at
deprecate_parameter
has lead you on a wrong path and over-complicated things.
No worries! I did this quickly and thought I would iterate afterwards.
And also breaks
a = np.ones(10) b = np.zeros(10) ski.util.compare_images(a, b, "blend") # warns but still uses method="diff"which used to work before and should continue working during the deprecation.
Pardon me? I just tried locally and it works; it warns, indeed, and it returns
array([0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5])
just like current main. method="diff"
returns
array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
instead. I just noticed that method="checkerboard"
errors because a
and b
are 1-dimensional, which doesn't work with
scikit-image/skimage/util/compare.py
Line 48 in e453a53
shapex, shapey = img1.shape |
So we should probably improve the function by checking, in the method="checkerboard"
case, that ndim == 2 😬
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.
So we should probably improve the function by checking, in the
method="checkerboard"
case, that ndim == 2 😬
Done db1b774. If you prefer, I can cherry-pick this commit and submit it as a separate PR.
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.
Ah, you are right, it does handle this. I chose my example poorly and got confused by the image2=DEPRECATED
as the third positional arg.
... which brings me to the point that we should probably remove it and make it so that the function shows the expected new signature as un-ambiguously as possible.
skimage/util/compare.py
Outdated
if len(args) > 2 and args[len(args) - 1] in [ | ||
'diff', | ||
'blend', | ||
'checkerboard', | ||
]: |
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.
The wrapper shouldn't recognize the arg
for method
by value but by position. The function will/should handle if a wrong value is passed as method
.
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.
Yes, the function does handle it:
scikit-image/skimage/util/compare.py
Lines 59 to 62 in e453a53
raise ValueError( | |
'Wrong value for `method`. ' | |
'Must be either "diff", "blend" or "checkerboard".' | |
) |
Basically I wanted to cover both cases, where a user might call:
compare_images(a, b, 'diff')
and another one might call:
compare_images(a, b, c, 'diff')
which should definitely error -- but this should also be taken care of by the function, right? I realize that my RuntimeError("Use image0, image1
to pass the two input images.") isn't catching it anyway, it's only catching
compare_images(image0=a, image1=b, image2=c, ...)
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.
That's why I think we shouldn't use the value of the 3rd and 4th argument to determine which one is method
... just don't use image2=DEPRECATED
and we can simply use the position reliably? Right now it seems even harder to think of all edge cases. E.g. consider
import skimage as ski
a = np.ones((10, 10)) * 3
b = np.zeros((10, 10))
ski.util.compare_images(a, b, "unknown")
This will neither warn nor error but will simply return as if the user requested compare_images(a, b)
(default method=diff
). It should definitely error.
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.
Replying to your question: compare_images(a, b, c, 'diff')
should definitely error but it's not an expected use case of the old or new API so users shouldn't really encounter this case unless they try to mess around.
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.
Replying to your question:
compare_images(a, b, c, 'diff')
should definitely error but it's not an expected use case of the old or new API so users shouldn't really encounter this case unless they try to mess around.
Right; there's definitely no reason to 'handle' it within the function itself and perhaps not even through the decorator...
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.
That's why I think we shouldn't use the value of the 3rd and 4th argument to determine which one is
method
... just don't useimage2=DEPRECATED
and we can simply use the position reliably? Right now it seems even harder to think of all edge cases. E.g. considerimport skimage as ski a = np.ones((10, 10)) * 3 b = np.zeros((10, 10)) ski.util.compare_images(a, b, "unknown")This will neither warn nor error but will simply return as if the user requested
compare_images(a, b)
(defaultmethod=diff
). It should definitely error.
Ouch, yes. Maybe we should also check the args' types then? Let me try something based on your suggestion and ensure that yet this other edge case is covered.
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.
In the end, I was lazy and went for 9b9f757 (not trying to figure out what the third positional argument was meant to be...).
Sorry, @mkcor I don't want to come of as devaluing your approach. I'm happy with your solution as long as it gets all edge cases. :) There are a lot of edge cases to keep in mind and the longer I think about it the more I'm undecided which signature is actually least confusing to users during the deprecation. Is it (a) def compare_images(image0, image1, image2=DEPRECATED, method='diff', *, n_tiles=(8, 8)) because it's easy to see that Or is it (b) def compare_images(image0, image1, *, method='diff', n_tiles=(8, 8)) because this one makes it easy to see that |
All good, @lagru! I think my code wasn't very elegant, so I definitely welcome your inputs. ❤️ Now, maybe this kind of code doesn't have to be the most elegant anyway, since we will remove it two releases later? I haven't looked closely at your alternative yet, just because potential additional edge cases came up...
Exactly, that's the point.
Exactly, that's the main problem with this deprecation (more so than
This was my initial move, but you said (and rightfully so) that the old API (where def compare_images(image0, image1, method='diff', *, n_tiles=(8, 8)) ? |
Co-authored-by: Lars Grüter <lagru+github@mailbox.org>
Not sure we're covering all edge cases, but this implementation is definitely lighter! Thanks, @lagru. |
Co-authored-by: Lars Grüter <lagru+github@mailbox.org>
The previous default stacklevel wouldn't have shown a confusing origin of the warning to users.
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.
Co-authored-by: Lars Grüter <lagru+github@mailbox.org>
Co-authored-by: Lars Grüter <lagru+github@mailbox.org>
At yesterday's community call, @lagru and I were discussing whether we should merge this (skimage2-transition--related) PR, but we refrained so as not to interfere with the 0.23 release. @jarrodmillman could we merge it now? |
and reduce visual noise. The first versionchanged already informs users that image2 was deprecated.
|
||
|
||
@_rename_image_params | ||
def compare_images(image0, image1, *, method='diff', n_tiles=(8, 8)): | ||
""" | ||
Return an image showing the differences between two images. | ||
|
||
.. versionadded:: 0.16 |
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.
@mkcor I corrected a minor indent issue in 9a10712. Also, I'm thinking about how we can reduce the visual noise these kind of directives create in our docs. E.g. this directive I'm commenting on doesn't seem all that useful. We should probably reserve using them for breaking changes when they are absolutely necessary to avoid user confusion. And do we plan on removing them ever? Curious what you think. :)
Let's merge once the CI is green.
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.
Thanks, @lagru.
E.g. this directive I'm commenting on doesn't seem all that useful.
I guess it was useful back when the latest release was 0.16, 0.17, ... but it's losing this character of usefulness as time goes by, for sure.
We should probably reserve using them for breaking changes when they are absolutely necessary to avoid user confusion.
I guess it's fair to be informative, even if the information isn't technically 'necessary.' When is it information, when does it become noise...? I can hear that we want to strike the right balance.
I thought that, since these directives existed, they were probably very useful somewhere somewhat... 🙄
And do we plan on removing them ever?
I say we remove them in skimage2 😁
No idea about the new error yet:
But it seems unrelated since this PR doesn't touch any part of the infrastructure.. |
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.
Will merge once #7407 is sorted out and merged. Thanks!!
Description
This PR is a follow-up to #7255.
Checklist
./doc/examples
for new featuresRelease note
Summarize the introduced changes in the code block below in one or a few sentences. The
summary will be included in the next release notes automatically: