Skip to content

Commit bb9b60c

Browse files
authored
Merge pull request #161 from diffCheckOrg/feature/pose_comparison
Feature/pose comparison
2 parents 7d41f20 + 2395fb0 commit bb9b60c

File tree

21 files changed

+262
-154
lines changed

21 files changed

+262
-154
lines changed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
cmake_minimum_required(VERSION 3.16.)
2-
project(diffCheck VERSION 1.3.0 LANGUAGES CXX C)
2+
project(diffCheck VERSION 1.3.1 LANGUAGES CXX C)
33
set(CMAKE_CXX_STANDARD 17)
44

55
list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)

deps/eigen

Submodule eigen updated from 81044ec to 954e211

manifest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: diffCheck
3-
version: 1.3.0
3+
version: 1.3.1
44
authors:
55
- Andrea Settimi
66
- Damien Gilliard

src/diffCheck/geometry/DFPointCloud.cc

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

219-
std::vector<Eigen::Vector3d> DFPointCloud::GetPrincipalAxes(int nComponents)
219+
Eigen::Vector3d DFPointCloud::FitPlaneRANSAC(
220+
double distanceThreshold,
221+
int ransacN,
222+
int numIterations)
220223
{
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)
224+
if (this->Points.size() < ransacN)
249225
{
250-
sortedClustersBySize[i] = {clusterSizes[i], centroids.col(i)};
226+
DIFFCHECK_ERROR("Not enough points to fit a plane with RANSAC.");
227+
return Eigen::Vector3d::Zero();
251228
}
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;
229+
230+
auto O3DPointCloud = this->Cvt2O3DPointCloud();
231+
std::tuple< Eigen::Vector4d, std::vector<size_t>> planeModel = O3DPointCloud->SegmentPlane(distanceThreshold, ransacN, numIterations);
232+
Eigen::Vector3d planeParameters = std::get<0>(planeModel).head<3>();
233+
return planeParameters;
287234
}
288235

289236
void DFPointCloud::Crop(const Eigen::Vector3d &minBound, const Eigen::Vector3d &maxBound)

src/diffCheck/geometry/DFPointCloud.hh

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,17 @@ namespace diffCheck::geometry
9292
void RemoveStatisticalOutliers(int nbNeighbors, double stdRatio);
9393

9494
/**
95-
* @brief Get the nCompoments principal axes of the normals of the point cloud
96-
* It is used to compute the pose of "boxy" point clouds. It relies on KMeans clustering to find the main axes of the point cloud.
97-
* @param nComponents the number of components to compute (default 6, each of 3 main axes in both directions)
98-
* @return std::vector<Eigen::Vector3d> the principal axes of the point cloud ordered by number of normals
95+
* @brief Fit a plane to the point cloud using RANSAC
96+
*
97+
* @param distanceThreshold the distance threshold to consider a point as an inlier
98+
* @param ransacN the number of points to sample for each RANSAC iteration
99+
* @param numIterations the number of RANSAC iterations
100+
* @return The Normal vector of the fitted plane as an Eigen::Vector3d
99101
*/
100-
std::vector<Eigen::Vector3d> GetPrincipalAxes(int nComponents = 6);
102+
Eigen::Vector3d FitPlaneRANSAC(
103+
double distanceThreshold = 0.01,
104+
int ransacN = 3,
105+
int numIterations = 100);
101106

