Skip to content

better accuracy of _rotatedRectangleIntersection#21677

Merged
alalek merged 9 commits intoopencv:4.xfrom
chacha21:rectangle_intersection
Mar 16, 2022
Merged

better accuracy of _rotatedRectangleIntersection#21677
alalek merged 9 commits intoopencv:4.xfrom
chacha21:rectangle_intersection

Conversation

@chacha21
Copy link
Copy Markdown
Contributor

@chacha21 chacha21 commented Mar 3, 2022

Proposal for #21659

Migrating to double precision is a solution, but we can gain some accuracy even with floats by scaling computations according to the actual numeric scale of the current data.

More tests are needed, but it would be a waste of time if we just move to double precision instead.

chacha21 added 5 commits March 3, 2022 14:43
instead of just migrating to double-precision (which would work), some computations are scaled by a factor that depends on the length of the smallest vectors.
There is a better accuracy even with floats, so this is certainly better for very sensitive cases
use L2SQR norm to tune the numeric scale
adapt samePointEps with L2 norm
fix wrong numericalScalingFactor usage
Copy link
Copy Markdown
Member

@alalek alalek left a comment

Choose a reason for hiding this comment

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

It makes sense to take a look on existed code coverage and add appropriate tests before refactoring (to avoid regressions).
Test case from the issue should be added too.

rect2.points(pts2);

// L2 metric
float samePointEps = 1e-6f * (float)std::max(rect1.size.area(), rect2.size.area());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We don't really want to have zero or denormalized zero value here.

Copy link
Copy Markdown
Contributor Author

@chacha21 chacha21 Mar 10, 2022

Choose a reason for hiding this comment

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

What do you mean ? this is the original code, I just moved it.

Comment on lines +130 to +138
vx1 *= numericalScalingFactor;
vy1 *= numericalScalingFactor;
vx2 *= numericalScalingFactor;
vy2 *= numericalScalingFactor;

float det = vx2*vy1 - vx1*vy2;

float t1 = (vx2*y21 - vy2*x21) / det;
float t2 = (vx1*y21 - vy1*x21) / det;
float t1 = (vx2*y21 - vy2*x21)*numericalScalingFactor/det;
float t2 = (vx1*y21 - vy1*x21)*numericalScalingFactor/det;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We already have normalization multiplier in the first section. Why do we apply it twice for t1/t2?

Copy link
Copy Markdown
Contributor Author

@chacha21 chacha21 Mar 10, 2022

Choose a reason for hiding this comment

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

This current code is correct : if you look closely, you can see that det is of the form alpha*numericalScalingFactor^2-beta*numericalScalingFactor^2, while (vx2*y21 - vy2*x21) and (vx1*y21 - vy1*x21) are of the form alpha*numericalScalingFactor-beta*numericalScalingFactor
That's why det must be divided by numericalScalingFactor to compute t1 and t2 and get the same result as when not using any numericalScalingFactor.
We could just change the code by float det = (vx2*vy1 - vx1*vy2)/numericalScalingFactor;, but I wanted to keep the initial det formula, and I prefered delaying that division to the computation of t1 and t2.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Agreed.

However more clear code should multiply:

x21 *= numericalScalingFactor;
y21 *= numericalScalingFactor;

This code has 2 multiply operations (but could be executed in parallel).


Alternative is to replace 2 mul + 2 div ops => 2 mul + 1 div

float det_inv = numericalScalingFactor/det;

float t1 = (...)*det_inv;
float t2 = (...)*det_inv;

Bailout check could be changed too:

-if( cvIsInf(t1) || cvIsInf(t2) || cvIsNaN(t1) || cvIsNaN(t2) )
+if (cvIsInf(dev_inv))
     continue;

But need to analyze numerical stability of this code first.


BTW, this code section is a generic "Solve for 2x2 Ax=b". Perhaps we could extract its code as a separate function (somewhere into core module).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍

Comment on lines 179 to 183
// line equation: Ax + By + C = 0
// see which side of the line this point is at
float A = -vec2[j].y;
float B = vec2[j].x;
float A = -vec2[j].y*numericalScalingFactor;
float B = vec2[j].x*numericalScalingFactor;
float C = -(A*pts2[j].x + B*pts2[j].y);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Changing A, B without C doesn't look correct.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

C will naturally be scaled since it is of the form C=alpha*A+beta*B, where A and B are already scaled.

@asmorkalov asmorkalov removed the pr: needs test New functionality requires minimal tests set label Mar 14, 2022
@asmorkalov asmorkalov requested a review from alalek March 14, 2022 11:52
Copy link
Copy Markdown
Member

@alalek alalek 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 for update!
Regression test looks good (fails without implementation changes)


TEST(Core_RotatedRect, three_point_constructor) { Core_RotatedRectConstructorTest test; test.safe_run(); }

TEST(Core_RotatedRect, intersection)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This test should be moved to imgproc module, see test_intersection.cpp file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍

r2 = cv::RotatedRect(cv::Point2f(-2.f, -2.f)*scaleFactor, cv::Size2f(1.f, 1.f)*scaleFactor, 0);
intersectionResult = (cv::RectanglesIntersectTypes) cv::rotatedRectangleIntersection(r1, r2, intersection);
intersectionArea = (intersection.size() <= 2) ? 0. : cv::contourArea(intersection);
ASSERT_EQ(intersectionResult, cv::RectanglesIntersectTypes::INTERSECT_NONE);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

