Skip to content

Commit

Permalink
Merge pull request #22 from Innoptech/connected-components
Browse files Browse the repository at this point in the history
Introducing connected components
  • Loading branch information
jeanchristopheruel authored Jan 15, 2025
2 parents 12de386 + b7c5580 commit 50651c8
Show file tree
Hide file tree
Showing 12 changed files with 546 additions and 154 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ ReadVersion(${PYPROJECT_PATH})
#-------------------------------------------------------------------------------
# COMPILATION
#-------------------------------------------------------------------------------
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

Expand Down
102 changes: 99 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,40 @@ The fastest and most intuitive library to manipulate STL files (stereolithograph
🌟 :fist_raised: Please consider starring and sponsoring the GitHub repo to show your support! :fist_raised: 🌟
![GitHub Sponsor](https://img.shields.io/github/sponsors/Innoptech?label=Sponsor&logo=GitHub)

## Index
1. **Performance**
- [Performance Benchmark](#performances-benchmark)

2. **Python Usage**
- [Install](#install)
- [Read and Write STL Files](#read-and-write-from-a-stl-file)
- [Rotate, Translate, and Scale Meshes](#rotate-translate-and-scale-a-mesh)
- [Convert Between Triangles and Vertices/Faces](#convert-triangles-arrow_right-vertices-and-faces)
- [Find Connected Components](#find-connected-components-in-mesh-topology-disjoint-solids)
- [Use with PyTorch](#use-with-pytorch)
- [Handling Large STL Files](#read-large-stl-file)

3. **C++ Usage**
- [Read STL from File](#read-stl-from-file)
- [Write STL to File](#write-stl-to-a-file)
- [Serialize STL to Stream](#serialize-stl-to-a-stream)
- [Convert Between Triangles and Vertices/Faces](#convert-triangles-arrow_right-vertices-and-faces-1)
- [Find Connected Components](#find-connected-components-in-mesh-topology)

4. **C++ Integration**
- [Smart Method with CMake](#smart-method)
- [Naïve Method](#naïve-method)

5. **Testing**
- [Run Tests](#test)

6. **Requirements**
- [C++ Standards](#requirements)

7. **Disclaimer**
- [STL File Format Limitations](#disclaimer-stl-file-format)


# Performances benchmark
Discover the staggering performance of OpenSTL in comparison to [numpy-stl](https://github.com/wolph/numpy-stl),
[meshio](https://github.com/nschloe/meshio) and [stl-reader](https://github.com/pyvista/stl-reader), thanks to its powerful C++ backend.
Expand Down Expand Up @@ -124,6 +158,35 @@ faces = [
triangles = openstl.convert.triangles(vertices, faces)
```

### Find Connected Components in Mesh Topology (Disjoint solids)
```python
import openstl

# Define vertices and faces for two disconnected components
vertices = [
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[2.0, 2.0, 0.0],
[3.0, 2.0, 0.0],
[2.5, 3.0, 0.0],
]

faces = [
[0, 1, 2], # Component 1
[3, 4, 5], # Component 2
]

# Identify connected components of faces
connected_components = openstl.topology.find_connected_components(vertices, faces)

# Print the result
print(f"Number of connected components: {len(connected_components)}")
for i, component in enumerate(connected_components):
print(f"Component {i + 1}: {component}")
```


### Use with `Pytorch`
```python
import openstl
Expand All @@ -148,7 +211,7 @@ scale = 1000.0
quad[:,1:4,:] *= scale # Avoid scaling normals
```

### Read large STL file
### Read large STL file
To read STL file with a large triangle count > **1 000 000**, the openstl buffer overflow safety must be unactivated with
`openstl.set_activate_overflow_safety(False)` after import. Deactivating overflow safety may expose the application
to a potential buffer overflow attack vector since the stl standard is not backed by a checksum.
Expand Down Expand Up @@ -223,7 +286,40 @@ std::vector<Face> faces = {
const auto& triangles = convertToTriangles(vertices, faces);
```
# Integrate to your codebase
### Find Connected Components in Mesh Topology
```c++
#include <openstl/topology.hpp>
#include <vector>
#include <iostream>
using namespace openstl;
int main() {
std::vector<Vec3> vertices = {
{0.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, // Component 1
{2.0f, 2.0f, 0.0f}, {3.0f, 2.0f, 0.0f}, {2.5f, 3.0f, 0.0f} // Component 2
};
std::vector<Face> faces = {
{0, 1, 2}, // Component 1
{3, 4, 5}, // Component 2
};
const auto& connected_components = findConnectedComponents(vertices, faces);
std::cout << "Number of connected components: " << connected_components.size() << "\\n";
for (size_t i = 0; i < connected_components.size(); ++i) {
std::cout << "Component " << i + 1 << ":\\n";
for (const auto& face : connected_components[i]) {
std::cout << " {" << face[0] << ", " << face[1] << ", " << face[2] << "}\\n";
}
}
return 0;
}
```
****
# Integrate to your C++ codebase
### Smart method
Include this repository with CMAKE Fetchcontent and link your executable/library to `openstl::core` library.
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.
Expand All @@ -250,7 +346,7 @@ ctest .
```

# Requirements
C++11 or higher.
C++17 or higher.


# DISCLAIMER: STL File Format #
Expand Down
72 changes: 70 additions & 2 deletions modules/core/include/openstl/core/stl.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ namespace openstl
* A library-level configuration to activate/deactivate the buffer overflow safety
* @return
*/
bool& activateOverflowSafety() {
inline bool& activateOverflowSafety() {
static bool safety_enabled = true;
return safety_enabled;
}
Expand Down Expand Up @@ -284,7 +284,7 @@ namespace openstl
}

//---------------------------------------------------------------------------------------------------------
// Transformation Utils
// Conversion Utils
//---------------------------------------------------------------------------------------------------------
using Face = std::array<size_t, 3>; // v0, v1, v2

Expand Down Expand Up @@ -389,5 +389,73 @@ namespace openstl
}
return triangles;
}

//---------------------------------------------------------------------------------------------------------
// Topology Utils
//---------------------------------------------------------------------------------------------------------
/**
* DisjointSet class to manage disjoint sets with union-find.
*/
class DisjointSet {
std::vector<size_t> parent;
std::vector<size_t> rank;

public:
explicit DisjointSet(size_t size) : parent(size), rank(size, 0) {
for (size_t i = 0; i < size; ++i) parent[i] = i;
}

size_t find(size_t x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}

void unite(size_t x, size_t y) {
size_t rootX = find(x), rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] < rank[rootY]) parent[rootX] = rootY;
else if (rank[rootX] > rank[rootY]) parent[rootY] = rootX;
else {
parent[rootY] = rootX;
++rank[rootX];
}
}
}

bool connected(size_t x, size_t y) {
return find(x) == find(y);
}
};

/**
* Identifies and groups connected components of faces based on shared vertices.
*
* @param vertices A container of vertices.
* @param faces A container of faces, where each face is a collection of vertex indices.
* @return A vector of connected components, where each component is a vector of faces.
*/
template<typename ContainerA, typename ContainerB>
inline std::vector<std::vector<Face>>
findConnectedComponents(const ContainerA& vertices, const ContainerB& faces) {
DisjointSet ds{vertices.size()};
for (const auto& tri : faces) {
ds.unite(tri[0], tri[1]);
ds.unite(tri[0], tri[2]);
}

std::vector<std::vector<Face>> result;
std::unordered_map<size_t, size_t> rootToIndex;

for (const auto& tri : faces) {
size_t root = ds.find(tri[0]);
if (rootToIndex.find(root) == rootToIndex.end()) {
rootToIndex[root] = result.size();
result.emplace_back();
}
result[rootToIndex[root]].push_back(tri);
}
return result;
}

} //namespace openstl
#endif //OPENSTL_OPENSTL_SERIALIZE_H
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ testpaths = ["tests/python"]

[tool.commitizen]
name = "cz_conventional_commits"
version = "1.2.10"
version = "1.3.0"
tag_format = "v$version"

[tool.cibuildwheel]
Expand Down
39 changes: 39 additions & 0 deletions python/core/src/stl.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
#include <pybind11/iostream.h>
#include <memory>
Expand Down Expand Up @@ -241,9 +242,47 @@ void convertSubmodule(py::module_ &_m)
}, "vertices"_a,"faces"_a, "Convert the mesh from vertices and faces to triangles");
}

void topologySubmodule(py::module_ &_m)
{
auto m = _m.def_submodule("topology", "A submodule for analyzing and segmenting connected components in mesh topology.");

m.def("find_connected_components", [](
const py::array_t<float, py::array::c_style | py::array::forcecast> &vertices,
const py::array_t<size_t, py::array::c_style | py::array::forcecast> &faces
) -> std::vector<std::vector<Face>>
{
py::scoped_ostream_redirect stream(std::cerr,py::module_::import("sys").attr("stderr"));
auto vbuf = py::array_t<float, py::array::c_style | py::array::forcecast>::ensure(vertices);
if(!vbuf){
std::cerr << "Vertices input array cannot be interpreted as a mesh.\n";
return {};
}
if (vbuf.ndim() != 2 || vbuf.shape(1) != 3){
std::cerr << "Vertices input array cannot be interpreted as a mesh. Shape must be N x 3.\n";
return {};
}

auto fbuf = py::array_t<size_t , py::array::c_style | py::array::forcecast>::ensure(faces);
if(!fbuf){
std::cerr << "Faces input array cannot be interpreted as a mesh.\n";
return {};
}
if (fbuf.ndim() != 2 || vbuf.shape(1) != 3){
std::cerr << "Faces input array cannot be interpreted as a mesh.\n";
std::cerr << "Shape must be N x 3 (v0, v1, v2).\n";
return {};
}

StridedSpan<Vec3,3, float> verticesIter{vbuf.data(), (size_t)vbuf.shape(0)};
StridedSpan<Face,3,size_t> facesIter{fbuf.data(), (size_t)fbuf.shape(0)};
return findConnectedComponents(verticesIter, facesIter);
}, "vertices"_a,"faces"_a, "Convert the mesh from vertices and faces to triangles");
}

PYBIND11_MODULE(openstl, m) {
serialize(m);
convertSubmodule(m);
topologySubmodule(m);
m.attr("__version__") = OPENSTL_PROJECT_VER;
m.doc() = "A simple STL serializer and deserializer";

Expand Down
File renamed without changes.
92 changes: 92 additions & 0 deletions tests/core/src/disjointsets.test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#include <catch2/catch_test_macros.hpp>
#include "openstl/core/stl.h"

using namespace openstl;

TEST_CASE("DisjointSet basic operations", "[DisjointSet]") {
DisjointSet ds(10);

SECTION("Initial state") {
for (size_t i = 0; i < 10; ++i) {
REQUIRE(ds.find(i) == i);
}
}

SECTION("Union operation") {
ds.unite(0, 1);
ds.unite(2, 3);
ds.unite(1, 3);

REQUIRE(ds.connected(0, 3));
REQUIRE(ds.connected(1, 2));
REQUIRE(!ds.connected(0, 4));
}

SECTION("Find with path compression") {
ds.unite(4, 5);
ds.unite(5, 6);
REQUIRE(ds.find(6) == ds.find(4));
REQUIRE(ds.find(5) == ds.find(4));
}

SECTION("Disconnected sets") {
ds.unite(7, 8);
REQUIRE(!ds.connected(7, 9));
REQUIRE(ds.connected(7, 8));
}
}

TEST_CASE("Find connected components of faces", "[findConnectedComponents]") {
std::vector<std::array<float, 3>> vertices = {
{0.0f, 0.0f, 0.0f},
{1.0f, 0.0f, 0.0f},
{0.0f, 1.0f, 0.0f},
{1.0f, 1.0f, 0.0f},
{0.5f, 0.5f, 1.0f},
};

std::vector<std::array<size_t, 3>> faces = {
{0, 1, 2},
{1, 3, 2},
{2, 3, 4},
};

SECTION("Single connected component") {
auto connectedComponents = findConnectedComponents(vertices, faces);
REQUIRE(connectedComponents.size() == 1);
REQUIRE(connectedComponents[0].size() == 3);
}

SECTION("Multiple disconnected components") {
faces.push_back({5, 6, 7});
vertices.push_back({2.0f, 2.0f, 0.0f});
vertices.push_back({3.0f, 2.0f, 0.0f});
vertices.push_back({2.5f, 3.0f, 0.0f});

auto connectedComponents = findConnectedComponents(vertices, faces);
REQUIRE(connectedComponents.size() == 2);
REQUIRE(connectedComponents[0].size() == 3);
REQUIRE(connectedComponents[1].size() == 1);
}

SECTION("No faces provided") {
faces.clear();
auto connectedComponents = findConnectedComponents(vertices, faces);
REQUIRE(connectedComponents.empty());
}

SECTION("Single face") {
faces = {{0, 1, 2}};
auto connectedComponents = findConnectedComponents(vertices, faces);
REQUIRE(connectedComponents.size() == 1);
REQUIRE(connectedComponents[0].size() == 1);
REQUIRE(connectedComponents[0][0] == std::array<size_t, 3>{0, 1, 2});
}

SECTION("Disconnected vertices") {
vertices.push_back({10.0f, 10.0f, 10.0f}); // Add an isolated vertex
auto connectedComponents = findConnectedComponents(vertices, faces);
REQUIRE(connectedComponents.size() == 1);
REQUIRE(connectedComponents[0].size() == 3); // Only faces contribute
}
}
Loading

0 comments on commit 50651c8

Please sign in to comment.