better accuracy of _rotatedRectangleIntersection#21677
Conversation
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
move comment
fix wrong numericalScalingFactor usage
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
We don't really want to have zero or denormalized zero value here.
There was a problem hiding this comment.
What do you mean ? this is the original code, I just moved it.
modules/imgproc/src/intersection.cpp
Outdated
| 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; |
There was a problem hiding this comment.
We already have normalization multiplier in the first section. Why do we apply it twice for t1/t2?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
modules/imgproc/src/intersection.cpp
Outdated
| // 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); |
There was a problem hiding this comment.
Changing A, B without C doesn't look correct.
There was a problem hiding this comment.
C will naturally be scaled since it is of the form C=alpha*A+beta*B, where A and B are already scaled.
alalek
left a comment
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
This test should be moved to imgproc module, see test_intersection.cpp file
| 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); |
There was a problem hiding this comment.
_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);
modules/imgproc/src/intersection.cpp
Outdated
| numericalScalingFactor = !numericalScalingFactor ? 1.f : 1.f/(numericalScalingFactor); | ||
| if (std::isinf(numericalScalingFactor)) | ||
| numericalScalingFactor = 1.f; |
There was a problem hiding this comment.
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
modules/imgproc/src/intersection.cpp
Outdated
| 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; |
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
BTW, this code is not performance critical, so probably we could use workaround with double computations inside instead of float.
There was a problem hiding this comment.
I think it would be better, but as a second pull request, since the current numericalScaleFactornormalizationScale is a robustness improvement anyway
modules/imgproc/src/intersection.cpp
Outdated
| 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)); |
There was a problem hiding this comment.
Why is std::min?
We need to "eliminate" large numbers.
There was a problem hiding this comment.
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
modules/imgproc/src/intersection.cpp
Outdated
| if (std::isinf(numericalScalingFactor)) | ||
| numericalScalingFactor = 1.f; | ||
|
|
||
| vx1 *= numericalScalingFactor; |
There was a problem hiding this comment.
BTW, Multiplication besides of 2^n could bring more artifacts.
There was a problem hiding this comment.
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"
modules/imgproc/src/intersection.cpp
Outdated
| vy2 *= normalizationScale; | ||
|
|
||
| const float det = vx2*vy1 - vx1*vy2; | ||
| const float detInvScaled = !det ? std::numeric_limits<float>::quiet_NaN() : (normalizationScale/det); |
There was a problem hiding this comment.
!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;
* 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
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.