Skip to content

Introducing connected components #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading