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

fix clip bug in resize when anti_aliasing is applied (#5202) #5209

Merged
merged 35 commits into from
Nov 22, 2021

Conversation

abouysso
Copy link
Contributor

@abouysso abouysso commented Jan 29, 2021

Description

Clipping was performed based on the values of the filtered image and not
the input image, which can produce values outside of input values range.

Added clarification on kwarg anti_aliasing description: it is applied by
default for non bool type inputs when downsizing.

Closes #5202

Checklist

For reviewers

  • Check that the PR title is short, concise, and will make sense 1 year
    later.
  • Check that new functions are imported in corresponding __init__.py.
  • Check that new features, API changes, and deprecations are mentioned in
    doc/release/release_dev.rst.

clipping was performed based on the values of the filtered image and not 
the input image, which can produce values outside of input values range.

Added clarification on kwarg anti_aliasing description: it is applied by 
default for non bool type inputs when downsizing.
Copy link
Member

@rfezzani rfezzani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @abouysso, I think that the 2D case is not fixed with the proposed approach :(
Can you please also add a test?

@@ -172,12 +174,12 @@ def resize(image, output_shape, order=None, mode='reflect', cval=0, clip=True,
tform.params[0, 1] = 0
tform.params[1, 0] = 0

out = warp(image, tform, output_shape=output_shape, order=order,
out = warp(img_in, tform, output_shape=output_shape, order=order,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed solution doesn't work in this case since the clipping is made in warp 😕

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out ! Fixed it in fallback code by performing clipping outside of warp function 😃.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent idea! The _clip_warp_output call can be performed outside the if ... elif ... else ... block once for all now 😉

skimage/transform/_warps.py Outdated Show resolved Hide resolved
@rfezzani rfezzani added the 🩹 type: Bug fix Fixes unexpected or incorrect behavior label Jan 29, 2021
Also removed unnecessary copy of the input data.
@abouysso abouysso marked this pull request as draft January 29, 2021 15:23
@abouysso
Copy link
Contributor Author

abouysso commented Jan 29, 2021

I ran in another issue, also related to potential out of input range values produced by ndi.gaussian_filter.
When kwarg order is set to 0 (nearest neighbors interpolation), no clipping is performed since such interpolation does not produce out of range data. Thus it seems to me that _clip_warp_output behavior needs to be modified in order to be able to handle such cases.

I see two ways of doing so:

  • perform clipping as long as clip is True, even if order is 0.
  • add anti_aliasing as boolean kwarg to the _clip_warp_output function, and perform clipping if clip is True and either order != 0 or anti_aliasing

This allows _clip_warp_output to clip out of input range values when 
caused by gaussian filter and not interpolation function.
@pep8speaks
Copy link

pep8speaks commented Jan 30, 2021

Hello @abouysso! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

Line 152:49: W504 line break after binary operator

Line 385:54: E225 missing whitespace around operator

Comment last updated at 2021-11-21 23:29:06 UTC

@abouysso abouysso marked this pull request as ready for review January 30, 2021 17:45
np.asarray(output_shape, dtype=float))
factors = np.divide(input_shape, output_shape)
# create copy so input values range stays accessible through image for clip
img_in = image
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see a case where this approach doesn't work: when image dtype is not floating point... In the case of uint8 for example, values will not be clipped between 0 and 1 as it is supposed to be...
We must call convert_to_float here to fix this!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed there could still be an issue when preserve_range is False 😄 Otherwise values will be clipped between 0 and 255 as expected. I will correct this as you suggested !

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, personally I dislike reassigning values in the middle of a function, so, rather than copy image to image_in then overwriting image, how about just renaming the downstream images? ie below, use image_smoothed = ndi.gaussian_filter(image, ...). What do you think @abouysso?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's in fact an option, but you need to define image_smoothed even when anti_aliasing is False in this case

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I instead created img_bounds to save input image bounds for later clipping... Though?

Clipping is performed w.r.t the values of image. In case preserve_range 
is False, for uint8 data for example, the reference can be outdated and 
produce unexpected behavior of clipping.
Because data types checks are performed, a new variable input_type is 
defined to store input dtype.
@rfezzani
Copy link
Member

rfezzani commented Feb 1, 2021

You forgot to import pytest, otherwise it looks really good! Thank you @abouysso 👌

Copy link
Member

@rfezzani rfezzani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @abouysso, it looks good! @scikit-image/core, the PR is ready for review 😉. CI failures seems unrelated.

Copy link
Member

@stefanv stefanv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the patch, @abouysso!

@rfezzani's review comments look good, and I left one additional one.

skimage/transform/_warps.py Outdated Show resolved Hide resolved
@rfezzani
Copy link
Member

@scikit-image/core, can anyone please find some time to review this, thanks 😉

skimage/transform/_warps.py Outdated Show resolved Hide resolved
skimage/transform/_warps.py Outdated Show resolved Hide resolved
skimage/transform/_warps.py Outdated Show resolved Hide resolved
skimage/transform/_warps.py Outdated Show resolved Hide resolved
skimage/transform/_warps.py Outdated Show resolved Hide resolved
abouysso and others added 5 commits April 14, 2021 09:55
down-sizing now used consistently in resize function, so its clearer 
than previous mix between down-sizing, down-sampling and down-scaling.
Co-authored-by: Marianne Corvellec <marianne.corvellec@ens-lyon.org>
@mkcor
Copy link
Member

mkcor commented Apr 19, 2021

Hi @abouysso,

Merging #5327 has led to a conflict in file skimage/transform/_warps.py for this PR. Are you comfortable resolving it (either locally in your file editor or in GitHub's online editor)?

@abouysso
Copy link
Contributor Author

Hi @abouysso,

Merging #5327 has led to a conflict in file skimage/transform/_warps.py for this PR. Are you comfortable resolving it (either locally in your file editor or in GitHub's online editor)?

Thanks for pointing this out ! I'm working on it, conflict is solved locally, but new errors are raised on pytest. A quick look seems to indicate those errors comes from test functions modified in #5327 . I still have to find the issue 😄

This allows to give the expected dtype as input of ndi.gaussian_filter 
when anti-aliasing is performed
Copy link
Member

@jni jni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abouysso apologies that it has taken so long to to review this... Our team members have been rather overwhelmed this year. I've left a couple of comments but this should be ready very quickly! Do you have the time/inclination to look at my comments, or would you rather we take over from here?

Thank you!

skimage/transform/_warps.py Outdated Show resolved Hide resolved
skimage/transform/_warps.py Outdated Show resolved Hide resolved
skimage/transform/_warps.py Outdated Show resolved Hide resolved
skimage/transform/_warps.py Outdated Show resolved Hide resolved
@@ -133,17 +139,13 @@ def resize(image, output_shape, order=None, mode='reflect', cval=0, clip=True,
warn("Anti-aliasing standard deviation greater than zero but "
"not down-sampling along all axes")
image = ndi.gaussian_filter(image, anti_aliasing_sigma,
cval=cval, mode=ndi_mode)
cval=cval, mode=ndi_mode)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cval=cval, mode=ndi_mode)
cval=cval, mode=ndi_mode)

np.asarray(output_shape, dtype=float))
factors = np.divide(input_shape, output_shape)
# create copy so input values range stays accessible through image for clip
img_in = image
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, personally I dislike reassigning values in the middle of a function, so, rather than copy image to image_in then overwriting image, how about just renaming the downstream images? ie below, use image_smoothed = ndi.gaussian_filter(image, ...). What do you think @abouysso?

rfezzani and others added 5 commits May 21, 2021 09:55
Co-authored-by: Juan Nunez-Iglesias <juan.nunez-iglesias@monash.edu>
Co-authored-by: Juan Nunez-Iglesias <juan.nunez-iglesias@monash.edu>
Co-authored-by: Juan Nunez-Iglesias <juan.nunez-iglesias@monash.edu>
Co-authored-by: Juan Nunez-Iglesias <juan.nunez-iglesias@monash.edu>
@rfezzani
Copy link
Member

@abouysso, I resolved the merge conflicts. Can you please address @jni comments if you have any bandwidth for this 😉

@rfezzani rfezzani added this to the 0.19 milestone Nov 18, 2021
@rfezzani
Copy link
Member

rfezzani commented Nov 18, 2021

@scikit-image/core, I added this PR to 0.19 milestone because it is close and think it fixes skimage.transform.resize which is a sensible function.
@abouysso, do you have bandwidth to work on this soon, otherwise I can take over if you wish 😉

@grlee77 grlee77 merged commit 8f3f49e into scikit-image:main Nov 22, 2021
@@ -220,7 +220,6 @@ def resize(image, output_shape, order=None, mode='reflect', cval=0, clip=True,
preserve_range=preserve_range)

else: # n-dimensional interpolation
order = _validate_interpolation_order(input_type, order)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch @grlee77

@rfezzani
Copy link
Member

🎉 Thank you @grlee77

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🩹 type: Bug fix Fixes unexpected or incorrect behavior
Projects
None yet
Development

Successfully merging this pull request may close these issues.

transform.resize uses anti_aliasing as default and fails to correctly clip values when anti_aliasing is used
7 participants