From 621ad482d625a75ab8c5337269a3f18b50414fac Mon Sep 17 00:00:00 2001 From: Dmitry Kurtaev Date: Tue, 25 Nov 2025 20:56:59 +0800 Subject: [PATCH] Merge pull request #28051 from dkurt:minAreaRect_angle Correct minAreaRect angle to be in range [-90, 0) #28051 ### Pull Request Readiness Checklist Box angle range over all imgproc tests is in interval `[-90, -0.0581199]` resolves https://github.com/opencv/opencv/issues/27667 resolves https://github.com/opencv/opencv/issues/19472 resolves https://github.com/opencv/opencv/issues/24436 See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake --- .../imgproc/misc/java/test/ImgprocTest.java | 4 +-- modules/imgproc/src/rotcalipers.cpp | 34 ++++++++++++++----- modules/imgproc/test/test_convhull.cpp | 18 ++++++++++ modules/js/test/test_imgproc.js | 16 ++++----- modules/python/test/test_legacy.py | 2 +- 5 files changed, 55 insertions(+), 19 deletions(-) diff --git a/modules/imgproc/misc/java/test/ImgprocTest.java b/modules/imgproc/misc/java/test/ImgprocTest.java index c9c5753da7..0e27f7d437 100644 --- a/modules/imgproc/misc/java/test/ImgprocTest.java +++ b/modules/imgproc/misc/java/test/ImgprocTest.java @@ -1344,8 +1344,8 @@ public class ImgprocTest extends OpenCVTestCase { RotatedRect rrect = Imgproc.minAreaRect(points); - assertEquals(new Size(5, 2), rrect.size); - assertEquals(0., rrect.angle); + assertEquals(new Size(2, 5), rrect.size); + assertEquals(-90., rrect.angle); assertEquals(new Point(3.5, 2), rrect.center); } diff --git a/modules/imgproc/src/rotcalipers.cpp b/modules/imgproc/src/rotcalipers.cpp index 923013b762..d906258650 100644 --- a/modules/imgproc/src/rotcalipers.cpp +++ b/modules/imgproc/src/rotcalipers.cpp @@ -365,6 +365,7 @@ cv::RotatedRect cv::minAreaRect( InputArray _points ) Mat hull; Point2f out[3]; RotatedRect box; + box.angle = -(float)CV_PI / 2; // default angle for box without rotation and single point static const bool clockwise = false; convexHull(_points, hull, clockwise, true); @@ -384,19 +385,34 @@ cv::RotatedRect cv::minAreaRect( InputArray _points ) rotatingCalipers( hpoints, n, clockwise ? -1.f : 1.f, CALIPERS_MINAREARECT, (float*)out ); box.center.x = out[0].x + (out[1].x + out[2].x)*0.5f; box.center.y = out[0].y + (out[1].y + out[2].y)*0.5f; - box.size.width = (float)std::sqrt((double)out[1].x*out[1].x + (double)out[1].y*out[1].y); - box.size.height = (float)std::sqrt((double)out[2].x*out[2].x + (double)out[2].y*out[2].y); - box.angle = (float)atan2( (double)out[1].y, (double)out[1].x ); + box.size.width = (float)std::sqrt((double)out[2].x*out[2].x + (double)out[2].y*out[2].y); + box.size.height = (float)std::sqrt((double)out[1].x*out[1].x + (double)out[1].y*out[1].y); + if (out[1].x == 0.f && out[1].y > 0.f) + std::swap(box.size.width, box.size.height); + else + box.angle += (float)atan2( (double)out[1].y, (double)out[1].x ); } else if( n == 2 ) { box.center.x = (hpoints[0].x + hpoints[1].x)*0.5f; box.center.y = (hpoints[0].y + hpoints[1].y)*0.5f; - double dx = hpoints[1].x - hpoints[0].x; - double dy = hpoints[1].y - hpoints[0].y; - box.size.width = (float)std::sqrt(dx*dx + dy*dy); - box.size.height = 0; - box.angle = (float)atan2( dy, dx ); + double dx = hpoints[0].x - hpoints[1].x; + double dy = hpoints[0].y - hpoints[1].y; + box.size.width = 0; + box.size.height = (float)std::sqrt(dx*dx + dy*dy); + if (dx == 0) + { + std::swap(box.size.width, box.size.height); + } + else if (dy < 0) + { + box.angle = (float)atan2( dy, dx ); + std::swap(box.size.width, box.size.height); + } + else if (dy > 0) + { + box.angle += (float)atan2( dy, dx ); + } } else { @@ -405,6 +421,8 @@ cv::RotatedRect cv::minAreaRect( InputArray _points ) } box.angle = (float)(box.angle*180/CV_PI); + CV_DbgCheckGE(box.angle, -90.0f, ""); + CV_DbgCheckLT(box.angle, 0.0f, ""); return box; } diff --git a/modules/imgproc/test/test_convhull.cpp b/modules/imgproc/test/test_convhull.cpp index 3663ce56f6..774bee7af6 100644 --- a/modules/imgproc/test/test_convhull.cpp +++ b/modules/imgproc/test/test_convhull.cpp @@ -1265,6 +1265,24 @@ TEST(Imgproc_minAreaRect, reproducer_21482_small_values) EXPECT_EQ(rr.size.height, 1e-4f); } +typedef testing::TestWithParam> minAreaRect_of_line; +TEST_P(minAreaRect_of_line, accuracy) +{ + Point2f p1 = get<0>(GetParam()); + Point2f p2 = get<1>(GetParam()); + RotatedRect out = minAreaRect(std::vector{p1, p2}); + EXPECT_EQ(out.center, get<2>(GetParam())); + EXPECT_EQ(out.size, get<3>(GetParam())); + EXPECT_NEAR(out.angle, get<4>(GetParam()), 1e-6); +} +INSTANTIATE_TEST_CASE_P(Imgproc, minAreaRect_of_line, + testing::Values( + std::make_tuple(Point2f(10, 15), Point2f(10, 25), Point2f(10, 20), Size2f(10, 0), -90.f), + std::make_tuple(Point2f(450, 500), Point2f(508, 500), Point2f(479, 500), Size2f(0, 58), -90.f), + std::make_tuple(Point2f(10, 20), Point2f(13, 16), Point2f(11.5, 18), Size2f(5, 0), -53.1301002f), + std::make_tuple(Point2f(9, 19), Point2f(4, 7), Point2f(6.5, 13), Size2f(0, 13), -22.6198654f) + )); + }} // namespace /* End of file. */ diff --git a/modules/js/test/test_imgproc.js b/modules/js/test/test_imgproc.js index 24da9b9266..786a7ba4d2 100644 --- a/modules/js/test/test_imgproc.js +++ b/modules/js/test/test_imgproc.js @@ -710,14 +710,14 @@ QUnit.test('test_rotatedRectangleIntersection', function(assert) { assert.deepEqual(intersectionType, cv.INTERSECT_FULL); intersectionPoints.convertTo(intersectionPoints, cv.CV_32S); let intersectionPointsData = intersectionPoints.data32S; - assert.deepEqual(intersectionPointsData[0], 30); - assert.deepEqual(intersectionPointsData[1], 40); - assert.deepEqual(intersectionPointsData[2], 40); - assert.deepEqual(intersectionPointsData[3], 30); - assert.deepEqual(intersectionPointsData[4], 50); - assert.deepEqual(intersectionPointsData[5], 40); - assert.deepEqual(intersectionPointsData[6], 40); - assert.deepEqual(intersectionPointsData[7], 50); + assert.deepEqual(intersectionPointsData[0], 40); + assert.deepEqual(intersectionPointsData[1], 50); + assert.deepEqual(intersectionPointsData[2], 30); + assert.deepEqual(intersectionPointsData[3], 40); + assert.deepEqual(intersectionPointsData[4], 40); + assert.deepEqual(intersectionPointsData[5], 30); + assert.deepEqual(intersectionPointsData[6], 50); + assert.deepEqual(intersectionPointsData[7], 40); intersectionType = cv.rotatedRectangleIntersection(rr1, rr3, intersectionPoints); diff --git a/modules/python/test/test_legacy.py b/modules/python/test/test_legacy.py index e550ab73c8..47fd4ecd13 100644 --- a/modules/python/test/test_legacy.py +++ b/modules/python/test/test_legacy.py @@ -81,7 +81,7 @@ class Hackathon244Tests(NewOpenCVTests): mc, mr = cv.minEnclosingCircle(a) be0 = ((150.2511749267578, 150.77322387695312), (158.024658203125, 197.57696533203125), 37.57804489135742) - br0 = ((161.2974090576172, 154.41793823242188), (207.7177734375, 199.2301483154297), 80.83544921875) + br0 = ((161.2974090576172, 154.41793823242188), (199.2301483154297, 207.7177734375), -9.164555549621582) mc0, mr0 = (160.41790771484375, 144.55152893066406), 136.713500977 self.check_close_boxes(be, be0, 5, 15)