102107
/**
103108
* @brief Crop the point cloud to a bounding box defined by the min and max bounds

src/diffCheck/segmentation/DFSegmentation.cc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ namespace diffCheck::segmentation
330330
void DFSegmentation::CleanUnassociatedClusters(
331331
bool isCylinder,
332332
std::vector<std::shared_ptr<geometry::DFPointCloud>> &unassociatedClusters,
333-
std::vector<std::shared_ptr<geometry::DFPointCloud>> &existingPointCloudSegments,
333+
std::vector<std::vector<std::shared_ptr<geometry::DFPointCloud>>> &existingPointCloudSegments,
334334
std::vector<std::vector<std::shared_ptr<geometry::DFMesh>>> meshes,
335335
double angleThreshold,
336336
double associationThreshold)
@@ -459,12 +459,12 @@ namespace diffCheck::segmentation
459459
DIFFCHECK_WARN("No mesh face found for the cluster. Skipping the cluster.");
460460
continue;
461461
}
462-
if (goodMeshIndex >= existingPointCloudSegments.size())
462+
if (goodMeshIndex >= existingPointCloudSegments.size() || goodFaceIndex >= existingPointCloudSegments[goodMeshIndex].size())
463463
{
464464
DIFFCHECK_WARN("No segment found for the face. Skipping the face.");
465465
continue;
466466
}
467-
std::shared_ptr<geometry::DFPointCloud> completed_segment = existingPointCloudSegments[goodMeshIndex];
467+
std::shared_ptr<geometry::DFPointCloud> completed_segment = existingPointCloudSegments[goodMeshIndex][goodFaceIndex];
468468

469469
for (Eigen::Vector3d point : cluster->Points)
470470
{

src/diffCheck/segmentation/DFSegmentation.hh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@ namespace diffCheck::segmentation
4444
/** @brief Iterated through clusters and finds the corresponding mesh face. It then associates the points of the cluster that are on the mesh face to the segment already associated with the mesh face.
4545
* @param isCylinder a boolean to indicate if the model is a cylinder. If true, the method will use the GetCenterAndAxis method of the mesh to find the center and axis of the mesh. based on that, we only want points that have normals more or less perpendicular to the cylinder axis.
4646
* @param unassociatedClusters the clusters from the normal-based segmentatinon that haven't been associated yet.
47-
* @param existingPointCloudSegments the already associated segments
47+
* @param existingPointCloudSegments the already associated segments per mesh face.
4848
* @param meshes the mesh faces for all the model. This is used to associate the clusters to the mesh faces.
4949
* * @param angleThreshold the threshold to consider the a cluster as potential candidate for association. the value passed is the minimum sine of the angles. A value of 0 requires perfect alignment (angle = 0), while a value of 0.1 allows an angle of 5.7 degrees.
5050
* @param associationThreshold the threshold to consider the points of a segment and a mesh face as associable. It is the ratio between the surface of the closest mesh triangle and the sum of the areas of the three triangles that form the rest of the pyramid described by the mesh triangle and the point we want to associate or not. The lower the number, the more strict the association will be and some poinnts on the mesh face might be wrongfully excluded.
5151
*/
5252
static void DFSegmentation::CleanUnassociatedClusters(
5353
bool isCylinder,
5454
std::vector<std::shared_ptr<geometry::DFPointCloud>> &unassociatedClusters,
55-
std::vector<std::shared_ptr<geometry::DFPointCloud>> &existingPointCloudSegments,
55+
std::vector<std::vector<std::shared_ptr<geometry::DFPointCloud>>> &existingPointCloudSegments,
5656
std::vector<std::vector<std::shared_ptr<geometry::DFMesh>>> meshes,
5757
double angleThreshold = 0.1,
5858
double associationThreshold = 0.1);