_EQ

Signature for _EQ is ASSERT_EQ(expected_reference, actual_result);.
Correct order of parameters are required to emit valid error messages.

Reference: https://github.com/opencv/opencv/blob/4.0.0/modules/ts/include/opencv2/ts/ts_gtest.h#L8196-L8200

GTEST_API_ AssertionResult EqFailure(const char* expected_expression,
                                     const char* actual_expression,
                                     const std::string& expected_value,
                                     const std::string& actual_value,
                                     bool ignoring_case);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍

Comment on lines +126 to +128
numericalScalingFactor = !numericalScalingFactor ? 1.f : 1.f/(numericalScalingFactor);
if (std::isinf(numericalScalingFactor))
numericalScalingFactor = 1.f;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Other OpenCV code uses this pattern without secondary check:

numericalScalingFactor = std::abs(numericalScalingFactor) < 1e-6 ? 1.f : 1.f/(numericalScalingFactor);

std::abs call may be excluded too as we have abs call above.


Could you please to rename variable numerical -> normalize? e.g. normalizationScale or similar

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍

Comment on lines +130 to +138
vx1 *= numericalScalingFactor;
vy1 *= numericalScalingFactor;
vx2 *= numericalScalingFactor;
vy2 *= numericalScalingFactor;

float det = vx2*vy1 - vx1*vy2;

float t1 = (vx2*y21 - vy2*x21) / det;
float t2 = (vx1*y21 - vy1*x21) / det;
float t1 = (vx2*y21 - vy2*x21)*numericalScalingFactor/det;
float t2 = (vx1*y21 - vy1*x21)*numericalScalingFactor/det;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Agreed.

However more clear code should multiply:

x21 *= numericalScalingFactor;
y21 *= numericalScalingFactor;

This code has 2 multiply operations (but could be executed in parallel).


Alternative is to replace 2 mul + 2 div ops => 2 mul + 1 div

float det_inv = numericalScalingFactor/det;

float t1 = (...)*det_inv;
float t2 = (...)*det_inv;

Bailout check could be changed too:

-if( cvIsInf(t1) || cvIsInf(t2) || cvIsNaN(t1) || cvIsNaN(t2) )
+if (cvIsInf(dev_inv))
     continue;

But need to analyze numerical stability of this code first.


BTW, this code section is a generic "Solve for 2x2 Ax=b". Perhaps we could extract its code as a separate function (somewhere into core module).

const float samePointEps = std::max(1e-16f, 1e-6f * (float)std::max(rect1.size.area(), rect2.size.area()));

Point2f vec1[4], vec2[4];
Point2f pts1[4], pts2[4];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

BTW, this code is not performance critical, so probably we could use workaround with double computations inside instead of float.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think it would be better, but as a second pull request, since the current numericalScaleFactor normalizationScale is a robustness improvement anyway

float vx2 = vec2[j].x;
float vy2 = vec2[j].y;

float numericalScalingFactor = std::min(std::abs(vx1*vx1+vy1*vy1), std::abs(vx2*vx2+vy2*vy2));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is std::min?
We need to "eliminate" large numbers.

Copy link
Copy Markdown
Contributor Author

@chacha21 chacha21 Mar 14, 2022

Choose a reason for hiding this comment

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

I think both min and max are valid and (more or less) equivalent (relatively to numerical stability) for this use case.
The idea is to scale the range to be close to 1, so either you do it relatively to the lower bound (with a min), or to the higher bound (with a max); we could even scale relatively to the (logarithmic) mean value

if (std::isinf(numericalScalingFactor))
numericalScalingFactor = 1.f;

vx1 *= numericalScalingFactor;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

BTW, Multiplication besides of 2^n could bring more artifacts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think that it would not be worse since a large exponent would certainly already be a problem about the significand digits in the current computations

renaming numericalScaleFctor to normalizationScale
refactor some computations
more "const"
vy2 *= normalizationScale;

const float det = vx2*vy1 - vx1*vy2;
const float detInvScaled = !det ? std::numeric_limits<float>::quiet_NaN() : (normalizationScale/det);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

!det

Please avoid using bit-exact zero comparison with floating point variables. It could not work as expected.

Use bailout pre-check:

if (std::abs(det) < 1e-6)
    continue;

Copy link
Copy Markdown
Member

@alalek alalek left a comment

Choose a reason for hiding this comment

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

LGTM! Thank you 👍

@alalek alalek merged commit ef6f421 into opencv:4.x Mar 16, 2022
@opencv-pushbot opencv-pushbot mentioned this pull request Apr 23, 2022
a-sajjad72 pushed a commit to a-sajjad72/opencv that referenced this pull request Mar 30, 2023
* better accuracy of _rotatedRectangleIntersection

instead of just migrating to double-precision (which would work), some computations are scaled by a factor that depends on the length of the smallest vectors.
There is a better accuracy even with floats, so this is certainly better for very sensitive cases

* Update intersection.cpp

use L2SQR norm to tune the numeric scale

* Update intersection.cpp

adapt samePointEps with L2 norm

* Update intersection.cpp

move comment

* Update intersection.cpp

fix wrong numericalScalingFactor usage

* added tests

* fixed warnings returned by buildbot

* modifications suggested by reviewer

renaming numericalScaleFctor to normalizationScale
refactor some computations
more "const"

* modifications as suggested by reviewer
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rotatedRectangleIntersection gives incorrect result for pairs of nearly identical rectangles

3 participants