Skip to content

Commit 50651c8

Browse files
Merge pull request #22 from Innoptech/connected-components
Introducing connected components
2 parents 12de386 + b7c5580 commit 50651c8

File tree

12 files changed

+546
-154
lines changed

12 files changed

+546
-154
lines changed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ ReadVersion(${PYPROJECT_PATH})
2020
#-------------------------------------------------------------------------------
2121
# COMPILATION
2222
#-------------------------------------------------------------------------------
23-
set(CMAKE_CXX_STANDARD 11)
23+
set(CMAKE_CXX_STANDARD 17)
2424
set(CMAKE_CXX_STANDARD_REQUIRED ON)
2525
set(CMAKE_CXX_EXTENSIONS OFF)
2626

README.md

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,40 @@ The fastest and most intuitive library to manipulate STL files (stereolithograph
1212
🌟 :fist_raised: Please consider starring and sponsoring the GitHub repo to show your support! :fist_raised: 🌟
1313
![GitHub Sponsor](https://img.shields.io/github/sponsors/Innoptech?label=Sponsor&logo=GitHub)
1414

15+
## Index
16+
1. **Performance**
17+
- [Performance Benchmark](#performances-benchmark)
18+
19+
2. **Python Usage**
20+
- [Install](#install)
21+
- [Read and Write STL Files](#read-and-write-from-a-stl-file)
22+
- [Rotate, Translate, and Scale Meshes](#rotate-translate-and-scale-a-mesh)
23+
- [Convert Between Triangles and Vertices/Faces](#convert-triangles-arrow_right-vertices-and-faces)
24+
- [Find Connected Components](#find-connected-components-in-mesh-topology-disjoint-solids)
25+
- [Use with PyTorch](#use-with-pytorch)
26+
- [Handling Large STL Files](#read-large-stl-file)
27+
28+
3. **C++ Usage**
29+
- [Read STL from File](#read-stl-from-file)
30+
- [Write STL to File](#write-stl-to-a-file)
31+
- [Serialize STL to Stream](#serialize-stl-to-a-stream)
32+
- [Convert Between Triangles and Vertices/Faces](#convert-triangles-arrow_right-vertices-and-faces-1)
33+
- [Find Connected Components](#find-connected-components-in-mesh-topology)
34+
35+
4. **C++ Integration**
36+
- [Smart Method with CMake](#smart-method)
37+
- [Naïve Method](#naïve-method)
38+
39+
5. **Testing**
40+
- [Run Tests](#test)
41+
42+
6. **Requirements**
43+
- [C++ Standards](#requirements)
44+
45+
7. **Disclaimer**
46+
- [STL File Format Limitations](#disclaimer-stl-file-format)
47+
48+
1549
# Performances benchmark
1650
Discover the staggering performance of OpenSTL in comparison to [numpy-stl](https://github.com/wolph/numpy-stl),
1751
[meshio](https://github.com/nschloe/meshio) and [stl-reader](https://github.com/pyvista/stl-reader), thanks to its powerful C++ backend.
@@ -124,6 +158,35 @@ faces = [
124158
triangles = openstl.convert.triangles(vertices, faces)
125159
```
126160

161+
### Find Connected Components in Mesh Topology (Disjoint solids)
162+
```python
163+
import openstl
164+
165+
# Define vertices and faces for two disconnected components
166+
vertices = [
167+
[0.0, 0.0, 0.0],
168+
[1.0, 0.0, 0.0],
169+
[0.0, 1.0, 0.0],
170+
[2.0, 2.0, 0.0],
171+
[3.0, 2.0, 0.0],
172+
[2.5, 3.0, 0.0],
173+
]
174+
175+
faces = [
176+
[0, 1, 2], # Component 1
177+
[3, 4, 5], # Component 2
178+
]
179+
180+
# Identify connected components of faces
181+
connected_components = openstl.topology.find_connected_components(vertices, faces)
182+
183+
# Print the result
184+
print(f"Number of connected components: {len(connected_components)}")
185+
for i, component in enumerate(connected_components):
186+
print(f"Component {i + 1}: {component}")
187+
```
188+
189+
127190
### Use with `Pytorch`
128191
```python
129192
import openstl
@@ -148,7 +211,7 @@ scale = 1000.0
148211
quad[:,1:4,:] *= scale # Avoid scaling normals
149212
```
150213

151-
### Read large STL file
214+
### Read large STL file
152215
To read STL file with a large triangle count > **1 000 000**, the openstl buffer overflow safety must be unactivated with
153216
`openstl.set_activate_overflow_safety(False)` after import. Deactivating overflow safety may expose the application
154217
to a potential buffer overflow attack vector since the stl standard is not backed by a checksum.
@@ -223,7 +286,40 @@ std::vector<Face> faces = {
223286
const auto& triangles = convertToTriangles(vertices, faces);
224287
```
225288
226-
# Integrate to your codebase
289+
### Find Connected Components in Mesh Topology
290+
```c++
291+
#include <openstl/topology.hpp>
292+
#include <vector>
293+
#include <iostream>
294+
295+
using namespace openstl;
296+
297+
int main() {
298+
std::vector<Vec3> vertices = {
299+
{0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, // Component 1
300+
{2.0f, 2.0f, 0.0f}, {3.0f, 2.0f, 0.0f}, {2.5f, 3.0f, 0.0f} // Component 2
301+
};
302+
303+
std::vector<Face> faces = {
304+
{0, 1, 2}, // Component 1
305+
{3, 4, 5}, // Component 2
306+
};
307+
308+
const auto& connected_components = findConnectedComponents(vertices, faces);
309+
310+
std::cout << "Number of connected components: " << connected_components.size() << "\\n";
311+
for (size_t i = 0; i < connected_components.size(); ++i) {
312+
std::cout << "Component " << i + 1 << ":\\n";
313+
for (const auto& face : connected_components[i]) {
314+
std::cout << " {" << face[0] << ", " << face[1] << ", " << face[2] << "}\\n";
315+
}
316+
}
317+
318+
return 0;
319+
}
320+
```
321+
****
322+
# Integrate to your C++ codebase
227323
### Smart method
228324
Include this repository with CMAKE Fetchcontent and link your executable/library to `openstl::core` library.
229325
Choose weither you want to fetch a specific branch or tag using `GIT_TAG`. Use the `main` branch to keep updated with the latest improvements.
@@ -250,7 +346,7 @@ ctest .
250346
```
251347

252348
# Requirements
253-
C++11 or higher.
349+
C++17 or higher.
254350

255351

256352
# DISCLAIMER: STL File Format #

modules/core/include/openstl/core/stl.h

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ namespace openstl
133133
* A library-level configuration to activate/deactivate the buffer overflow safety
134134
* @return
135135
*/
136-
bool& activateOverflowSafety() {
136+
inline bool& activateOverflowSafety() {
137137
static bool safety_enabled = true;
138138
return safety_enabled;
139139
}
@@ -284,7 +284,7 @@ namespace openstl
284284
}
285285

286286
//---------------------------------------------------------------------------------------------------------
287-
// Transformation Utils
287+
// Conversion Utils
288288
//---------------------------------------------------------------------------------------------------------
289289
using Face = std::array<size_t, 3>; // v0, v1, v2
290290

@@ -389,5 +389,73 @@ namespace openstl
389389
}
390390
return triangles;
391391
}
392+
393+
//---------------------------------------------------------------------------------------------------------
394+
// Topology Utils
395+
//---------------------------------------------------------------------------------------------------------
396+
/**
397+
* DisjointSet class to manage disjoint sets with union-find.
398+
*/
399+
class DisjointSet {
400+
std::vector<size_t> parent;
401+
std::vector<size_t> rank;
402+
403+
public:
404+
explicit DisjointSet(size_t size) : parent(size), rank(size, 0) {
405+
for (size_t i = 0; i < size; ++i) parent[i] = i;
406+
}
407+
408+
size_t find(size_t x) {
409+
if (parent[x] != x) parent[x] = find(parent[x]);
410+
return parent[x];
411+
}
412+
413+
void unite(size_t x, size_t y) {
414+
size_t rootX = find(x), rootY = find(y);
415+
if (rootX != rootY) {
416+
if (rank[rootX] < rank[rootY]) parent[rootX] = rootY;
417+
else if (rank[rootX] > rank[rootY]) parent[rootY] = rootX;
418+
else {
419+
parent[rootY] = rootX;
420+
++rank[rootX];
421+
}
422+
}
423+
}
424+
425+
bool connected(size_t x, size_t y) {
426+
return find(x) == find(y);
427+
}
428+
};
429+
430+
/**
431+
* Identifies and groups connected components of faces based on shared vertices.
432+
*
433+
* @param vertices A container of vertices.
434+
* @param faces A container of faces, where each face is a collection of vertex indices.
435+
* @return A vector of connected components, where each component is a vector of faces.
436+
*/
437+
template<typename ContainerA, typename ContainerB>
438+
inline std::vector<std::vector<Face>>
439+
findConnectedComponents(const ContainerA& vertices, const ContainerB& faces) {
440+
DisjointSet ds{vertices.size()};
441+
for (const auto& tri : faces) {
442+
ds.unite(tri[0], tri[1]);
443+
ds.unite(tri[0], tri[2]);
444+
}
445+
446+
std::vector<std::vector<Face>> result;
447+
std::unordered_map<size_t, size_t> rootToIndex;
448+
449+
for (const auto& tri : faces) {
450+
size_t root = ds.find(tri[0]);
451+
if (rootToIndex.find(root) == rootToIndex.end()) {
452+
rootToIndex[root] = result.size();
453+
result.emplace_back();
454+
}
455+
result[rootToIndex[root]].push_back(tri);
456+
}
457+
return result;
458+
}
459+
392460
} //namespace openstl
393461
#endif //OPENSTL_OPENSTL_SERIALIZE_H

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ testpaths = ["tests/python"]
2626

2727
[tool.commitizen]
2828
name = "cz_conventional_commits"
29-
version = "1.2.10"
29+
version = "1.3.0"
3030
tag_format = "v$version"
3131

3232
[tool.cibuildwheel]

python/core/src/stl.cpp

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <pybind11/pybind11.h>
2+
#include <pybind11/stl.h>
23
#include <pybind11/numpy.h>
34
#include <pybind11/iostream.h>
45
#include <memory>
@@ -241,9 +242,47 @@ void convertSubmodule(py::module_ &_m)
241242
}, "vertices"_a,"faces"_a, "Convert the mesh from vertices and faces to triangles");
242243
}
243244

245+
void topologySubmodule(py::module_ &_m)
246+
{
247+
auto m = _m.def_submodule("topology", "A submodule for analyzing and segmenting connected components in mesh topology.");
248+
249+
m.def("find_connected_components", [](
250+
const py::array_t<float, py::array::c_style | py::array::forcecast> &vertices,
251+
const py::array_t<size_t, py::array::c_style | py::array::forcecast> &faces
252+
) -> std::vector<std::vector<Face>>
253+
{
254+
py::scoped_ostream_redirect stream(std::cerr,py::module_::import("sys").attr("stderr"));
255+
auto vbuf = py::array_t<float, py::array::c_style | py::array::forcecast>::ensure(vertices);
256+
if(!vbuf){
257+
std::cerr << "Vertices input array cannot be interpreted as a mesh.\n";
258+
return {};
259+
}
260+
if (vbuf.ndim() != 2 || vbuf.shape(1) != 3){
261+
std::cerr << "Vertices input array cannot be interpreted as a mesh. Shape must be N x 3.\n";
262+
return {};
263+
}
264+
265+
auto fbuf = py::array_t<size_t , py::array::c_style | py::array::forcecast>::ensure(faces);
266+
if(!fbuf){
267+
std::cerr << "Faces input array cannot be interpreted as a mesh.\n";
268+
return {};
269+
}
270+
if (fbuf.ndim() != 2 || vbuf.shape(1) != 3){
271+
std::cerr << "Faces input array cannot be interpreted as a mesh.\n";
272+
std::cerr << "Shape must be N x 3 (v0, v1, v2).\n";
273+
return {};
274+
}
275+
276+
StridedSpan<Vec3,3, float> verticesIter{vbuf.data(), (size_t)vbuf.shape(0)};
277+
StridedSpan<Face,3,size_t> facesIter{fbuf.data(), (size_t)fbuf.shape(0)};
278+
return findConnectedComponents(verticesIter, facesIter);
279+
}, "vertices"_a,"faces"_a, "Convert the mesh from vertices and faces to triangles");
280+
}
281+
244282
PYBIND11_MODULE(openstl, m) {
245283
serialize(m);
246284
convertSubmodule(m);
285+
topologySubmodule(m);
247286
m.attr("__version__") = OPENSTL_PROJECT_VER;
248287
m.doc() = "A simple STL serializer and deserializer";
249288

File renamed without changes.

tests/core/src/disjointsets.test.cpp

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#include <catch2/catch_test_macros.hpp>
2+
#include "openstl/core/stl.h"
3+
4+
using namespace openstl;
5+
6+
TEST_CASE("DisjointSet basic operations", "[DisjointSet]") {
7+
DisjointSet ds(10);
8+
9+
SECTION("Initial state") {
10+
for (size_t i = 0; i < 10; ++i) {
11+
REQUIRE(ds.find(i) == i);
12+
}
13+
}
14+
15+
SECTION("Union operation") {
16+
ds.unite(0, 1);
17+
ds.unite(2, 3);
18+
ds.unite(1, 3);
19+
20+
REQUIRE(ds.connected(0, 3));
21+
REQUIRE(ds.connected(1, 2));
22+
REQUIRE(!ds.connected(0, 4));
23+
}
24+
25+
SECTION("Find with path compression") {
26+
ds.unite(4, 5);
27+
ds.unite(5, 6);
28+
REQUIRE(ds.find(6) == ds.find(4));
29+
REQUIRE(ds.find(5) == ds.find(4));
30+
}
31+
32+
SECTION("Disconnected sets") {
33+
ds.unite(7, 8);
34+
REQUIRE(!ds.connected(7, 9));
35+
REQUIRE(ds.connected(7, 8));
36+
}
37+
}
38+
39+
TEST_CASE("Find connected components of faces", "[findConnectedComponents]") {
40+
std::vector<std::array<float, 3>> vertices = {
41+
{0.0f, 0.0f, 0.0f},
42+
{1.0f, 0.0f, 0.0f},
43+
{0.0f, 1.0f, 0.0f},
44+
{1.0f, 1.0f, 0.0f},
45+
{0.5f, 0.5f, 1.0f},
46+
};
47+
48+
std::vector<std::array<size_t, 3>> faces = {
49+
{0, 1, 2},
50+
{1, 3, 2},
51+
{2, 3, 4},
52+
};
53+
54+
SECTION("Single connected component") {
55+
auto connectedComponents = findConnectedComponents(vertices, faces);
56+
REQUIRE(connectedComponents.size() == 1);
57+
REQUIRE(connectedComponents[0].size() == 3);
58+
}
59+
60+
SECTION("Multiple disconnected components") {
61+
faces.push_back({5, 6, 7});
62+
vertices.push_back({2.0f, 2.0f, 0.0f});
63+
vertices.push_back({3.0f, 2.0f, 0.0f});
64+
vertices.push_back({2.5f, 3.0f, 0.0f});
65+
66+
auto connectedComponents = findConnectedComponents(vertices, faces);
67+
REQUIRE(connectedComponents.size() == 2);
68+
REQUIRE(connectedComponents[0].size() == 3);
69+
REQUIRE(connectedComponents[1].size() == 1);
70+
}
71+
72+
SECTION("No faces provided") {
73+
faces.clear();
74+
auto connectedComponents = findConnectedComponents(vertices, faces);
75+
REQUIRE(connectedComponents.empty());
76+
}
77+
78+
SECTION("Single face") {
79+
faces = {{0, 1, 2}};
80+
auto connectedComponents = findConnectedComponents(vertices, faces);
81+
REQUIRE(connectedComponents.size() == 1);
82+
REQUIRE(connectedComponents[0].size() == 1);
83+
REQUIRE(connectedComponents[0][0] == std::array<size_t, 3>{0, 1, 2});
84+
}
85+
86+
SECTION("Disconnected vertices") {
87+
vertices.push_back({10.0f, 10.0f, 10.0f}); // Add an isolated vertex
88+
auto connectedComponents = findConnectedComponents(vertices, faces);
89+
REQUIRE(connectedComponents.size() == 1);
90+
REQUIRE(connectedComponents[0].size() == 3); // Only faces contribute
91+
}
92+
}

0 commit comments

Comments
 (0)