From d5ed24e6ca791524e23eb6e1c2b86ee97f99c783 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:26:29 +0200 Subject: [PATCH 01/15] feat: add principal axes calculation for pointclouds for pose estimation --- src/diffCheck/geometry/DFPointCloud.cc | 53 ++++++++++++++++++++++++++ src/diffCheck/geometry/DFPointCloud.hh | 10 +++++ 2 files changed, 63 insertions(+) diff --git a/src/diffCheck/geometry/DFPointCloud.cc b/src/diffCheck/geometry/DFPointCloud.cc index d00fac65..67727e82 100644 --- a/src/diffCheck/geometry/DFPointCloud.cc +++ b/src/diffCheck/geometry/DFPointCloud.cc @@ -216,6 +216,59 @@ namespace diffCheck::geometry this->Normals.push_back(normal); } + std::vector DFPointCloud::GetPrincipalAxes(int nComponents) + { + std::vector principalAxes; + + if (! this->HasNormals()) + { + DIFFCHECK_WARN("The point cloud has no normals. Normals will be estimated with knn = 20."); + this->EstimateNormals(true, 20); + } + + // Convert normals to Eigen matrix + Eigen::Matrix normalMatrix(3, this->Normals.size()); + for (size_t i = 0; i < this->Normals.size(); ++i) + { + normalMatrix.col(i) = this->Normals[i].cast(); + } + + cilantro::KMeans kmeans(normalMatrix); + kmeans.cluster(nComponents); + + const auto& centroids = kmeans.getClusterCentroids(); + const auto& assignments = kmeans.getPointToClusterIndexMap(); + + std::vector> clusters(nComponents); + for (size_t i = 0; i < nComponents; ++i) + { + clusters[i] = {i, centroids.col(i)}; + } + std::sort(clusters.begin(), clusters.end(), [](const auto& a, const auto& b) + { + return a.second.norm() < b.second.norm(); + }); + + std::vector> sortedClusters(nComponents); + std::vector clusterSizes(nComponents, 0); + + std::vector> sortedClustersBySize(nComponents); + for (size_t i = 0; i < nComponents; ++i) + { + sortedClustersBySize[i] = {clusterSizes[i], centroids.col(i)}; + } + std::sort(sortedClustersBySize.begin(), sortedClustersBySize.end(), [](const auto& a, const auto& b) + { + return a.first > b.first; + }); + + for(size_t i = 0; i < nComponents; ++i) + { + principalAxes.push_back(sortedClustersBySize[i].second); + } + return principalAxes; + } + void DFPointCloud::UniformDownsample(int everyKPoints) { auto O3DPointCloud = this->Cvt2O3DPointCloud(); diff --git a/src/diffCheck/geometry/DFPointCloud.hh b/src/diffCheck/geometry/DFPointCloud.hh index b3f0a3be..3512aca0 100644 --- a/src/diffCheck/geometry/DFPointCloud.hh +++ b/src/diffCheck/geometry/DFPointCloud.hh @@ -8,6 +8,8 @@ #include #include +#include + namespace diffCheck::geometry { @@ -89,6 +91,14 @@ namespace diffCheck::geometry */ void RemoveStatisticalOutliers(int nbNeighbors, double stdRatio); + /** + * @brief Get the nCompoments principal axes of the normals of the point cloud + * 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. + * @param nComponents the number of components to compute (default 6, each of 3 main axes in both directions) + * @return std::vector the principal axes of the point cloud ordered by number of normals + */ + std::vector GetPrincipalAxes(int nComponents = 6); + public: ///< Downsamplers /** * @brief Downsample the point cloud with voxel grid From f43e19773604a2774d3e3afc8adec32a614bcb5c Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:29:56 +0200 Subject: [PATCH 02/15] fix: add cilantro KMeans clustering to main header --- src/diffCheck.hh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/diffCheck.hh b/src/diffCheck.hh index 733801aa..31dec076 100644 --- a/src/diffCheck.hh +++ b/src/diffCheck.hh @@ -6,6 +6,9 @@ #include #include +#include + +#include // diffCheck includes #include "diffCheck/log.hh" From 4809f3065c534fb82cf9fd272945711e272c0d2a Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 9 Apr 2025 18:02:33 +0200 Subject: [PATCH 03/15] feat: add empty component for axis calculation. code must be written --- src/gh/components/DF_main_pc_axes/code.py | 0 src/gh/components/DF_main_pc_axes/icon.png | Bin 0 -> 1159 bytes .../components/DF_main_pc_axes/metadata.json | 40 ++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 src/gh/components/DF_main_pc_axes/code.py create mode 100644 src/gh/components/DF_main_pc_axes/icon.png create mode 100644 src/gh/components/DF_main_pc_axes/metadata.json diff --git a/src/gh/components/DF_main_pc_axes/code.py b/src/gh/components/DF_main_pc_axes/code.py new file mode 100644 index 00000000..e69de29b diff --git a/src/gh/components/DF_main_pc_axes/icon.png b/src/gh/components/DF_main_pc_axes/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1240418fe6917f8ef52400a80c595c059514f48c GIT binary patch literal 1159 zcmV;21bF+2P)EX>4Tx04R}tkvmAkKpe)uKBOv@4t5Z6$WS|35MMY-6^me@v=v%)FuC*vO&XFE z7e~Rh;NWAi>fqw6tAnc`2tGjE1t&!pDe-?vp+$@b$Nl*3zK^@_0B^I(6vICbD5|Na z6LBGzUljtc@S_hQL@_NgLrZ2Bvv3_>_wexbF2=Lm&;2NI`&vIx48bM2uFI6a#76kGuGXZGVbf61gg1 z!Q6Iv>{Roe=;402y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00Li0L_t(Y$EB4`NK{c2$A52}O{;=zQE0XachZNrY0=_| zh=>d#84^@BXE|WsMG+KX2v&$e)Z8+Hh(ScfM5;Np%7wU^O;M|)F-Tj~rh;XFbmu@Dg(|!q9-V| zp-Dg81<3-&s~|-6TmbuWK_pCz|CADdl~p02t8ggfrSqnm9wtCeYGB79JroKWUm507E+llPTT|FFmSBo zJ+Sa0qjJgBYgg*o^{x@%S}u{j0sLG@WwtqS4*>!CjLLI@o94g=jmnjP-UmEeJ+Q>` z?vGgb))4qnqmo&-+byHLe}P!jRIB%WU!MG)_v?=2j&BX@oZsuM9$aF%J`|n?lE4w8 zGVaC}TU66D)ynt%U&uFnSXY^av(o&TuXUdQewBdt7?p|8tDp4KYrtDz)ZW%LJNv)@ zaL%aQ4r!+WP66FUWir%TY|rzK8@kR|3(OmpLuKFzqcUCUG{%8~QK>6)nqGwHkAQxl z%c#thyGv2*da(q&)2Pf=3XT^YvXX;Z_+#KC&|y^OqWD;p3f3%q3^)O_8 Date: Wed, 9 Apr 2025 20:52:59 +0200 Subject: [PATCH 04/15] feat: add python binding for axis calculation --- src/diffCheckBindings.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/diffCheckBindings.cc b/src/diffCheckBindings.cc index 434f5da2..d0256d0e 100644 --- a/src/diffCheckBindings.cc +++ b/src/diffCheckBindings.cc @@ -55,6 +55,9 @@ PYBIND11_MODULE(diffcheck_bindings, m) { .def("remove_statistical_outliers", &diffCheck::geometry::DFPointCloud::RemoveStatisticalOutliers, py::arg("nb_neighbors"), py::arg("std_ratio")) + .def("get_principal_axes", &diffCheck::geometry::DFPointCloud::GetPrincipalAxes, + py::arg("n_components") = 6) + .def("load_from_PLY", &diffCheck::geometry::DFPointCloud::LoadFromPLY) .def("save_to_PLY", &diffCheck::geometry::DFPointCloud::SaveToPLY) From ad8e895bedcc0c3a2b6c3b91c4e34c56af68ee92 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 7 May 2025 23:36:48 +0200 Subject: [PATCH 05/15] feat: add poses module for pose historic saving --- src/gh/diffCheck/diffCheck/df_poses.py | 67 ++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/gh/diffCheck/diffCheck/df_poses.py diff --git a/src/gh/diffCheck/diffCheck/df_poses.py b/src/gh/diffCheck/diffCheck/df_poses.py new file mode 100644 index 00000000..bb2149e8 --- /dev/null +++ b/src/gh/diffCheck/diffCheck/df_poses.py @@ -0,0 +1,67 @@ +from scriptcontext import sticky as rh_sticky_dict + +from dataclasses import dataclass, field + +@dataclass +class DFPose: + """ + This class represents the pose of a single element at a given time in the assembly process. + """ + origin: list + xDirection: list + yDirection: list + +@dataclass +class DFPosesBeam: + """ + This class contains the poses of a single beam, at different times in the assembly process. + """ + poses_dictionnary: dict + + def add_pose(self, pose: DFPose, step_number: int): + """ + Add a pose to the dictionary of poses. + """ + self.poses_dictionnary[f"pose_{step_number}"] = pose + +@dataclass +class DFPosesAssembly: + n_step: int = 0 + poses_per_element_dictionary: dict = field(default_factory=lambda: rh_sticky_dict) + + """ + This class contains the poses of the different elements of the assembly, at different times in the assembly process. + """ + def __post_init__(self): + """ + Initialize the poses_per_element_dictionary with empty DFPosesBeam objects. + """ + lengths = [] + for element in self.poses_per_element_dictionary: + lengths.append(len(self.poses_per_element_dictionary[element].poses_dictionnary)) + self.n_step = max(lengths) if lengths else 0 + + def add_step(self, new_poses: list[DFPose]): + for i, pose in enumerate(new_poses): + if f"element_{i}" not in self.poses_per_element_dictionary: + self.poses_per_element_dictionary[f"element_{i}"] = DFPosesBeam({}) + self.poses_per_element_dictionary[f"element_{i}"].add_pose(pose, self.n_step + 1) + self.n_step += 1 + + def get_last_poses(self): + """ + Get the last poses of each element. + """ + if self.n_step == 0: + return None + last_poses = [] + for i in range(len(self.poses_per_element_dictionary)): + last_poses.append(self.poses_per_element_dictionary[f"element_{i}"].poses_dictionnary[f"pose_{self.n_step}"]) + return last_poses + + def reset(self): + """ + Reset the assembly poses to the initial state. + """ + self.n_step = 0 + rh_sticky_dict.clear() From 4d5c71b9ee19deb4a877c6590bee69c35d7dc333 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 7 May 2025 23:44:44 +0200 Subject: [PATCH 06/15] feat-wip: add code to main axis calculation component. OK but reliability will be improved --- src/gh/components/DF_main_pc_axes/code.py | 89 +++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/gh/components/DF_main_pc_axes/code.py b/src/gh/components/DF_main_pc_axes/code.py index e69de29b..6d4cce87 100644 --- a/src/gh/components/DF_main_pc_axes/code.py +++ b/src/gh/components/DF_main_pc_axes/code.py @@ -0,0 +1,89 @@ +#! python3 + +from diffCheck import diffcheck_bindings +from diffCheck import df_cvt_bindings +from diffCheck import df_poses + +import Rhino + +from ghpythonlib.componentbase import executingcomponent as component +# from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML + +import System + +def compute_dot_product(v1, v2): + """ + Compute the dot product of two vectors. + """ + return v1.X * v2.X + v1.Y * v2.Y + v1.Z * v2.Z + +class DFMainPCAxes(component): + def RunScript(self, + i_clouds: System.Collections.Generic.List[Rhino.Geometry.PointCloud], + i_file_name: str, + reset: bool) -> System.Collections.Generic.IList[Rhino.Geometry.Vector3d]: + + planes = [] + all_poses_in_time = df_poses.DFPosesAssembly() + if reset: + all_poses_in_time.reset() + return None, None + + previous_poses = all_poses_in_time.get_last_poses() + all_poses_this_time = [] + for i, cloud in enumerate(i_clouds): + df_cloud = df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud) + if df_cloud is None: + return None, None + df_cloud.estimate_normals(True, 12) + + # hint = df_cvt_bindings.cvt_dfcloud_2_rhcloud(df_cloud) + df_points = df_cloud.get_axis_aligned_bounding_box() + df_point = (df_points[0] + df_points[1]) / 2 + rh_point = Rhino.Geometry.Point3d(df_point[0], df_point[1], df_point[2]) + vectors = [] + # Get the main axes of the point cloud + previous_pose = previous_poses[i] if previous_poses else None + if previous_pose: + rh_previous_xDirection = Rhino.Geometry.Vector3d(previous_pose.xDirection[0], previous_pose.xDirection[1], previous_pose.xDirection[2]) + rh_previous_yDirection = Rhino.Geometry.Vector3d(previous_pose.yDirection[0], previous_pose.yDirection[1], previous_pose.yDirection[2]) + n_faces = len(diffcheck_bindings.dfb_segmentation.DFSegmentation.segment_by_normal(df_cloud, 12, int(len(df_cloud.points)/20), True, int(len(df_cloud.points)/200), 1)) + axes = df_cloud.get_principal_axes(n_faces) + for axe in axes: + vectors.append(Rhino.Geometry.Vector3d(axe[0], axe[1], axe[2])) + if previous_pose: + # Sort the vectors by their alignment with the previous xDirection and yDirection + sorted_vectors_by_alignment = sorted(vectors, key=lambda v: compute_dot_product(v, rh_previous_xDirection), reverse=True) + sorted_vectors_by_perpendicularity = sorted(vectors, key=lambda v: compute_dot_product(v, rh_previous_yDirection), reverse=True) + new_xDirection = sorted_vectors_by_alignment[0] + new_yDirection = sorted_vectors_by_perpendicularity[0] #- compute_dot_product(sorted_vectors_by_perpendicularity[0], new_xDirection) * new_xDirection + new_yDirection.Unitize() + else: + # If no previous pose, just use the first two vectors as x and y directions + new_xDirection = vectors[0] + new_yDirection = vectors[1] #- compute_dot_product(vectors[1], new_xDirection) * new_xDirection + new_yDirection.Unitize() + + print(new_xDirection) + print(new_yDirection) + + pose = df_poses.DFPose( + origin = [rh_point.X, rh_point.Y, rh_point.Z], + xDirection = [new_xDirection.X, new_xDirection.Y, new_xDirection.Z], + yDirection = [new_yDirection.X, new_yDirection.Y, new_yDirection.Z]) + all_poses_this_time.append(pose) + plane = Rhino.Geometry.Plane(origin = rh_point, xDirection=new_xDirection, yDirection=new_yDirection) + planes.append(plane) + + all_poses_in_time.add_step(all_poses_this_time) + + return planes, all_poses_in_time.poses_per_element_dictionary + + +# if __name__ == "__main__": +# i_file_name = "C:/Users/localuser/test_file_poses.json" +# component = DFMainPCAxes() +# if reset == None: # noqa: E711 +# reset = False + +# a, dico = component.RunScript(x, i_file_name, reset) # noqa: F821 From 256fd886b4bb50c43f8a876ef13978421649f89e Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Fri, 9 May 2025 10:05:22 +0200 Subject: [PATCH 07/15] feat: add 'save' mathod to DFPoses classes to dump it in a JSON --- src/gh/diffCheck/diffCheck/df_poses.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/gh/diffCheck/diffCheck/df_poses.py b/src/gh/diffCheck/diffCheck/df_poses.py index bb2149e8..07926e22 100644 --- a/src/gh/diffCheck/diffCheck/df_poses.py +++ b/src/gh/diffCheck/diffCheck/df_poses.py @@ -1,5 +1,5 @@ from scriptcontext import sticky as rh_sticky_dict - +import json from dataclasses import dataclass, field @dataclass @@ -65,3 +65,10 @@ def reset(self): """ self.n_step = 0 rh_sticky_dict.clear() + + def save(self, file_path: str): + """ + Save the assembly poses to a JSON file. + """ + with open(file_path, 'w') as f: + json.dump(self.poses_per_element_dictionary, f, default=lambda o: o.__dict__, indent=4) From 3971d4061b2b77dba4e8e8757ef4ad044c9cb6ea Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Fri, 9 May 2025 22:52:18 +0200 Subject: [PATCH 08/15] fix: update the metadata of the axis calculation component --- .../components/DF_main_pc_axes/metadata.json | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/gh/components/DF_main_pc_axes/metadata.json b/src/gh/components/DF_main_pc_axes/metadata.json index d90aae96..e4cc1a10 100644 --- a/src/gh/components/DF_main_pc_axes/metadata.json +++ b/src/gh/components/DF_main_pc_axes/metadata.json @@ -1,9 +1,9 @@ { - "name": "DFMainAxes", - "nickname": "MainAxes", + "name": "DFPoseEstimation", + "nickname": "PoseEsimation", "category": "diffCheck", "subcategory": "PointCloud", - "description": "This compoment calculates the main axes of a point cloud. This is initially ment for pose estimation.", + "description": "This compoment calculates the pose of a list of point clouds.", "exposure": 4, "instanceGuid": "22b0c6fc-bc16-4ff5-b789-e99776277f65", "ghpython": { @@ -24,13 +24,33 @@ "wireDisplay": "default", "sourceCount": 0, "typeHintID": "pointcloud" + }, + { + "name": "i_reset", + "nickname": "i_reset", + "description": "reset the history of the pose estimation", + "optional": true, + "allowTreeAccess": false, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "boolean" } ], "outputParameters": [ { - "name": "o_vectors", - "nickname": "vectors", - "description": "The resulting vectors of the axis calculation.", + "name": "o_planes", + "nickname": "o_planes", + "description": "The resulting planes of the pose estimation in the last iteration.", + "optional": false, + "sourceCount": 0, + "graft": false + }, + { + "name": "o_history", + "nickname": "o_history", + "description": "The history of poses of all the elements.", "optional": false, "sourceCount": 0, "graft": false From 13c475977f952f3b212e803d6017791d2b280ee9 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Fri, 9 May 2025 23:35:22 +0200 Subject: [PATCH 09/15] feat: make pose calculation component leaner --- src/gh/components/DF_main_pc_axes/code.py | 42 +++++-------------- .../components/DF_main_pc_axes/metadata.json | 2 +- 2 files changed, 12 insertions(+), 32 deletions(-) diff --git a/src/gh/components/DF_main_pc_axes/code.py b/src/gh/components/DF_main_pc_axes/code.py index 6d4cce87..360e71e5 100644 --- a/src/gh/components/DF_main_pc_axes/code.py +++ b/src/gh/components/DF_main_pc_axes/code.py @@ -7,7 +7,6 @@ import Rhino from ghpythonlib.componentbase import executingcomponent as component -# from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML import System @@ -15,17 +14,16 @@ def compute_dot_product(v1, v2): """ Compute the dot product of two vectors. """ - return v1.X * v2.X + v1.Y * v2.Y + v1.Z * v2.Z + return (v1.X * v2.X) + (v1.Y * v2.Y) + (v1.Z * v2.Z) class DFMainPCAxes(component): def RunScript(self, i_clouds: System.Collections.Generic.List[Rhino.Geometry.PointCloud], - i_file_name: str, - reset: bool) -> System.Collections.Generic.IList[Rhino.Geometry.Vector3d]: + i_reset: bool): planes = [] all_poses_in_time = df_poses.DFPosesAssembly() - if reset: + if i_reset: all_poses_in_time.reset() return None, None @@ -37,7 +35,6 @@ def RunScript(self, return None, None df_cloud.estimate_normals(True, 12) - # hint = df_cvt_bindings.cvt_dfcloud_2_rhcloud(df_cloud) df_points = df_cloud.get_axis_aligned_bounding_box() df_point = (df_points[0] + df_points[1]) / 2 rh_point = Rhino.Geometry.Point3d(df_point[0], df_point[1], df_point[2]) @@ -47,25 +44,17 @@ def RunScript(self, if previous_pose: rh_previous_xDirection = Rhino.Geometry.Vector3d(previous_pose.xDirection[0], previous_pose.xDirection[1], previous_pose.xDirection[2]) rh_previous_yDirection = Rhino.Geometry.Vector3d(previous_pose.yDirection[0], previous_pose.yDirection[1], previous_pose.yDirection[2]) - n_faces = len(diffcheck_bindings.dfb_segmentation.DFSegmentation.segment_by_normal(df_cloud, 12, int(len(df_cloud.points)/20), True, int(len(df_cloud.points)/200), 1)) + n_faces = all_poses_in_time.poses_per_element_dictionary[f"element_{i}"].n_faces + else: + rh_previous_xDirection = None + rh_previous_yDirection = None + n_faces = len(diffcheck_bindings.dfb_segmentation.DFSegmentation.segment_by_normal(df_cloud, 12, int(len(df_cloud.points)/20), True, int(len(df_cloud.points)/200), 1)) + axes = df_cloud.get_principal_axes(n_faces) for axe in axes: vectors.append(Rhino.Geometry.Vector3d(axe[0], axe[1], axe[2])) - if previous_pose: - # Sort the vectors by their alignment with the previous xDirection and yDirection - sorted_vectors_by_alignment = sorted(vectors, key=lambda v: compute_dot_product(v, rh_previous_xDirection), reverse=True) - sorted_vectors_by_perpendicularity = sorted(vectors, key=lambda v: compute_dot_product(v, rh_previous_yDirection), reverse=True) - new_xDirection = sorted_vectors_by_alignment[0] - new_yDirection = sorted_vectors_by_perpendicularity[0] #- compute_dot_product(sorted_vectors_by_perpendicularity[0], new_xDirection) * new_xDirection - new_yDirection.Unitize() - else: - # If no previous pose, just use the first two vectors as x and y directions - new_xDirection = vectors[0] - new_yDirection = vectors[1] #- compute_dot_product(vectors[1], new_xDirection) * new_xDirection - new_yDirection.Unitize() - print(new_xDirection) - print(new_yDirection) + new_xDirection, new_yDirection = df_poses.select_vectors(vectors, rh_previous_xDirection, rh_previous_yDirection) pose = df_poses.DFPose( origin = [rh_point.X, rh_point.Y, rh_point.Z], @@ -77,13 +66,4 @@ def RunScript(self, all_poses_in_time.add_step(all_poses_this_time) - return planes, all_poses_in_time.poses_per_element_dictionary - - -# if __name__ == "__main__": -# i_file_name = "C:/Users/localuser/test_file_poses.json" -# component = DFMainPCAxes() -# if reset == None: # noqa: E711 -# reset = False - -# a, dico = component.RunScript(x, i_file_name, reset) # noqa: F821 + return [planes, all_poses_in_time] diff --git a/src/gh/components/DF_main_pc_axes/metadata.json b/src/gh/components/DF_main_pc_axes/metadata.json index e4cc1a10..1ba30408 100644 --- a/src/gh/components/DF_main_pc_axes/metadata.json +++ b/src/gh/components/DF_main_pc_axes/metadata.json @@ -35,7 +35,7 @@ "scriptParamAccess": "item", "wireDisplay": "default", "sourceCount": 0, - "typeHintID": "boolean" + "typeHintID": "bool" } ], "outputParameters": [ From 78231f73bd5c3e0b48a676c48e7602ed142b4c65 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Fri, 9 May 2025 23:37:51 +0200 Subject: [PATCH 10/15] feat: slight refactor of df_poses module --- src/gh/diffCheck/diffCheck/df_poses.py | 44 +++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/gh/diffCheck/diffCheck/df_poses.py b/src/gh/diffCheck/diffCheck/df_poses.py index 07926e22..8638eb93 100644 --- a/src/gh/diffCheck/diffCheck/df_poses.py +++ b/src/gh/diffCheck/diffCheck/df_poses.py @@ -15,8 +15,10 @@ class DFPose: class DFPosesBeam: """ This class contains the poses of a single beam, at different times in the assembly process. + It also contains the number of faces detected for this element, based on which the poses are calculated. """ poses_dictionnary: dict + n_faces: int = 3 def add_pose(self, pose: DFPose, step_number: int): """ @@ -24,6 +26,12 @@ def add_pose(self, pose: DFPose, step_number: int): """ self.poses_dictionnary[f"pose_{step_number}"] = pose + def set_n_faces(self, n_faces: int): + """ + Set the number of faces detected for this element. + """ + self.n_faces = n_faces + @dataclass class DFPosesAssembly: n_step: int = 0 @@ -44,7 +52,7 @@ def __post_init__(self): def add_step(self, new_poses: list[DFPose]): for i, pose in enumerate(new_poses): if f"element_{i}" not in self.poses_per_element_dictionary: - self.poses_per_element_dictionary[f"element_{i}"] = DFPosesBeam({}) + self.poses_per_element_dictionary[f"element_{i}"] = DFPosesBeam({}, 4) self.poses_per_element_dictionary[f"element_{i}"].add_pose(pose, self.n_step + 1) self.n_step += 1 @@ -72,3 +80,37 @@ def save(self, file_path: str): """ with open(file_path, 'w') as f: json.dump(self.poses_per_element_dictionary, f, default=lambda o: o.__dict__, indent=4) + + +def compute_dot_product(v1, v2): + """ + Compute the dot product of two vectors. + """ + return (v1.X * v2.X) + (v1.Y * v2.Y) + (v1.Z * v2.Z) + + +def select_vectors(vectors, previous_xDirection, previous_yDirection): + """ + Select the vectors that are aligned with the xDirection and yDirection. + """ + if previous_xDirection is not None and previous_yDirection is not None: + sorted_vectors_by_alignment = sorted(vectors, key=lambda v: compute_dot_product(v, previous_xDirection), reverse=True) + new_xDirection = sorted_vectors_by_alignment[0] + else: + new_xDirection = vectors[0] + + condidates_for_yDirection = [] + for v in vectors: + if compute_dot_product(v, new_xDirection) ** 2 < 0.5: + condidates_for_yDirection.append(v) + if previous_xDirection is not None and previous_yDirection is not None: + sorted_vectors_by_perpendicularity = sorted(condidates_for_yDirection, key=lambda v: compute_dot_product(v, previous_yDirection), reverse=True) + new_xDirection = sorted_vectors_by_alignment[0] + new_yDirection = sorted_vectors_by_perpendicularity[0] - compute_dot_product(sorted_vectors_by_perpendicularity[0], new_xDirection) * new_xDirection + new_yDirection.Unitize() + else: + new_xDirection = vectors[0] + sorted_vectors = sorted(vectors[1:], key=lambda v: compute_dot_product(v, new_xDirection)**2) + new_yDirection = sorted_vectors[0] - compute_dot_product(vectors[1], new_xDirection) * new_xDirection + new_yDirection.Unitize() + return new_xDirection, new_yDirection From d967ea867195cca45e8e20fc614f110024673364 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Fri, 9 May 2025 23:51:37 +0200 Subject: [PATCH 11/15] fix: simplify k-means clustering implementation --- src/diffCheck/geometry/DFPointCloud.cc | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/diffCheck/geometry/DFPointCloud.cc b/src/diffCheck/geometry/DFPointCloud.cc index 67727e82..0277b802 100644 --- a/src/diffCheck/geometry/DFPointCloud.cc +++ b/src/diffCheck/geometry/DFPointCloud.cc @@ -236,22 +236,14 @@ namespace diffCheck::geometry cilantro::KMeans kmeans(normalMatrix); kmeans.cluster(nComponents); - const auto& centroids = kmeans.getClusterCentroids(); - const auto& assignments = kmeans.getPointToClusterIndexMap(); - - std::vector> clusters(nComponents); - for (size_t i = 0; i < nComponents; ++i) + const cilantro::VectorSet3d& centroids = kmeans.getClusterCentroids(); + const std::vector& assignments = kmeans.getPointToClusterIndexMap(); + std::vector clusterSizes(nComponents, 0); + for (size_t i = 0; i < assignments.size(); ++i) { - clusters[i] = {i, centroids.col(i)}; + clusterSizes[assignments[i]]++; } - std::sort(clusters.begin(), clusters.end(), [](const auto& a, const auto& b) - { - return a.second.norm() < b.second.norm(); - }); - - std::vector> sortedClusters(nComponents); - std::vector clusterSizes(nComponents, 0); - + // Sort clusters by size std::vector> sortedClustersBySize(nComponents); for (size_t i = 0; i < nComponents; ++i) { From 188dc8140ba7f3974dbe1635e3ffdecdff5e28c8 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Sat, 10 May 2025 22:05:29 +0200 Subject: [PATCH 12/15] feat: add unit test to KMeans clustering of normals --- src/diffCheck/IOManager.cc | 7 +++++++ src/diffCheck/IOManager.hh | 2 ++ tests/unit_tests/DFPointCloudTest.cc | 10 +++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/diffCheck/IOManager.cc b/src/diffCheck/IOManager.cc index 2a857950..2da591a8 100644 --- a/src/diffCheck/IOManager.cc +++ b/src/diffCheck/IOManager.cc @@ -68,4 +68,11 @@ namespace diffCheck::io std::filesystem::path pathCloud = pathTestData / "test_pc_for_SOR_101pts_with_1_outlier.ply"; return pathCloud.string(); } + + std::string GetTwoConnectedPlanesPlyPath() + { + std::filesystem::path pathTestData = GetTestDataDir(); + std::filesystem::path pathCloud = pathTestData / "two_connected_planes_with_normals.ply"; + return pathCloud.string(); + } } // namespace diffCheck::io \ No newline at end of file diff --git a/src/diffCheck/IOManager.hh b/src/diffCheck/IOManager.hh index e22c029d..646ce511 100644 --- a/src/diffCheck/IOManager.hh +++ b/src/diffCheck/IOManager.hh @@ -42,4 +42,6 @@ namespace diffCheck::io std::string GetRoofQuarterPlyPath(); /// @brief Get the path to the plane point cloud with one outlier std::string GetPlanePCWithOneOutliers(); + /// @brief Get the path to the two connected planes ply test file + std::string GetTwoConnectedPlanesPlyPath(); } // namespace diffCheck::io \ No newline at end of file diff --git a/tests/unit_tests/DFPointCloudTest.cc b/tests/unit_tests/DFPointCloudTest.cc index d6d669d5..72b5a9db 100644 --- a/tests/unit_tests/DFPointCloudTest.cc +++ b/tests/unit_tests/DFPointCloudTest.cc @@ -219,4 +219,12 @@ TEST_F(DFPointCloudTestFixture, Transform) { //------------------------------------------------------------------------- // Others -//------------------------------------------------------------------------- \ No newline at end of file +//------------------------------------------------------------------------- + +TEST_F(DFPointCloudTestFixture, KMeansClusteringOfNormals) { + std::string path = diffCheck::io::GetTwoConnectedPlanesPlyPath(); + diffCheck::geometry::DFPointCloud dfPointCloud2Planes; + dfPointCloud2Planes.LoadFromPLY(path); + std::vector axes = dfPointCloud2Planes.GetPrincipalAxes(2); + EXPECT_TRUE((axes[0] - Eigen::Vector3d(0, 0, 1)).norm() < 1e-2 || (axes[1] - Eigen::Vector3d(0, 0, 1)).norm() < 1e-2); +} \ No newline at end of file From 56a80d54b33d4551451b8901330fef191af59c30 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Fri, 16 May 2025 10:48:06 +0200 Subject: [PATCH 13/15] fix: marameter name from singular to plural, and corresponding list access --- src/gh/components/DF_main_pc_axes/metadata.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gh/components/DF_main_pc_axes/metadata.json b/src/gh/components/DF_main_pc_axes/metadata.json index 1ba30408..2aacae70 100644 --- a/src/gh/components/DF_main_pc_axes/metadata.json +++ b/src/gh/components/DF_main_pc_axes/metadata.json @@ -14,13 +14,13 @@ "iconDisplay": 2, "inputParameters": [ { - "name": "i_cloud", + "name": "i_clouds", "nickname": "i_cloud", "description": "cloud whose main axes are to be calculated", "optional": true, "allowTreeAccess": true, "showTypeHints": true, - "scriptParamAccess": "item", + "scriptParamAccess": "list", "wireDisplay": "default", "sourceCount": 0, "typeHintID": "pointcloud" From 0d98ad0d63dc7e535637c443a1a8f2c802fc2662 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 28 May 2025 15:24:05 +0200 Subject: [PATCH 14/15] fix: have new elements pick-up the indexes of the already-existing elements, with 'None' for nonexisting steps --- src/gh/diffCheck/diffCheck/df_poses.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/gh/diffCheck/diffCheck/df_poses.py b/src/gh/diffCheck/diffCheck/df_poses.py index 8638eb93..397a3ffb 100644 --- a/src/gh/diffCheck/diffCheck/df_poses.py +++ b/src/gh/diffCheck/diffCheck/df_poses.py @@ -53,7 +53,9 @@ def add_step(self, new_poses: list[DFPose]): for i, pose in enumerate(new_poses): if f"element_{i}" not in self.poses_per_element_dictionary: self.poses_per_element_dictionary[f"element_{i}"] = DFPosesBeam({}, 4) - self.poses_per_element_dictionary[f"element_{i}"].add_pose(pose, self.n_step + 1) + for j in range(self.n_step): + self.poses_per_element_dictionary[f"element_{i}"].add_pose(None, j) + self.poses_per_element_dictionary[f"element_{i}"].add_pose(pose, self.n_step) self.n_step += 1 def get_last_poses(self): @@ -64,7 +66,7 @@ def get_last_poses(self): return None last_poses = [] for i in range(len(self.poses_per_element_dictionary)): - last_poses.append(self.poses_per_element_dictionary[f"element_{i}"].poses_dictionnary[f"pose_{self.n_step}"]) + last_poses.append(self.poses_per_element_dictionary[f"element_{i}"].poses_dictionnary[f"pose_{self.n_step-1}"]) return last_poses def reset(self): From 00ceefa6c362f377fecadb3006d54540e31c15bc Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 28 May 2025 15:27:04 +0200 Subject: [PATCH 15/15] fix: typo in nickname of i_clouds --- src/gh/components/DF_main_pc_axes/metadata.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gh/components/DF_main_pc_axes/metadata.json b/src/gh/components/DF_main_pc_axes/metadata.json index 2aacae70..cdfa6371 100644 --- a/src/gh/components/DF_main_pc_axes/metadata.json +++ b/src/gh/components/DF_main_pc_axes/metadata.json @@ -15,8 +15,8 @@ "inputParameters": [ { "name": "i_clouds", - "nickname": "i_cloud", - "description": "cloud whose main axes are to be calculated", + "nickname": "i_clouds", + "description": "clouds whose main axes are to be calculated", "optional": true, "allowTreeAccess": true, "showTypeHints": true,