src/diffCheckBindings.cc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,11 @@ PYBIND11_MODULE(diffcheck_bindings, m) {
6161
.def("remove_statistical_outliers", &diffCheck::geometry::DFPointCloud::RemoveStatisticalOutliers,
6262
py::arg("nb_neighbors"), py::arg("std_ratio"))
6363

64-
.def("get_principal_axes", &diffCheck::geometry::DFPointCloud::GetPrincipalAxes,
65-
py::arg("n_components") = 6)
64+
.def("fit_plane_ransac", &diffCheck::geometry::DFPointCloud::FitPlaneRANSAC,
65+
py::arg("distance_threshold") = 0.01,
66+
py::arg("ransac_n") = 3,
67+
py::arg("num_iterations") = 100)
68+
6669
.def("crop",
6770
(void (diffCheck::geometry::DFPointCloud::*)(const Eigen::Vector3d&, const Eigen::Vector3d&))
6871
&diffCheck::geometry::DFPointCloud::Crop,

src/gh/components/DF_CAD_segmentator/code.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import Rhino
66
from ghpythonlib.componentbase import executingcomponent as component
77
from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML
8+
import ghpythonlib.treehelpers as th
89

910

1011
from diffCheck.diffcheck_bindings import dfb_segmentation
@@ -19,7 +20,7 @@ def RunScript(self,
1920
i_clouds: System.Collections.Generic.IList[Rhino.Geometry.PointCloud],
2021
i_assembly,
2122
i_angle_threshold: float = 0.1,
22-
i_association_threshold: float = 0.1) -> Rhino.Geometry.PointCloud:
23+
i_association_threshold: float = 0.1):
2324

2425
if i_clouds is None or i_assembly is None:
2526
self.AddRuntimeMessage(RML.Warning, "Please provide a cloud and an assembly to segment.")
@@ -29,20 +30,18 @@ def RunScript(self,
2930
if i_association_threshold is None:
3031
i_association_threshold = 0.1
3132

32-
o_clusters = []
33+
o_face_clusters = []
3334
df_clusters = []
3435
# we make a deepcopy of the input clouds
3536
df_clouds = [df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud.Duplicate()) for cloud in i_clouds]
3637

3738
df_beams = i_assembly.beams
38-
df_beams_meshes = []
39-
rh_beams_meshes = []
4039

4140
for df_b in df_beams:
41+
o_face_clusters.append([])
42+
4243
rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces]
4344
df_b_mesh_faces = [df_cvt_bindings.cvt_rhmesh_2_dfmesh(rh_b_mesh_face) for rh_b_mesh_face in rh_b_mesh_faces]
44-
df_beams_meshes.append(df_b_mesh_faces)
45-
rh_beams_meshes.append(rh_b_mesh_faces)
4645

4746
# different association depending on the type of beam
4847
df_asssociated_cluster_faces = dfb_segmentation.DFSegmentation.associate_clusters(
@@ -53,27 +52,30 @@ def RunScript(self,
5352
association_threshold=i_association_threshold
5453
)
5554

56-
df_asssociated_cluster = dfb_geometry.DFPointCloud()
57-
for df_associated_face in df_asssociated_cluster_faces:
58-
df_asssociated_cluster.add_points(df_associated_face)
59-
6055
dfb_segmentation.DFSegmentation.clean_unassociated_clusters(
6156
is_roundwood=df_b.is_roundwood,
6257
unassociated_clusters=df_clouds,
63-
associated_clusters=[df_asssociated_cluster],
58+
associated_clusters=[df_asssociated_cluster_faces],
6459
reference_mesh=[df_b_mesh_faces],
6560
angle_threshold=i_angle_threshold,
6661
association_threshold=i_association_threshold
6762
)
6863

64+
o_face_clusters[-1] = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_asssociated_cluster_faces]
65+
66+
df_asssociated_cluster = dfb_geometry.DFPointCloud()
67+
for df_associated_face in df_asssociated_cluster_faces:
68+
df_asssociated_cluster.add_points(df_associated_face)
69+
6970
df_clusters.append(df_asssociated_cluster)
7071

71-
o_clusters = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_clusters]
72+
o_beam_clouds = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_clusters]
7273

73-
for o_cluster in o_clusters:
74-
if not o_cluster.IsValid:
75-
o_cluster = None
74+
for i, o_beam_cloud in enumerate(o_beam_clouds):
75+
if not o_beam_cloud.IsValid:
76+
o_beam_clouds[i] = None
7677
ghenv.Component.AddRuntimeMessage(RML.Warning, "Some beams could not be segmented and were replaced by 'None'") # noqa: F821
7778

79+
o_face_clouds = th.list_to_tree(o_face_clusters)
7880

79-
return o_clusters
81+
return [o_beam_clouds, o_face_clouds]

0 commit comments

Comments
 (0)