Skip to content

Commit 7d41f20

Browse files
Merge branch 'release/2.0.0' of https://github.com/diffCheckOrg/diffCheck into release/2.0.0
2 parents d5f437c + b2af2b8 commit 7d41f20

File tree

50 files changed

+2013
-127
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2013
-127
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ repos:
3434
types-requests==2.31.0,
3535
numpy==2.0.1,
3636
pytest==8.3.1,
37+
websockets>=10.4,
3738
types-setuptools>=71.1.0.20240818
3839
]
3940
args: [--config=pyproject.toml]

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,35 @@ gantt
8888
Fabrication of iterative prototype :fab, 2025-08-01, 2w
8989
```
9090

91+
92+
9193
## How to contribute
9294

9395
If you want to contribute to the project, please refer to the [contribution guidelines]([./CONTRIBUTING.md](https://diffcheckorg.github.io/diffCheck/contribute.html)).
96+
97+
## Logic
98+
The logic of the workflow is currently as follows:
99+
100+
```mermaid
101+
stateDiagram-v2
102+
state "[breps to assemble]" as s1
103+
state "scan of latest element placed" as s2
104+
state "get pose of i-th brep" as s3
105+
state "get pose of i-1-th brep" as s4
106+
state "compute pose of i-1-th element from scan" as s5
107+
state "compute pose difference" as s6
108+
state "compute pose correction" as s7
109+
state "assemble i-th-element" as s8
110+
state "i += 1" as s9
111+
[*]-->s2
112+
s1-->s3
113+
s1-->s4
114+
s2-->s5
115+
s5-->s6
116+
s4-->s6
117+
s6-->s7
118+
s3-->s7
119+
s7-->s8
120+
s8-->s9
121+
s9-->[*]
122+
```

deps/eigen

Submodule eigen updated from 11fd34c to 81044ec

deps/pybind11

Submodule pybind11 updated 144 files

environment.yml

48 Bytes
Binary file not shown.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ module = [
2020
"GH_IO.*",
2121
"clr.*",
2222
"diffcheck_bindings",
23-
"diffCheck.diffcheck_bindings"
23+
"diffCheck.diffcheck_bindings",
24+
"ghpythonlib.*"
2425
]
2526
ignore_missing_imports = true
2627

src/diffCheck.hh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
#include <loguru.hpp>
77

88
#include <cilantro/cilantro.hpp>
9+
#include <cilantro/clustering/kmeans.hpp>
10+
11+
#include <Eigen/Dense>
912

1013
// diffCheck includes
1114
#include "diffCheck/log.hh"

src/diffCheck/IOManager.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,11 @@ namespace diffCheck::io
6868
std::filesystem::path pathCloud = pathTestData / "test_pc_for_SOR_101pts_with_1_outlier.ply";
6969
return pathCloud.string();
7070
}
71+
72+
std::string GetTwoConnectedPlanesPlyPath()
73+
{
74+
std::filesystem::path pathTestData = GetTestDataDir();
75+
std::filesystem::path pathCloud = pathTestData / "two_connected_planes_with_normals.ply";
76+
return pathCloud.string();
77+
}
7178
} // namespace diffCheck::io

src/diffCheck/IOManager.hh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,6 @@ namespace diffCheck::io
4242
std::string GetRoofQuarterPlyPath();
4343
/// @brief Get the path to the plane point cloud with one outlier
4444
std::string GetPlanePCWithOneOutliers();
45+
/// @brief Get the path to the two connected planes ply test file
46+
std::string GetTwoConnectedPlanesPlyPath();
4547
} // namespace diffCheck::io

src/diffCheck/geometry/DFPointCloud.cc

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,114 @@ namespace diffCheck::geometry
216216
this->Normals.push_back(normal);
217217
}
218218

219+
std::vector<Eigen::Vector3d> DFPointCloud::GetPrincipalAxes(int nComponents)
220+
{
221+
std::vector<Eigen::Vector3d> principalAxes;
222+
223+
if (! this->HasNormals())
224+
{
225+
DIFFCHECK_WARN("The point cloud has no normals. Normals will be estimated with knn = 20.");
226+
this->EstimateNormals(true, 20);
227+
}
228+
229+
// Convert normals to Eigen matrix
230+
Eigen::Matrix<double, 3, Eigen::Dynamic> normalMatrix(3, this->Normals.size());
231+
for (size_t i = 0; i < this->Normals.size(); ++i)
232+
{
233+
normalMatrix.col(i) = this->Normals[i].cast<double>();
234+
}
235+
236+
cilantro::KMeans<double, 3> kmeans(normalMatrix);
237+
kmeans.cluster(nComponents);
238+
239+
const cilantro::VectorSet3d& centroids = kmeans.getClusterCentroids();
240+
const std::vector<size_t>& assignments = kmeans.getPointToClusterIndexMap();
241+
std::vector<int> clusterSizes(nComponents, 0);
242+
for (size_t i = 0; i < assignments.size(); ++i)
243+
{
244+
clusterSizes[assignments[i]]++;
245+
}
246+
// Sort clusters by size
247+
std::vector<std::pair<int, Eigen::Vector3d>> sortedClustersBySize(nComponents);
248+
for (size_t i = 0; i < nComponents; ++i)
249+
{
250+
sortedClustersBySize[i] = {clusterSizes[i], centroids.col(i)};
251+
}
252+
std::sort(sortedClustersBySize.begin(), sortedClustersBySize.end(), [](const auto& a, const auto& b)
253+
{
254+
return a.first > b.first;
255+
});
256+
257+
for(size_t i = 0; i < nComponents; ++i)
258+
{
259+
if(principalAxes.size() == 0)
260+
{
261+
principalAxes.push_back(sortedClustersBySize[i].second);
262+
}
263+
else
264+
{
265+
bool isAlreadyPresent = false;
266+
for (const auto& axis : principalAxes)
267+
{
268+
double dotProduct = std::abs(axis.dot(sortedClustersBySize[i].second));
269+
if (std::abs(dotProduct) > 0.7) // Threshold to consider as similar direction
270+
{
271+
isAlreadyPresent = true;
272+
break;
273+
}
274+
}
275+
if (!isAlreadyPresent)
276+
{
277+
principalAxes.push_back(sortedClustersBySize[i].second);
278+
}
279+
}
280+
}
281+
if (principalAxes.size() < 2) // Fallback to OBB if k-means fails to provide enough distinct axes
282+
{
283+
open3d::geometry::OrientedBoundingBox obb = this->Cvt2O3DPointCloud()->GetOrientedBoundingBox();
284+
principalAxes = {obb.R_.col(0), obb.R_.col(1), obb.R_.col(2)};
285+
}
286+
return principalAxes;
287+
}
288+
289+
void DFPointCloud::Crop(const Eigen::Vector3d &minBound, const Eigen::Vector3d &maxBound)
290+
{
291+
auto O3DPointCloud = this->Cvt2O3DPointCloud();
292+
auto O3DPointCloudCropped = O3DPointCloud->Crop(open3d::geometry::AxisAlignedBoundingBox(minBound, maxBound));
293+
this->Points.clear();
294+
for (auto &point : O3DPointCloudCropped->points_)
295+
this->Points.push_back(point);
296+
this->Colors.clear();
297+
for (auto &color : O3DPointCloudCropped->colors_)
298+
this->Colors.push_back(color);
299+
this->Normals.clear();
300+
for (auto &normal : O3DPointCloudCropped->normals_)
301+
this->Normals.push_back(normal);
302+
}
303+
304+
void DFPointCloud::Crop(const std::vector<Eigen::Vector3d> &corners)
305+
{
306+
if (corners.size() != 8)
307+
throw std::invalid_argument("The corners vector must contain exactly 8 points.");
308+
open3d::geometry::OrientedBoundingBox obb = open3d::geometry::OrientedBoundingBox::CreateFromPoints(corners);
309+
auto O3DPointCloud = this->Cvt2O3DPointCloud();
310+
auto O3DPointCloudCropped = O3DPointCloud->Crop(obb);
311+
this->Points.clear();
312+
for (auto &point : O3DPointCloudCropped->points_)
313+
this->Points.push_back(point);
314+
this->Colors.clear();
315+
for (auto &color : O3DPointCloudCropped->colors_)
316+
this->Colors.push_back(color);
317+
this->Normals.clear();
318+
for (auto &normal : O3DPointCloudCropped->normals_)
319+
this->Normals.push_back(normal);
320+
}
321+
322+
DFPointCloud DFPointCloud::Duplicate() const
323+
{
324+
return DFPointCloud(this->Points, this->Colors, this->Normals);
325+
}
326+
219327
void DFPointCloud::UniformDownsample(int everyKPoints)
220328
{
221329
auto O3DPointCloud = this->Cvt2O3DPointCloud();
@@ -258,6 +366,86 @@ namespace diffCheck::geometry
258366
return bboxPts;
259367
}
260368

369+
void DFPointCloud::SubtractPoints(const DFPointCloud &pointCloud, double distanceThreshold)
370+
{
371+
if (this->Points.size() == 0 || pointCloud.Points.size() == 0)
372+
throw std::invalid_argument("One of the point clouds is empty.");
373+
374+
auto O3DSourcePointCloud = this->Cvt2O3DPointCloud();
375+
auto O3DTargetPointCloud = std::make_shared<DFPointCloud>(pointCloud)->Cvt2O3DPointCloud();
376+
auto O3DResultPointCloud = std::make_shared<open3d::geometry::PointCloud>();
377+
378+
open3d::geometry::KDTreeFlann threeDTree;
379+
threeDTree.SetGeometry(*O3DTargetPointCloud);
380+
std::vector<int> indices;
381+
std::vector<double> distances;
382+
for (const auto &point : O3DSourcePointCloud->points_)
383+
{
384+
threeDTree.SearchRadius(point, distanceThreshold, indices, distances);
385+
if (indices.empty())
386+
{
387+
O3DResultPointCloud->points_.push_back(point);
388+
if (O3DSourcePointCloud->HasColors())
389+
{
390+
O3DResultPointCloud->colors_.push_back(O3DSourcePointCloud->colors_[&point - &O3DSourcePointCloud->points_[0]]);
391+
}
392+
if (O3DSourcePointCloud->HasNormals())
393+
{
394+
O3DResultPointCloud->normals_.push_back(O3DSourcePointCloud->normals_[&point - &O3DSourcePointCloud->points_[0]]);
395+
}
396+
}
397+
}
398+
this->Points.clear();
399+
for (auto &point : O3DResultPointCloud->points_)
400+
this->Points.push_back(point);
401+
if (O3DResultPointCloud->HasColors())
402+
{
403+
this->Colors.clear();
404+
for (auto &color : O3DResultPointCloud->colors_){this->Colors.push_back(color);};
405+
}
406+
if (O3DResultPointCloud->HasNormals())
407+
{
408+
this->Normals.clear();
409+
for (auto &normal : O3DResultPointCloud->normals_){this->Normals.push_back(normal);};
410+
}
411+
}
412+
413+
diffCheck::geometry::DFPointCloud DFPointCloud::Intersect(const DFPointCloud &pointCloud, double distanceThreshold)
414+
{
415+
if (this->Points.size() == 0 || pointCloud.Points.size() == 0)
416+
throw std::invalid_argument("One of the point clouds is empty.");
417+
418+
auto O3DSourcePointCloud = this->Cvt2O3DPointCloud();
419+
auto O3DTargetPointCloud = std::make_shared<DFPointCloud>(pointCloud)->Cvt2O3DPointCloud();
420+
auto O3DResultPointCloud = std::make_shared<open3d::geometry::PointCloud>();
421+
422+
open3d::geometry::KDTreeFlann threeDTree;
423+
threeDTree.SetGeometry(*O3DTargetPointCloud);
424+
std::vector<int> indices;
425+
std::vector<double> distances;
426+
for (const auto &point : O3DSourcePointCloud->points_)
427+
{
428+
threeDTree.SearchRadius(point, distanceThreshold, indices, distances);
429+
if (!indices.empty())
430+
{
431+
O3DResultPointCloud->points_.push_back(point);
432+
if (O3DSourcePointCloud->HasColors())
433+
{
434+
O3DResultPointCloud->colors_.push_back(O3DSourcePointCloud->colors_[&point - &O3DSourcePointCloud->points_[0]]);
435+
}
436+
if (O3DSourcePointCloud->HasNormals())
437+
{
438+
O3DResultPointCloud->normals_.push_back(O3DSourcePointCloud->normals_[&point - &O3DSourcePointCloud->points_[0]]);
439+
}
440+
}
441+
}
442+
diffCheck::geometry::DFPointCloud result;
443+
result.Points = O3DResultPointCloud->points_;
444+
result.Colors = O3DResultPointCloud->colors_;
445+
result.Normals = O3DResultPointCloud->normals_;
446+
return result;
447+
}
448+
261449
void DFPointCloud::ApplyTransformation(const diffCheck::transformation::DFTransformation &transformation)
262450
{
263451
auto O3DPointCloud = this->Cvt2O3DPointCloud();

0 commit comments

Comments
 (0)