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

Ellipsis fitting: Check the condition number before computiing eigen vectors #3103

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

Conversation

hmaarrfk
Copy link
Member

@hmaarrfk hmaarrfk commented May 24, 2018

Under certain conditions, the result of a matrix we have to invert becomes ill conditioneed. The 32 bit solver and 64 bit solver converge on different answers.
We first check if that matrix is ill conditioned before we move forward with the computation.

See #3091

Checklist

[It's fine to submit PRs which are a work in progress! But before they are merged, all PRs should provide:]

[For detailed information on these and other aspects see scikit-image contribution guidelines]

References

[If this is a bug-fix or enhancement, it closes issue # ]
[If this is a new feature, it implements the following paper: ]

For reviewers

(Don't remove the checklist below.)

  • 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.

@pep8speaks
Copy link

pep8speaks commented May 24, 2018

Hello @hmaarrfk! Thanks for updating the PR.

Line 238:80: E501 line too long (83 > 79 characters)
Line 241:80: E501 line too long (82 > 79 characters)
Line 242:80: E501 line too long (81 > 79 characters)
Line 243:80: E501 line too long (80 > 79 characters)

Comment last updated on September 20, 2018 at 03:00 Hours UTC

@hmaarrfk hmaarrfk force-pushed the bugfix_linalg_error_ellipsis_model branch 2 times, most recently from 9fd7a12 to 4d40a5d Compare May 24, 2018 21:32
@codecov-io
Copy link

codecov-io commented May 24, 2018

Codecov Report

Merging #3103 into master will decrease coverage by 0.09%.
The diff coverage is 100%.

Impacted file tree graph

@@           Coverage Diff            @@
##           master   #3103     +/-   ##
========================================
- Coverage   86.79%   86.7%   -0.1%     
========================================
  Files         341     341             
  Lines       27444   27429     -15     
========================================
- Hits        23820   23782     -38     
- Misses       3624    3647     +23
Impacted Files Coverage Δ
skimage/measure/tests/test_fit.py 100% <100%> (ø) ⬆️
skimage/viewer/qt.py 25.71% <0%> (-62.86%) ⬇️
skimage/viewer/__init__.py 80% <0%> (-20%) ⬇️
skimage/segmentation/random_walker_segmentation.py 92.01% <0%> (-1.88%) ⬇️
skimage/draw/_random_shapes.py 95.78% <0%> (-1.06%) ⬇️
skimage/feature/tests/test_register_translation.py 100% <0%> (ø) ⬆️
skimage/feature/register_translation.py 100% <0%> (+6.41%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6af7e2e...6ce1e2e. Read the comment docs.

@@ -434,6 +434,11 @@ def estimate(self, data):
except np.linalg.LinAlgError: # LinAlgError: Singular matrix
return False

# For some reason the above doesn't catch the error on linux 32 bit
# https://stackoverflow.com/questions/13249108/efficient-pythonic-check-for-singular-matrix
if np.linalg.cond(M) > 1 / np.finfo(np.double).eps:
Copy link
Member

Choose a reason for hiding this comment

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

This check can be done before the inversion is attempted? It simplifies the logic a bit.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure. in fact, when I computed cond(M) in the debugger with import pdb; pdb.set_trace(), the condition number was already Inf. I don't really want to cause other overflow errors or "old numpy specific errors"

From the docs

eps | (float) The smallest representable positive number such that 1.0 + eps != 1.0. Type of eps is an appropriate floating point type.

They don't seem to have a eps^-1. Thought I could do
eps**-1 if you like.

@stefanv
Copy link
Member

stefanv commented May 24, 2018

It seems like the test suite already fails, but it may be worth inserting an explicit test for this, potentially one that also fails on 64-bit.

@matthew-brett
Copy link
Contributor

Should this be a bug report to numpy too?

@stefanv
Copy link
Member

stefanv commented May 24, 2018

@matthew-brett I've raised this with the NumPy folks; they think it's not NumPy's responsibility.

Overall, it is a bad idea to use inverse if you can use solve. Could we rewrite the solution here to do solves instead?

@hmaarrfk
Copy link
Member Author

hmaarrfk commented May 24, 2018

Just for you two, I actually stepped through to figure out what was happening.

S1 and C1 are well conditioned. It is the result of the matrix multiplications and addition that is illconditionned. Typically this is caught a little lower

        # M*|a b c >=l|a b c >. Find eigenvalues and eigenvectors
        # from this equation [eqn. 28]
        eig_vals, eig_vecs = np.linalg.eig(M)

        # eigenvector must meet constraint 4ac - b^2 to be valid.
        cond = 4 * np.multiply(eig_vecs[0, :], eig_vecs[2, :]) \
               - np.power(eig_vecs[1, :], 2)
        a1 = eig_vecs[:, (cond > 0)]
        # seeks for empty matrix
        if 0 in a1.shape or len(a1.ravel()) != 3:
            return False

On 64 bit:

(Pdb) cond
array([-0.28717554, -0.71271829, -0.00108459])

On 32 bit

(Pdb) cond
array([-1.38708836,  0.84396406, -0.03172746])

It just happened that on 64 bit the error didn't show up. But I don't think that the eig algorithm will complain if you have a ill-conditionned matrix.

Therefore, the tests in both 64 bit and 32 bit are caught by my added logic.

@hmaarrfk hmaarrfk changed the title Fixes one bug on a 32 bit machine not caught by np Ellipsis fitting: Check the condition number before computiing eigen vectors May 24, 2018
@hmaarrfk
Copy link
Member Author

Technically, since the matrix is so small, I think we can simply check that if one eig_vals is small and also return False.

Anyway, I think checking the condition number is more "mathematical"

@hmaarrfk
Copy link
Member Author

@stefanv. I probably agree that doing something like finding tge best eigen value is probably verbose.

From ‘numpy.linalg.solve’

a must be square and of full-rank

We would need to check the condition number of the matrix anyway otherwise risk having an other linalg error

@hmaarrfk
Copy link
Member Author

Finally @stefanv. I quickly skimmed the paper and the code seems to be following the formalism the paper is using.

I suggest that we leave the reformulation to somebody that wants to derive it. (It May be a paper for itself)

@hmaarrfk hmaarrfk force-pushed the bugfix_linalg_error_ellipsis_model branch 5 times, most recently from e2b7248 to d8c098c Compare June 19, 2018 02:06
@emmanuelle
Copy link
Member

So, should we merge this PR?

@hmaarrfk
Copy link
Member Author

hmaarrfk commented Sep 1, 2018

I think so! I can rebase if you want.

@hmaarrfk hmaarrfk force-pushed the bugfix_linalg_error_ellipsis_model branch from d8c098c to e81aadb Compare September 1, 2018 22:37
@jni
Copy link
Member

jni commented Sep 19, 2018

@hmaarrfk I can't quite tell from the conversation whether you have a test case that will fail on 64 bit? It would be nice for this to be tested.

@hmaarrfk
Copy link
Member Author

Sorry, I guess I coudln't think of one. I'll try to bring out my pen and paper this weekend. Remind me.

@hmaarrfk hmaarrfk force-pushed the bugfix_linalg_error_ellipsis_model branch 2 times, most recently from 5381ec2 to 3eee483 Compare September 20, 2018 00:49
@hmaarrfk
Copy link
Member Author

@jni

=============================================== FAILURES ================================================
__________________________________ test_ellipse_model_estimate_failers __________________________________

    def test_ellipse_model_estimate_failers():
        # estimate parameters of real data
        model = EllipseModel()
        assert not model.estimate(np.ones((5, 2)))
        # Before PR https://github.com/scikit-image/scikit-image/pull/3103
        # This next line would return True on 32bit
        assert not model.estimate(np.array([[50, 80], [51, 81], [52, 80]]))
        # This is the same test that causes the model to
        # return true on 64 bit prooving that this was also an issue on
        # 64 bit arch
>       assert not model.estimate(np.array([[50, 80], [51, 81], [52, 80.00000000001]]))
E       assert not True
E        +  where True = <bound method EllipseModel.estimate of <skimage.measure.fit.EllipseModel object at 0x7f422d07e5c0>>(array([[ 50.,  80.],\n       [ 51.,  81.],\n       [ 52.,  80.]]))
E        +    where <bound method EllipseModel.estimate of <skimage.measure.fit.EllipseModel object at 0x7f422d07e5c0>> = <skimage.measure.fit.EllipseModel object at 0x7f422d07e5c0>.estimate
E        +    and   array([[ 50.,  80.],\n       [ 51.,  81.],\n       [ 52.,  80.]]) = <built-in function array>([[50, 80], [51, 81], [52, 80.00000000001]])
E        +      where <built-in function array> = np.array

skimage/measure/tests/test_fit.py:233: AssertionError
================================== 1 failed, 22 passed in 0.72 seconds ==================================

You can go ahead and force push to remove my "Reverting" of the fix once travis shows that the tests fail. I'm not really a fan of using eps. Apparently it is pretty slow the first time you call it.

Also, you can precompute the inv of C1, and paste it in instead. It is nice round numbers so it stays readable. I figured that adding that in would delay this PR getting merged.

@hmaarrfk hmaarrfk force-pushed the bugfix_linalg_error_ellipsis_model branch from 3eee483 to 34395c3 Compare September 20, 2018 01:08
@hmaarrfk
Copy link
Member Author

Python 3.7 finally failed. I guess adding all those tests was worth it.
https://travis-ci.org/scikit-image/scikit-image/jobs/430817941#L2839

@hmaarrfk hmaarrfk force-pushed the bugfix_linalg_error_ellipsis_model branch 3 times, most recently from 12666fc to 9b5aae0 Compare September 20, 2018 03:00
@hmaarrfk
Copy link
Member Author

@jni Here is what recreates the issue:

from skimage.measure import EllipseModel
import numpy as np
model = EllipseModel()
print(model.estimate(np.array([[50, 80], [51, 81], [52, 80.00000000001]])))
print(model.estimate(np.array([[50, 80], [51, 81], [52, 80.0001000001]])))
print(model.estimate(np.array([[50, 80], [51, 81], [52, 80.000000001]])))
print(model.estimate(np.array([[50, 80], [51, 81], [52, 80.00000001]])))
print(model.estimate(np.array([[50, 80], [51, 81], [52, 80.0000001]])))
print(model.estimate(np.array([[50, 80], [51, 81], [52, 80.000001]])))
print(model.estimate(np.array([[50, 80], [51, 81], [52, 80.00001]])))
False
False
True
True
True
True
False

I thought you could simply check the condition number, but it fails the doctest, which should clearly pass....

@hmaarrfk
Copy link
Member Author

part of me thinks we should chek the number of input points. The problem of fitting an ellipse is ilposed with less than 5 points

# 64 bit arch
assert not model.estimate(np.array([[50, 80], [51, 81], [52, 80.00000000001]]))
# Unfortunately, it seems to depend on the version of python that is used
# Therefore, test for many cases
Copy link
Member

Choose a reason for hiding this comment

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

@hmaarrfk based on your comment, shouldn't this test be:

assert not all(model.estimate(np.array([[50, 80], [51, 81], [52, 80 + 10**(-n)]]))
               for n in range(4, 12))

?

Copy link
Member Author

Choose a reason for hiding this comment

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

@jni, refactored as per your suggestion. problem remains :/

Copy link
Member

Choose a reason for hiding this comment

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

Which problem? Do they all pass for some Python versions?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well my fix causes the doctest to fail. But the doctest seems to make sense. https://travis-ci.org/scikit-image/scikit-image/jobs/432304643#L2823

I'm glad to make the test xfail for code self-documentation.

Copy link
Member

Choose a reason for hiding this comment

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

OOOOoooooOOOOhhhh. Very interesting. Hmm, I think ellipse-fitting to a line should work, actually. =\

@hmaarrfk hmaarrfk force-pushed the bugfix_linalg_error_ellipsis_model branch from 9b5aae0 to 6ce1e2e Compare September 24, 2018 02:58
…ipsis fitting

Explicitely check the condition number of the matrix before computing
the eigen vectors
assert not model.estimate(np.array([[50, 80], [51, 81], [52, 80]]))
assert not model.estimate(np.array([[50, 80],
[51, 81],
[52, 80 + epsilon]]))
Copy link
Contributor

Choose a reason for hiding this comment

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

according to the comment, what is the epsilon number (it is not clear from the review scope)

Copy link
Member Author

Choose a reason for hiding this comment

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

sorry, quite often, epsilon is a small number in math. I could add a comment, but this wasn't the right approach anyway, and gave the same results as before.

@rfezzani rfezzani added 🩹 type: Bug fix Fixes unexpected or incorrect behavior and removed type: bug labels Feb 22, 2020
Base automatically changed from master to main February 18, 2021 18:23
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

10 participants