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 zero division error in SIFT #7069

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

hayatoikoma
Copy link
Contributor

@hayatoikoma hayatoikoma commented Jul 18, 2023

Description

I encountered a math domain error at cos in _rotate because _fit returned inf with float32 due to numerical precision.

(h[0] - h[2]) / (2 * (h[0] + h[2] - 2 * h[1]))

In the failed case,h[0] was slightly smaller than h[1] but h[1] and h[2] is exactly the same. Arithmetically, the divisor should not become zero, but it became exactly zero due to numerical precision.

Release note

Handle zero division in SIFT.

@@ -429,7 +429,7 @@ def _find_localize_evaluate(self, dogspace, img_shape):

def _fit(self, h):
"""Refine the position of the peak by fitting it to a parabola"""
return (h[0] - h[2]) / (2 * (h[0] + h[2] - 2 * h[1]))
return (h[0] - h[2]) / (2 * ((h[0] - h[1]) + (h[2] - h[1])))
Copy link
Contributor Author

@hayatoikoma hayatoikoma Jul 18, 2023

Choose a reason for hiding this comment

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

Arithmetically, it should be exactly the same, but this change prevents "zero" most of the time if not all because h[0] > = h[1] and h[2] >= h[1] are guaranteed and either of them should not hold equality.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense. Thanks for reporting. Would you mind adding a comment above this line?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I can add a comment.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, I'll wait for your new commit.

@hayatoikoma hayatoikoma marked this pull request as ready for review July 18, 2023 06:04
@@ -429,7 +429,7 @@ def _find_localize_evaluate(self, dogspace, img_shape):

def _fit(self, h):
"""Refine the position of the peak by fitting it to a parabola"""
return (h[0] - h[2]) / (2 * (h[0] + h[2] - 2 * h[1]))
return (h[0] - h[2]) / (2 * ((h[0] - h[1]) + (h[2] - h[1])))
Copy link
Member

Choose a reason for hiding this comment

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

Makes sense. Thanks for reporting. Would you mind adding a comment above this line?

@@ -519,6 +519,8 @@ def _compute_orientation(self, positions_oct, scales_oct, sigmas_oct,
# result
ori = ((m + self._fit(hist[neigh]) + 0.5)
* 2 * np.pi / self.n_bins)
if not np.isfinite(ori):
continue
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't the exception be handled explicitly (i.e., with a try statement and except ZeroDivisionError)? And within the _fit function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's say if we handle it by exception within _fit. What do you want to return by _fit when we catch the exception?

Copy link
Member

Choose a reason for hiding this comment

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

I admit I would let it error, with a helpful message though: Maybe suggesting that the user pass h with a different precision, to circumvent the ZeroDivisionError?

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, sorry, I guess h is not really 'passed by the user' at this point; it's usually an intermediate result in another private function... Maybe try returning h[0] - h[2]) / (2 * ((h[0] - h[1]) + (h[2] - h[1])) as you wrote, and if this fails return h[0] - h[2]) / (2 * (h[0] + h[2] - 2 * h[1]) as originally, so we have better chances overall...? And handle the exception in _compute_orientation instead?

Copy link
Contributor Author

@hayatoikoma hayatoikoma Sep 26, 2023

Choose a reason for hiding this comment

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

Sorry for the late reply. I was too busy to take a look on it.

Maybe try returning h[0] - h[2]) / (2 * ((h[0] - h[1]) + (h[2] - h[1])) as you wrote, and if this fails return h[0] - h[2]) / (2 * (h[0] + h[2] - 2 * h[1])

I don't think it increases the chance. But, in any case, I guess it could be h[0] == h[1] == h[2] because

In [2]: ndi.maximum_filter([1., 1., 1.], [3], mode='wrap')
Out[2]: array([1., 1., 1.])

So, I think ori could be nan and inf, and isfinite can handle both cases.

And handle the exception in _compute_orientation instead?

Actually, my PR description was inaccurate. The zero division was actually happening, but the error happened at this line with ValueError: math domain error because angle became inf. (Maybe better to replace cos and sin to numpy's function.)

So, unless we cast each element of h to python built-in type, we can't catch it as ZeroDivisionError because h is a numpy array.

By the way, I have two more PRs open for fixing bugs of SIFT (#7065 and #6862). So, it would be great if you can review them too.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, my PR description was inaccurate.

No worries; feel free to edit your PR's title.

Actually, my PR description was inaccurate. The zero division was actually happening, but the error happened at this line with ValueError: math domain error because angle became inf.

Thanks for investigating, @hayatoikoma. So, I think that your addition line 522 would be a good enough fix... How about reverting your change line 432?

(Maybe better to replace cos and sin to numpy's function.)

I agree.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it should handle both, but why do you want to revert line 432?

Copy link
Member

Choose a reason for hiding this comment

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

Can you please add a test? Maybe you could write a toy version of your use case? So it breaks with current main, and passes with your proposed changes.

Yes, it should handle both

Sorry, I don't get it: both what?

but why do you want to revert line 432?

Let's not reverse the burden of proof... 😉 Unless you are fixing two different things and I didn't get it. I was under the impression that the change line 522 was enough because it handles the case when the angle becomes inf or nan. If you are changing line 432 as well, please add a commend and/or test for it. Thanks for following through!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thinking about it again, I thought I should just handle when the denominator becomes zero. Then, it should handle the cases I reported. What do you think?

@mkcor mkcor added the 🩹 type: Bug fix Fixes unexpected or incorrect behavior label Sep 7, 2023
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.

None yet

2 participants