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 cal3 fisheye jacobian #902

Merged
merged 16 commits into from
Nov 28, 2021

Conversation

roderick-koehle
Copy link
Contributor

In the calculation of the jacobian for the Cal3Fisheye calibration model the resulting jacobian contain NaN due to an unhandled division by zero. The problem occurs in case the spatial object point is located on the optical axis.

Provided unit test in test_Cal3Fisheye illustrate the issue by projecting a point at (0, 0, 1) with the camera being located at the origin.

@ProfFan ProfFan requested a review from dellaert October 22, 2021 18:18
DR << dxd_dxi, dxd_dyi, dyd_dxi, dyd_dyi;

*H2 = DK * DR;
if (r2==0) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe this should be r2 < 1e-7 or some other tolerance.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The condition r2>0 implies the resolution of the image radius r to be above sqrt(std::numeric_limits<double>::denorm_min()) ~ 1.49e-154. By avoiding to compute the square or divisor of two small values, the resulting gradient should be reasonably accurate.

I implemented a python testcase test_jacobian_convergence in test_Cal3Fisheye.py to cover this situation.

In the current implementation of the jacobian, this results in a overflow or division by zero error, if the square radius r2 is of the size of 1 ulp (unit of least precision in C++ std::numeric_limits::denorm_min() ~ 4.94e-324).

I suggest the refactoring in provided patch below, avoiding the offending division by 1/r2 (or multiplication with rrdiv).

Eventually, the interface design might be extended supporting incident angles above 90 deg. It may be beneficial to generalize the formulation of derivatives, to cover the case zi != 1. The scaling function needs to be generalized to use atan2(r, z)/r instead of atan(r)/r. From numeric tests, is seems fine to implement the scaling for (r/z != 0). See the test_scaling_factor unit test to cover this case.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@roderick-koehle I believe @ProfFan was referring to the fact that doing a r2==0 check is numerically unstable since that check will fail for r2=1e-15 but the value of r2 in this case is still small enough to cause various issues like overflow and bad jacobians.

This is a recommendation for doing a numerical check rather than an algebraic check.

Copy link
Contributor Author

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.

An arbitrary fixed threshold to select the paraxial approximation I would not recommend.

The critical numerically unstable calculation is the calculation of the unit directions.
c, s = x/r, y/r
this problem is related to computing the givens rotation, e.g. used in the blas library.
Once the unit direction is known, the sine and cosine will be a normalised number and no overflow/underflow issues can occur.

For a more in depth discussion about the issues, see:
Bindel, Demmel, Kahan, "On Computing Givens Rotations Reliably and Efficiently"

The current suggested implementation is simple and numerically stable, this is demonstrated and covered by the provided python tests. In this case, using (r2==0)is a well defined lower threshold imposed on r2 being independent of the floating point datatype being used.

Alternatively, for using a higher threshold, e.g. for 1/3 r2 < 0.5 eps with eps being the machine precision, the distortion induced by the arctan(xi)/xi ~ 1+1/3 x^2 becomes equal to one and the paraxial approximation can be used. For this choice, k1 should not exceed 1/3 (the fist taylor coefficient of the tan series development) and z is assumed 1. See the boost sinc_pi implementation to illustrate this technique. Note the discussion, https://stackoverflow.com/questions/47215765/is-boostmathsinc-pi-unnecessarily-complicated
about using taylor bounds.

My suggestion is to either stay with the simple type-independent scheme using the conditionr2==0, or compute the direction cosines using a stable unit direction implementation, similar to the givens rotation implementation as proposed by the publication by Blink. Then, no paraxial case is necessary.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Okay this makes sense. Thanks for the explanation.

Can you please address the other comments as well?

Copy link
Collaborator

@varunagrawal varunagrawal left a comment

Choose a reason for hiding this comment

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

Great stuff, but I have some comments.

Unit in the last place of floating point datatypes
"""
f = np.finfo(ftype)
return f.tiny / ftype(1 << f.nmant)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is awesome but is it physically plausible? I don't expect the radial value to be 1e-154 given today's manufacturing precisions. Using a reasonable value of say 1e-20 should be equivalent?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The goal here is to use the ulp value to test for a possible overflow in the implementation, independent of the floating point datatype.

The value of ulp() for half float, float and double is given by,

julia> eps(nextfloat(Float16(0.0)))
Float16(6.0e-8)
julia> eps(nextfloat(Float32(0.0)))
1.0f-45
julia> eps(nextfloat(Float64(0.0)))
5.0e-324

The f.tiny provides the smallest value, at which the floating point number can still be represented as normalized value.
Since python 3.9, numpy provides the numpy.ulp() function (see https://bugs.python.org/issue39310 or https://www.boost.org/doc/libs/1_77_0/libs/math/doc/html/math_toolkit/next_float/ulp.html).

In our case, it defines the smallest denormalized value ulp, such that 0+ulp > 0. In simple terms, when numbers are denormalized, calculations are done in fixed-point arithmetic, using the smallest exponent for the mantissa.

This value is used in the test to check whether overflow occurs. In the original implementation, setting x=ulp() and y=0, resulted in an overflow returning a NaN Jacobian.

In terms of the numeric resolution of double float, a value of 1e-20 is a huge number. For half-float, it is a too small number to represent.

The test would not catch any critical overflow condition. (Same for using f.tiny - it does not catch an overflow for computing the Jacobian)

Physically, thinking in terms of taylor expansion, a radius of 1e-20 the Jacobian would be sufficient to be in the paraxial region - as noted, the implementation then depends on the datatype.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Awesome, this is a great explanation.

const double dr_dxi = xi * rinv;
const double dr_dyi = yi * rinv;
const double dtd_dr = dtd_dt * dt_dr;
// const double dtd_dxi = dtd_dt * dt_dr * dr_dxi;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Delete these commented lines if not used?

// values by avoiding division with the square radius.
//
// const double td = t * K.dot(T); - note: s = td/r
// const double rrinv = 1 / r2; - division by r2 may cause overflow
Copy link
Collaborator

Choose a reason for hiding this comment

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

Delete these commented lines if not used?

// Derivatives by depth, for future use to support incident
// angles above 90 deg.
// const double dxd_dzi = -dtd_dt * x / R2
// const double dyd_dzi = -dtd_dt * y / R2
Copy link
Collaborator

Choose a reason for hiding this comment

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

Delete these commented lines if not used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comments are removed.

@ProfFan
Copy link
Collaborator

ProfFan commented Nov 21, 2021

@roderick-koehle could you address the comments so we can merge this great PR?

@ProfFan ProfFan mentioned this pull request Nov 22, 2021
14 tasks
Copy link
Collaborator

@varunagrawal varunagrawal left a comment

Choose a reason for hiding this comment

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

LGTM

@varunagrawal varunagrawal merged commit ad86b14 into borglab:develop Nov 28, 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.

3 participants