Skip to content

Commit 2fae599

Browse files
authored
Add convex hull algorithm for 2D contours (#4770)
* Add convex hull algorithm for 2D contours * Add unit tests * Don't compute angle values * Don't allocate additional array * Fix assert * Update VS project files
1 parent 1d70450 commit 2fae599

File tree

4 files changed

+139
-0
lines changed

4 files changed

+139
-0
lines changed

source/MRMesh/MRConvexHull.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,52 @@ Mesh makeConvexHull( const PointCloud & in )
281281
return makeConvexHull( in.points, in.validPoints );
282282
}
283283

284+
Contour2f makeConvexHull( Contour2f points )
285+
{
286+
if ( points.size() < 2 )
287+
return points;
288+
289+
auto minPointIt = std::min_element( points.begin(), points.end(), [] ( auto&& a, auto&& b )
290+
{
291+
return std::tie( a.y, a.x ) < std::tie( b.y, b.x );
292+
} );
293+
std::swap( *points.begin(), *minPointIt );
294+
const auto& minPoint = points.front();
295+
296+
// sort points by polar angle and distance to the start point
297+
std::sort( points.begin() + 1, points.end(), [&] ( const Vector2f& a, const Vector2f& b )
298+
{
299+
const auto va = a - minPoint, vb = b - minPoint;
300+
if ( auto c = cross( va, vb ); c != 0.f )
301+
return c > 0.f;
302+
return va.lengthSq() > vb.lengthSq();
303+
} );
304+
305+
size_t size = 2;
306+
for ( auto i = 2; i < points.size(); ++i )
307+
{
308+
if ( cross( points[i - 1] - minPoint, points[i - 0] - minPoint ) == 0.f )
309+
{
310+
assert( ( points[i - 1] - minPoint ).lengthSq() >= ( points[i - 0] - minPoint ).lengthSq() );
311+
continue;
312+
}
313+
314+
const auto& p = points[i];
315+
while ( size >= 2 )
316+
{
317+
const auto& a = points[size - 2];
318+
const auto& b = points[size - 1];
319+
if ( cross( b - a, p - a ) > 0.f )
320+
break;
321+
size--;
322+
}
323+
points[size++] = p;
324+
}
325+
points.erase( points.begin() + size, points.end() );
326+
327+
return points;
328+
}
329+
284330
TEST( MRMesh, ConvexHull )
285331
{
286332
Mesh torus = makeTorus( 1.0f, 0.3f, 16, 16 );

source/MRMesh/MRConvexHull.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ MRMESH_API Mesh makeConvexHull( const VertCoords & points, const VertBitSet & va
1010
MRMESH_API Mesh makeConvexHull( const Mesh & in );
1111
MRMESH_API Mesh makeConvexHull( const PointCloud & in );
1212

13+
// computes the contour of convex hull from given input points
14+
MRMESH_API Contour2f makeConvexHull( Contour2f points );
15+
1316
} //namespace MR

source/MRTest/MRConvexHull.cpp

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#include "MRMesh/MRConvexHull.h"
2+
3+
#include "MRMesh/MRGTest.h"
4+
5+
namespace MR
6+
{
7+
8+
// Test case for an empty set of points
9+
TEST(MakeConvexHullTest, EmptyInput) {
10+
std::vector<Vector2f> points;
11+
std::vector<Vector2f> hull = makeConvexHull(points);
12+
ASSERT_TRUE(hull.empty());
13+
}
14+
15+
// Test case for a single point
16+
TEST(MakeConvexHullTest, SinglePoint) {
17+
std::vector<Vector2f> points = {{1.0f, 1.0f}};
18+
std::vector<Vector2f> hull = makeConvexHull(points);
19+
EXPECT_EQ(hull, points);
20+
}
21+
22+
// Test case for two points
23+
TEST(MakeConvexHullTest, TwoPoints) {
24+
std::vector<Vector2f> points = {{2.0f, 2.0f}, {1.0f, 1.0f} };
25+
std::vector<Vector2f> hull = makeConvexHull(points);
26+
ASSERT_EQ(hull.size(), 2);
27+
// The hull should start from a minimal coords point.
28+
EXPECT_EQ(hull[0], points[1]);
29+
EXPECT_EQ(hull[1], points[0]);
30+
}
31+
32+
// Test case for three collinear points
33+
TEST(MakeConvexHullTest, ThreeCollinearPoints) {
34+
std::vector<Vector2f> points = {{1.0f, 1.0f}, {2.0f, 2.0f}, {3.0f, 3.0f}};
35+
std::vector<Vector2f> hull = makeConvexHull(points);
36+
ASSERT_EQ(hull.size(), 2);
37+
// The hull should only contain the two extreme points
38+
EXPECT_EQ(hull[0], points[0]);
39+
EXPECT_EQ(hull[1], points[2]);
40+
}
41+
42+
// Test case for a simple square
43+
TEST(MakeConvexHullTest, SimpleSquare) {
44+
std::vector<Vector2f> points = {{0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 1.0f}, {1.0f, 1.0f}};
45+
std::vector<Vector2f> hull = makeConvexHull(points);
46+
ASSERT_EQ(hull.size(), 4);
47+
48+
// The expected hull in counter-clockwise order
49+
std::vector<Vector2f> expected_hull = {{0.0f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 1.0f}};
50+
EXPECT_EQ(hull, expected_hull);
51+
}
52+
53+
// Test case with points inside the hull
54+
TEST(MakeConvexHullTest, PointsInside) {
55+
std::vector<Vector2f> points = {
56+
{0.0f, 0.0f}, {5.0f, 0.0f}, {0.0f, 5.0f}, {5.0f, 5.0f}, // The corners of the hull
57+
{1.0f, 1.0f}, {2.0f, 3.0f}, {4.0f, 2.0f} // Points inside
58+
};
59+
std::vector<Vector2f> hull = makeConvexHull(points);
60+
ASSERT_EQ(hull.size(), 4);
61+
62+
std::vector<Vector2f> expected_hull = {{0.0f, 0.0f}, {5.0f, 0.0f}, {5.0f, 5.0f}, {0.0f, 5.0f}};
63+
EXPECT_EQ(hull, expected_hull);
64+
}
65+
66+
// Test case with duplicate points
67+
TEST(MakeConvexHullTest, DuplicatePoints) {
68+
std::vector<Vector2f> points = {{0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f}};
69+
std::vector<Vector2f> hull = makeConvexHull(points);
70+
ASSERT_EQ(hull.size(), 3);
71+
72+
std::vector<Vector2f> expected_hull = {{0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 1.0f}};
73+
EXPECT_EQ(hull, expected_hull);
74+
}
75+
76+
// A more complex test case with various points
77+
TEST(MakeConvexHullTest, ComplexShape) {
78+
std::vector<Vector2f> points = {
79+
{0, 3}, {1, 1}, {2, 2}, {4, 4},
80+
{0, 0}, {1, 2}, {3, 1}, {3, 3}
81+
};
82+
std::vector<Vector2f> hull = makeConvexHull(points);
83+
ASSERT_EQ(hull.size(), 4);
84+
85+
std::vector<Vector2f> expected_hull = {{0, 0}, {3, 1}, {4, 4}, {0, 3}};
86+
EXPECT_EQ(hull, expected_hull);
87+
}
88+
89+
} // namespace MR

source/MRTest/MRTest.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
<ClCompile Include="MRVolumeToMeshByPartsTests.cpp" />
4545
<ClCompile Include="MRZlib.cpp" />
4646
<ClCompile Include="MRProgressCallback.cpp" />
47+
<ClCompile Include="MRConvexHull.cpp" />
4748
</ItemGroup>
4849
<ItemGroup>
4950
<ProjectReference Include="..\..\thirdparty\pybind11nonlimitedapi_stubs.vcxproj">

0 commit comments

Comments
 (0)