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 glass_blur for float dtype #826

Merged

Conversation

i-aki-y
Copy link
Contributor

@i-aki-y i-aki-y commented Jan 27, 2021

I found a bug about the GlassBlur (not GausssianBlur!) transform.
In this PR, I tried to fix the problem.
I would appreciate it if this PR is reviewed.

Problem

The GlassBlur transform returns wrong values when an input image's dtype is not uint8. This is caused by misimplementation of dtype conversion in the glass_blur function.

The following code is a reproducible example.
Note, the result of float32 case is expected to be non-zero.

import numpy as np
import albumentations

x_uint8 = np.zeros((5, 5)).astype(np.uint8)
x_uint8[2, 2] = 255

x_float32 = np.zeros((5, 5)).astype(np.float32)
x_float32[2, 2] = 1.0

glassblur = albumentations.augmentations.transforms.GlassBlur(always_apply=True, max_delta=1)

np.random.seed(13)
print(glassblur(image=x_uint8)["image"])

# [[ 6 10 10  3  1]
#  [14 23 18  7  5]
#  [25 36 20 11 14]
#  [14 22 14  9 10]
#  [ 6 11 14  7  5]]

np.random.seed(13)
print(glassblur(image=x_float32)["image"] * 255)

# [[0. 0. 0. 0. 0.]
# [0. 0. 0. 0. 0.]
# [0. 0. 0. 0. 0.]
# [0. 0. 0. 0. 0.]
# [0. 0. 0. 0. 0.]]

Fix

I change the following two lines.

  1. The output of the cv2.GaussianBlur should be multiplied by 255 (max uint8) instead of coef (max of input image dtype).
+ x = np.uint8(cv2.GaussianBlur(np.array(img) / coef, sigmaX=sigma, ksize=(0, 0)) * coef)
- x = np.uint8(cv2.GaussianBlur(np.array(img) / coef, sigmaX=sigma, ksize=(0, 0)) * 255)
  1. The transformed x values whose dtype is uint8 should be divided by 255 instead of coef before passed to the cv2.GaussianBlur.
+ return np.clip(cv2.GaussianBlur(x / coef, sigmaX=sigma, ksize=(0, 0)), 0, 1) * coef
- return np.clip(cv2.GaussianBlur(x / 255, sigmaX=sigma, ksize=(0, 0)), 0, 1) * coef

After the fix, I got the following results.

import numpy as np
import albumentations

x_uint8 = np.zeros((5, 5)).astype(np.uint8)
x_uint8[2, 2] = 255

x_float32 = np.zeros((5, 5)).astype(np.float32)
x_float32[2, 2] = 1.0

glassblur = albumentations.augmentations.transforms.GlassBlur(always_apply=True, max_delta=1)

np.random.seed(13)
print(glassblur(image=x_uint8)["image"])

# [[ 6 10 10  3  1]
#  [14 23 18  7  5]
#  [25 36 20 11 14]
#  [14 22 14  9 10]
#  [ 6 11 14  7  5]]


np.random.seed(13)
print(glassblur(image=x_float32)["image"] * 255)

# [[ 6.2885346 10.978549  10.776732   3.9743576  1.4602503]
#  [14.964603  23.983921  18.80205    7.6914196  5.5994816]
#  [25.221247  36.9323    20.978346  11.711028  14.315237 ]
#  [14.775828  22.008005  14.221998   9.483424  10.28976  ]
#  [ 6.3716593 11.8987875 14.053254   7.8504953  5.2329097]]

* Fix dtype conversion code in glass_blur
* Add a test case for above fix
Copy link
Collaborator

@Dipet Dipet 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 very much! These are important fixes.
But could you a little bit rewrite your PR?

@@ -1514,7 +1514,7 @@ def fancy_pca(img, alpha=0.1):
@clipped
def glass_blur(img, sigma, max_delta, iterations, dxy, mode):
coef = MAX_VALUES_BY_DTYPE[img.dtype]
x = np.uint8(cv2.GaussianBlur(np.array(img) / coef, sigmaX=sigma, ksize=(0, 0)) * coef)
x = np.uint8(cv2.GaussianBlur(np.array(img) / coef, sigmaX=sigma, ksize=(0, 0)) * 255)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Better to simple remove conversion into uint8 and convert to image data type

x = cv2.GaussianBlur(np.array(img) / coef, sigmaX=sigma, ksize=(0, 0)) * coef
x = x.astype(img.dtype)

Copy link
Collaborator

Choose a reason for hiding this comment

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

@creafz @BloodAxe @ternaus Here is a little bit strange implementation. Maybe we will remove normalization before applying blur? After these changes transform will work fine, but the result may differ from this implementation by 2 abs(old_result - new_result).max() <= 2

Copy link
Contributor

Choose a reason for hiding this comment

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

@Dipet, I agree with removing this normalization. I assume that it was used in the original implementation of glass_blur (https://github.com/hendrycks/robustness/blob/master/ImageNet-C/create_c/make_imagenet_c.py#L300) because skimage works mostly with float images that have values in the range [0.0, 1.0].

albumentations/augmentations/functional.py Outdated Show resolved Hide resolved
albumentations/augmentations/functional.py Outdated Show resolved Hide resolved
@i-aki-y
Copy link
Contributor Author

i-aki-y commented Jan 27, 2021

@Dipet Thank you for your quick reply.

But could you a little bit rewrite your PR?

Of course! Now, I updated my PR.

@creafz @BloodAxe @ternaus Here is a little bit strange implementation. Maybe we will remove normalization before applying blur? After these changes transform will work fine, but the result may differ from this implementation by 2 abs(old_result - new_result).max() <= 2

I modified the test case to allow for the difference between uint8 and float at most 2.

If any fix is needed about this issue, please let me know.

def glass_blur(img, sigma, max_delta, iterations, dxy, mode):
coef = MAX_VALUES_BY_DTYPE[img.dtype]
x = np.uint8(cv2.GaussianBlur(np.array(img) / coef, sigmaX=sigma, ksize=(0, 0)) * coef)
x = cv2.GaussianBlur(np.array(img) / coef, sigmaX=sigma, ksize=(0, 0)) * coef
Copy link
Collaborator

@Dipet Dipet Feb 1, 2021

Choose a reason for hiding this comment

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

Can you remove normalization by coef?
With these changes we also don't need normalize x in the and and clip it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Dipet OK, I see your point.

Can I ask for some advice about the test case?
In the first PR version, I added a test case to check the transformed values of uint8, float32 images are approximately the same.
My intention against the test case is to confirm some dtype operations are done appropriately.
Thanks to your review, now the code has no explicit dtype operations, so I think the test case becomes less effective.
Is it better to remove my test case?
If there is any idea about this issue’s test case, let me know?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it is good that we have this test.
In my opinion all transforms in the library must have similar tests to check that transforms work the same for uint8 and float datatypes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, I see. Thanks for the comment.

@Dipet Dipet requested review from creafz, BloodAxe and ternaus February 2, 2021 04:52
@Dipet Dipet merged commit cf688e4 into albumentations-team:master Feb 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants