From 9fcdfa636df63f31d91ac5ac8322e14c9aa30a6a Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:01:03 +0100 Subject: [PATCH 01/10] feat: add parameter to association to check or not iof the points are on face --- src/diffCheck/segmentation/DFSegmentation.cc | 43 ++++++++++++++++++-- src/diffCheck/segmentation/DFSegmentation.hh | 4 ++ src/diffCheckBindings.cc | 2 + 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/diffCheck/segmentation/DFSegmentation.cc b/src/diffCheck/segmentation/DFSegmentation.cc index 91a11691..d81c2ecd 100644 --- a/src/diffCheck/segmentation/DFSegmentation.cc +++ b/src/diffCheck/segmentation/DFSegmentation.cc @@ -97,6 +97,7 @@ namespace diffCheck::segmentation std::vector> DFSegmentation::AssociateClustersToMeshes( bool isCylinder, + bool discriminatePoints, std::vector> referenceMesh, std::vector> &clusters, double angleThreshold, @@ -289,8 +290,32 @@ namespace diffCheck::segmentation for (Eigen::Vector3d point : correspondingSegment->Points) { - bool pointInFace = false; - if (face->IsPointOnFace(point, associationThreshold)) + if (discriminatePoints) + { + bool pointInFace = false; + if (face->IsPointOnFace(point, associationThreshold)) + { + facePoints->Points.push_back(point); + facePoints->Normals.push_back( + correspondingSegment->Normals[std::distance( + correspondingSegment->Points.begin(), + std::find(correspondingSegment->Points.begin(), + correspondingSegment->Points.end(), + point))] + ); + if (hasColors) + { + facePoints->Colors.push_back( + correspondingSegment->Colors[std::distance( + correspondingSegment->Points.begin(), + std::find(correspondingSegment->Points.begin(), + correspondingSegment->Points.end(), + point))] + ); + } + } + } + else { facePoints->Points.push_back(point); facePoints->Normals.push_back( @@ -330,6 +355,7 @@ namespace diffCheck::segmentation void DFSegmentation::CleanUnassociatedClusters( bool isCylinder, + bool discriminatePoints, std::vector> &unassociatedClusters, std::vector>> &existingPointCloudSegments, std::vector>> meshes, @@ -477,12 +503,23 @@ namespace diffCheck::segmentation completed_segment->Colors.push_back(cluster->Colors[std::distance(cluster->Points.begin(), std::find(cluster->Points.begin(), cluster->Points.end(), point))]); } else - if (correspondingMeshFace->IsPointOnFace(point, associationThreshold)) + { + if (discriminatePoints) + { + if (correspondingMeshFace->IsPointOnFace(point, associationThreshold)) + { + completed_segment->Points.push_back(point); + completed_segment->Normals.push_back(cluster->Normals[std::distance(cluster->Points.begin(), std::find(cluster->Points.begin(), cluster->Points.end(), point))]); + completed_segment->Colors.push_back(cluster->Colors[std::distance(cluster->Points.begin(), std::find(cluster->Points.begin(), cluster->Points.end(), point))]); + } + } + else { completed_segment->Points.push_back(point); completed_segment->Normals.push_back(cluster->Normals[std::distance(cluster->Points.begin(), std::find(cluster->Points.begin(), cluster->Points.end(), point))]); completed_segment->Colors.push_back(cluster->Colors[std::distance(cluster->Points.begin(), std::find(cluster->Points.begin(), cluster->Points.end(), point))]); } + } } std::vector indicesToRemove; diff --git a/src/diffCheck/segmentation/DFSegmentation.hh b/src/diffCheck/segmentation/DFSegmentation.hh index cb453158..cf778990 100644 --- a/src/diffCheck/segmentation/DFSegmentation.hh +++ b/src/diffCheck/segmentation/DFSegmentation.hh @@ -28,6 +28,7 @@ namespace diffCheck::segmentation public: ///< segmentation refinement methods /** @brief Associates point cloud segments to mesh faces and merges them. It uses the center of mass of the segments and the mesh faces to find correspondances. For each mesh face it then iteratively associate the points of the segment that are actually on the mesh face. * @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. + * @param discriminatePoints a boolean to indicate if we want to discriminate points based on their normal direction when associating clusters to mesh faces. If true, only points that have normals more or less aligned with the face normal will be considered for association. * @param referenceMesh the vector of mesh faces to associate with the segments. It is a representation of a beam and its faces. * @param clusters the vector of clusters from cilantro to associate with the mesh faces of the reference mesh * @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. @@ -37,6 +38,7 @@ namespace diffCheck::segmentation */ static std::vector> DFSegmentation::AssociateClustersToMeshes( bool isCylinder, + bool discriminatePoints, std::vector> referenceMesh, std::vector> &clusters, double angleThreshold = 0.1, @@ -45,6 +47,7 @@ namespace diffCheck::segmentation /** @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. * @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. + * @param discriminatePoints a boolean to indicate if we want to discriminate points based on their normal direction when associating clusters to mesh faces. If true, only points that have normals more or less aligned with the face normal will be considered for association. * @param unassociatedClusters the clusters from the normal-based segmentatinon that haven't been associated yet. * @param existingPointCloudSegments the already associated segments per mesh face. * @param meshes the mesh faces for all the model. This is used to associate the clusters to the mesh faces. @@ -55,6 +58,7 @@ namespace diffCheck::segmentation */ static void DFSegmentation::CleanUnassociatedClusters( bool isCylinder, + bool discriminatePoints, std::vector> &unassociatedClusters, std::vector>> &existingPointCloudSegments, std::vector>> meshes, diff --git a/src/diffCheckBindings.cc b/src/diffCheckBindings.cc index e1a7a081..9a08d15a 100644 --- a/src/diffCheckBindings.cc +++ b/src/diffCheckBindings.cc @@ -226,6 +226,7 @@ PYBIND11_MODULE(diffcheck_bindings, m) { .def_static("associate_clusters", &diffCheck::segmentation::DFSegmentation::AssociateClustersToMeshes, py::arg("is_roundwood"), + py::arg("discriminate_points"), py::arg("reference_mesh"), py::arg("unassociated_clusters"), py::arg("angle_threshold") = 0.1, @@ -234,6 +235,7 @@ PYBIND11_MODULE(diffcheck_bindings, m) { .def_static("clean_unassociated_clusters", &diffCheck::segmentation::DFSegmentation::CleanUnassociatedClusters, py::arg("is_roundwood"), + py::arg("discriminate_points"), py::arg("unassociated_clusters"), py::arg("associated_clusters"), py::arg("reference_mesh"), From 552e374c3907c8798521400f7a78c75adcf36134 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:02:29 +0100 Subject: [PATCH 02/10] feat-wip: implement the 2-step CAD segmentation --- src/gh/components/DF_CAD_segmentator/code.py | 99 +++++++++++++++++--- 1 file changed, 88 insertions(+), 11 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index 81bc8e94..28b3b105 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -9,7 +9,8 @@ from diffCheck.diffcheck_bindings import dfb_segmentation -from diffCheck.diffcheck_bindings import dfb_geometry +from diffCheck.diffcheck_bindings import dfb_geometry, dfb_registrations +# from diffCheck.diffCheck_bindings import dfb_registrations from diffCheck import df_cvt_bindings @@ -17,11 +18,11 @@ class DFCADSegmentator(component): def RunScript(self, - i_clouds: System.Collections.Generic.IList[Rhino.Geometry.PointCloud], - i_assembly, - i_angle_threshold: float = 0.1, - i_association_threshold: float = 0.1, - i_angle_association_threshold: float = 0.5): + i_clouds: System.Collections.Generic.List[Rhino.Geometry.PointCloud], + i_assembly, + i_angle_threshold: float, + i_association_threshold: float, + i_angle_association_threshold: float): if i_clouds is None or i_assembly is None: self.AddRuntimeMessage(RML.Warning, "Please provide a cloud and an assembly to segment.") @@ -33,8 +34,10 @@ def RunScript(self, if i_angle_association_threshold is None: i_angle_association_threshold = 0.5 o_face_clusters = [] + df_clusters_temp = [] df_clusters = [] # we make a deepcopy of the input clouds + df_clouds_copy = [df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud.Duplicate()) for cloud in i_clouds] df_clouds = [df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud.Duplicate()) for cloud in i_clouds] df_beams = i_assembly.beams @@ -47,8 +50,9 @@ def RunScript(self, # different association depending on the type of beam df_asssociated_cluster_faces = dfb_segmentation.DFSegmentation.associate_clusters( is_roundwood=df_b.is_roundwood, + discriminate_points=False, reference_mesh=df_b_mesh_faces, - unassociated_clusters=df_clouds, + unassociated_clusters=df_clouds_copy, angle_threshold=i_angle_threshold, association_threshold=i_association_threshold, angle_association_threshold=i_angle_association_threshold @@ -56,13 +60,14 @@ def RunScript(self, df_asssociated_cluster_faces_per_beam.append(df_asssociated_cluster_faces) for i, df_b in enumerate(df_beams): - o_face_clusters.append([]) + # o_face_clusters.append([]) rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] 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] dfb_segmentation.DFSegmentation.clean_unassociated_clusters( is_roundwood=df_b.is_roundwood, - unassociated_clusters=df_clouds, + discriminate_points=False, + unassociated_clusters=df_clouds_copy, associated_clusters=[df_asssociated_cluster_faces_per_beam[i]], reference_mesh=[df_b_mesh_faces], angle_threshold=i_angle_threshold, @@ -70,12 +75,84 @@ def RunScript(self, angle_association_threshold=i_angle_association_threshold ) - o_face_clusters[-1] = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_asssociated_cluster_faces_per_beam[i]] + # o_face_clusters[-1] = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_asssociated_cluster_faces_per_beam[i]] df_asssociated_cluster = dfb_geometry.DFPointCloud() for df_associated_face in df_asssociated_cluster_faces_per_beam[i]: df_asssociated_cluster.add_points(df_associated_face) + df_clusters_temp.append(df_asssociated_cluster) + + # Now with the df_clusters, we sample a point cloud on the beam of the assembly, and perform an ICP to align the beam mesh to the point cloud. + # Then we re-compute the association on the scan with thigher thresholds to have a better segmentation. + o_transforms = [] + for i, df_b in enumerate(df_beams): + rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] + rh_mesh = Rhino.Geometry.Mesh() + for rh_b_mesh_face in rh_b_mesh_faces: + rh_mesh.Append(rh_b_mesh_face) + df_b_mesh = df_cvt_bindings.cvt_rhmesh_2_dfmesh(rh_mesh) + df_sampled_cloud = df_b_mesh.sample_points_uniformly(10000) + df_sampled_cloud.estimate_normals(use_cilantro_evaluator=True, + knn = 10, + ) + print(df_clusters_temp[i].get_num_points()) + transform = dfb_registrations.DFRefinedRegistration.O3DGeneralizedICP( + source=df_sampled_cloud, + target=df_clusters_temp[i], + max_correspondence_distance= 0.03 + + ) + df_xform = transform.transformation_matrix + rh_xform = Rhino.Geometry.Transform() + for i in range(4): + for j in range(4): + rh_xform[i, j] = df_xform[i, j] + o_transforms.append(rh_xform) + + df_new_asssociated_cluster_faces_per_beam = [] + for i, df_b in enumerate(df_beams): + rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] + for rh_mesh in rh_b_mesh_faces: + rh_mesh.Transform(o_transforms[i]) + 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] + + # different association depending on the type of beam + df_new_asssociated_cluster_faces = dfb_segmentation.DFSegmentation.associate_clusters( + is_roundwood=df_b.is_roundwood, + discriminate_points=True, + reference_mesh=df_b_mesh_faces, + unassociated_clusters=df_clouds, + angle_threshold=i_angle_threshold, + association_threshold=i_association_threshold, + angle_association_threshold=i_angle_association_threshold + ) + df_new_asssociated_cluster_faces_per_beam.append(df_new_asssociated_cluster_faces) + + for i, df_b in enumerate(df_beams): + o_face_clusters.append([]) + rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] + for rh_mesh in rh_b_mesh_faces: + rh_mesh.Transform(o_transforms[i]) + 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] + + dfb_segmentation.DFSegmentation.clean_unassociated_clusters( + is_roundwood=df_b.is_roundwood, + discriminate_points=True, + unassociated_clusters=df_clouds, + associated_clusters=[df_new_asssociated_cluster_faces_per_beam[i]], + reference_mesh=[df_b_mesh_faces], + angle_threshold=i_angle_threshold, + association_threshold=i_association_threshold, + angle_association_threshold=i_angle_association_threshold + ) + + o_face_clusters[-1] = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_new_asssociated_cluster_faces_per_beam[i]] + + df_asssociated_cluster = dfb_geometry.DFPointCloud() + for df_associated_face in df_new_asssociated_cluster_faces_per_beam[i]: + df_asssociated_cluster.add_points(df_associated_face) + df_clusters.append(df_asssociated_cluster) o_beam_clouds = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_clusters] @@ -87,4 +164,4 @@ def RunScript(self, o_face_clouds = th.list_to_tree(o_face_clusters) - return [o_beam_clouds, o_face_clouds] + return [o_beam_clouds, o_face_clouds, o_transforms] From 00b7337a5b848593dad85796f3c0ead0e98556a7 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:09:37 +0100 Subject: [PATCH 03/10] feat-wip: add maximum distance for cluster association --- src/diffCheck/segmentation/DFSegmentation.cc | 14 ++++++++------ src/diffCheck/segmentation/DFSegmentation.hh | 8 ++++++-- src/diffCheckBindings.cc | 6 ++++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/diffCheck/segmentation/DFSegmentation.cc b/src/diffCheck/segmentation/DFSegmentation.cc index d81c2ecd..8a6878e8 100644 --- a/src/diffCheck/segmentation/DFSegmentation.cc +++ b/src/diffCheck/segmentation/DFSegmentation.cc @@ -102,7 +102,8 @@ namespace diffCheck::segmentation std::vector> &clusters, double angleThreshold, double associationThreshold, - double angleAssociationThreshold) + double angleAssociationThreshold, + double maximumFaceSegmentDistance) { std::vector> faceSegments = std::vector>(); @@ -271,12 +272,12 @@ namespace diffCheck::segmentation for (auto normal : segment->Normals){segmentNormal += normal;} segmentNormal.normalize(); double currentDistance = (faceCenter - segmentCenter).norm(); + double currentDitanceOrthogonalToFace = std::abs((faceCenter - segmentCenter).dot(faceNormal)); double currentAngle = std::abs(sin(acos(faceNormal.dot(faceCenter - segmentCenter)))); - // if the distance is smaller than the previous one, update the distance and the corresponding segment - if (std::abs(sin(acos(faceNormal.dot(segmentNormal)))) < angleThreshold && currentDistance * (angleAssociationThreshold + std::abs(faceNormal.dot((faceCenter - segmentCenter) / (faceCenter - segmentCenter).norm()))) < faceDistance) + if (std::abs(sin(acos(faceNormal.dot(segmentNormal)))) < angleThreshold && currentDitanceOrthogonalToFace < maximumFaceSegmentDistance && currentDitanceOrthogonalToFace < faceDistance) { correspondingSegment = segment; - faceDistance = currentDistance * (angleAssociationThreshold + std::abs(faceNormal.dot((faceCenter - segmentCenter) / (faceCenter - segmentCenter).norm()))); + faceDistance = currentDitanceOrthogonalToFace; } } @@ -361,7 +362,8 @@ namespace diffCheck::segmentation std::vector>> meshes, double angleThreshold, double associationThreshold, - double angleAssociationThreshold) + double angleAssociationThreshold, + double maximumFaceSegmentDistance) { if (unassociatedClusters.size() == 0) { @@ -469,7 +471,7 @@ namespace diffCheck::segmentation double currentDistance = (clusterCenter - faceCenter).norm() * std::abs(std::cos(clusterNormalToJunctionLineAngle)) / std::min(std::abs(clusterNormal.dot(faceNormal)), 0.05) ; - if (std::abs(sin(acos(faceNormal.dot(clusterNormal)))) < angleThreshold && currentDistance * (angleAssociationThreshold + std::abs(faceNormal.dot((faceCenter - clusterCenter) / (faceCenter - clusterCenter).norm()))) < distance) + if (std::abs(sin(acos(faceNormal.dot(clusterNormal)))) < angleThreshold && currentDistance < maximumFaceSegmentDistance && currentDistance * (angleAssociationThreshold + std::abs(faceNormal.dot((faceCenter - clusterCenter) / (faceCenter - clusterCenter).norm()))) < distance) { goodMeshIndex = meshIndex; goodFaceIndex = faceIndex; diff --git a/src/diffCheck/segmentation/DFSegmentation.hh b/src/diffCheck/segmentation/DFSegmentation.hh index cf778990..7b2e45f3 100644 --- a/src/diffCheck/segmentation/DFSegmentation.hh +++ b/src/diffCheck/segmentation/DFSegmentation.hh @@ -34,6 +34,7 @@ namespace diffCheck::segmentation * @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. * @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. * @param angleAssociationThreshold a number to indicate how much distance in the plane of the face should be favored, compared to distance orthogonal to the face normal. If set to 0, any face in the same plane as the face will be considered as having a distance of 0. If set to a high value (e.g. 1000000), no difference will be made between distance in the plane of the face and orthogonal to it. Default is 0.5 + * @param maximumFaceSegmentDistance the maximum distance a segment's center of mass can be perpendicularly to a mesh face * @return std::shared_ptr The unified segments */ static std::vector> DFSegmentation::AssociateClustersToMeshes( @@ -43,7 +44,8 @@ namespace diffCheck::segmentation std::vector> &clusters, double angleThreshold = 0.1, double associationThreshold = 0.1, - double angleAssociationThreshold = 0.5); + double angleAssociationThreshold = 0.5, + double maximumFaceSegmentDistance = 0.05); /** @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. * @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. @@ -54,6 +56,7 @@ namespace diffCheck::segmentation * @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. * @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. * @param angleAssociationThreshold a number to indicate how much distance in the plane of the face should be favored, compared to distance orthogonal to the face normal. If set to 0, any face in the same plane as the face will be considered as having a distance of 0. If set to a high value (e.g. 1000000), no difference will be made between distance in the plane of the face and orthogonal to it. Default is 0.5 + * @param maximumFaceSegmentDistance the maximum distance a segment's center of mass can be perpendicularly to a mesh face * @return void */ static void DFSegmentation::CleanUnassociatedClusters( @@ -64,6 +67,7 @@ namespace diffCheck::segmentation std::vector>> meshes, double angleThreshold = 0.1, double associationThreshold = 0.1, - double angleAssociationThreshold = 0.5); + double angleAssociationThreshold = 0.5, + double maximumFaceSegmentDistance = 0.05); }; } // namespace diffCheck::segmentation \ No newline at end of file diff --git a/src/diffCheckBindings.cc b/src/diffCheckBindings.cc index 9a08d15a..e0666d25 100644 --- a/src/diffCheckBindings.cc +++ b/src/diffCheckBindings.cc @@ -231,7 +231,8 @@ PYBIND11_MODULE(diffcheck_bindings, m) { py::arg("unassociated_clusters"), py::arg("angle_threshold") = 0.1, py::arg("association_threshold") = 0.1, - py::arg("angle_association_threshold") = 0.5) + py::arg("angle_association_threshold") = 0.5, + py::arg("maximum_face_segment_distance") = 0.05) .def_static("clean_unassociated_clusters", &diffCheck::segmentation::DFSegmentation::CleanUnassociatedClusters, py::arg("is_roundwood"), @@ -241,5 +242,6 @@ PYBIND11_MODULE(diffcheck_bindings, m) { py::arg("reference_mesh"), py::arg("angle_threshold") = 0.1, py::arg("association_threshold") = 0.1, - py::arg("angle_association_threshold") = 0.5); + py::arg("angle_association_threshold") = 0.5, + py::arg("maximum_face_segment_distance") = 0.05); } From 3811b56c6cbe4f7f4410b2e32ff93d174a29ed89 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:28:03 +0100 Subject: [PATCH 04/10] feat: add ICP registration in CAD segmentation component and expose basic parameters --- src/gh/components/DF_CAD_segmentator/code.py | 82 ++++++++++++------- .../DF_CAD_segmentator/metadata.json | 24 ++++++ 2 files changed, 78 insertions(+), 28 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index 28b3b105..fb296308 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -9,8 +9,7 @@ from diffCheck.diffcheck_bindings import dfb_segmentation -from diffCheck.diffcheck_bindings import dfb_geometry, dfb_registrations -# from diffCheck.diffCheck_bindings import dfb_registrations +from diffCheck.diffcheck_bindings import dfb_geometry, dfb_registrations, dfb_transformation from diffCheck import df_cvt_bindings @@ -22,7 +21,10 @@ def RunScript(self, i_assembly, i_angle_threshold: float, i_association_threshold: float, - i_angle_association_threshold: float): + i_angle_association_threshold: float, + i_maximum_face_segment_distance: float, + i_radius_normal_estimation: float, + i_max_correspondence_distance_icp: float,): if i_clouds is None or i_assembly is None: self.AddRuntimeMessage(RML.Warning, "Please provide a cloud and an assembly to segment.") @@ -33,12 +35,17 @@ def RunScript(self, i_association_threshold = 0.1 if i_angle_association_threshold is None: i_angle_association_threshold = 0.5 + if i_radius_normal_estimation is None: + i_radius_normal_estimation = 0.01 o_face_clusters = [] df_clusters_temp = [] df_clusters = [] # we make a deepcopy of the input clouds df_clouds_copy = [df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud.Duplicate()) for cloud in i_clouds] df_clouds = [df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud.Duplicate()) for cloud in i_clouds] + df_merged_cloud = dfb_geometry.DFPointCloud() + for pc in df_clouds: + df_merged_cloud.add_points(pc) df_beams = i_assembly.beams df_asssociated_cluster_faces_per_beam = [] @@ -50,33 +57,32 @@ def RunScript(self, # different association depending on the type of beam df_asssociated_cluster_faces = dfb_segmentation.DFSegmentation.associate_clusters( is_roundwood=df_b.is_roundwood, - discriminate_points=False, + discriminate_points=True, reference_mesh=df_b_mesh_faces, unassociated_clusters=df_clouds_copy, angle_threshold=i_angle_threshold, - association_threshold=i_association_threshold, - angle_association_threshold=i_angle_association_threshold + association_threshold=i_association_threshold , + angle_association_threshold=i_angle_association_threshold, + maximum_face_segment_distance=i_maximum_face_segment_distance ) df_asssociated_cluster_faces_per_beam.append(df_asssociated_cluster_faces) for i, df_b in enumerate(df_beams): - # o_face_clusters.append([]) rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] 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] dfb_segmentation.DFSegmentation.clean_unassociated_clusters( is_roundwood=df_b.is_roundwood, - discriminate_points=False, + discriminate_points=True, unassociated_clusters=df_clouds_copy, associated_clusters=[df_asssociated_cluster_faces_per_beam[i]], reference_mesh=[df_b_mesh_faces], angle_threshold=i_angle_threshold, - association_threshold=i_association_threshold, - angle_association_threshold=i_angle_association_threshold + association_threshold=i_association_threshold , + angle_association_threshold=i_angle_association_threshold, + maximum_face_segment_distance=i_maximum_face_segment_distance ) - # o_face_clusters[-1] = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_asssociated_cluster_faces_per_beam[i]] - df_asssociated_cluster = dfb_geometry.DFPointCloud() for df_associated_face in df_asssociated_cluster_faces_per_beam[i]: df_asssociated_cluster.add_points(df_associated_face) @@ -86,23 +92,36 @@ def RunScript(self, # Now with the df_clusters, we sample a point cloud on the beam of the assembly, and perform an ICP to align the beam mesh to the point cloud. # Then we re-compute the association on the scan with thigher thresholds to have a better segmentation. o_transforms = [] + rh_meshes = [] for i, df_b in enumerate(df_beams): rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] + for df_j_face in df_b.joint_faces: + rh_b_mesh_faces.append(df_j_face.to_mesh()) rh_mesh = Rhino.Geometry.Mesh() for rh_b_mesh_face in rh_b_mesh_faces: rh_mesh.Append(rh_b_mesh_face) + df_b_mesh = df_cvt_bindings.cvt_rhmesh_2_dfmesh(rh_mesh) - df_sampled_cloud = df_b_mesh.sample_points_uniformly(10000) - df_sampled_cloud.estimate_normals(use_cilantro_evaluator=True, - knn = 10, + df_sampled_cloud = df_b_mesh.sample_points_uniformly(1000) + df_sampled_cloud.estimate_normals(use_cilantro_evaluator=False, + search_radius = i_radius_normal_estimation, ) - print(df_clusters_temp[i].get_num_points()) - transform = dfb_registrations.DFRefinedRegistration.O3DGeneralizedICP( - source=df_sampled_cloud, - target=df_clusters_temp[i], - max_correspondence_distance= 0.03 - + cluster_without_normals = df_clusters_temp[i] + cluster_without_normals.estimate_normals(use_cilantro_evaluator=False, + search_radius = i_radius_normal_estimation, ) + df_merged_cloud.remove_statistical_outliers(100, 1.5) + self.AddRuntimeMessage(RML.Warning, f"size of pc: {cluster_without_normals.get_num_points()}") + if cluster_without_normals.get_num_points() != 0: + transform = dfb_registrations.DFRefinedRegistration.O3DICP( + source=df_sampled_cloud, + target=df_merged_cloud, + max_correspondence_distance= i_max_correspondence_distance_icp, + max_iteration = 1000 + + ) + else: + transform = dfb_transformation.DFTransformation() df_xform = transform.transformation_matrix rh_xform = Rhino.Geometry.Transform() for i in range(4): @@ -113,8 +132,12 @@ def RunScript(self, df_new_asssociated_cluster_faces_per_beam = [] for i, df_b in enumerate(df_beams): rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] - for rh_mesh in rh_b_mesh_faces: - rh_mesh.Transform(o_transforms[i]) + rh_test_mesh = Rhino.Geometry.Mesh() + for j in range(len(rh_b_mesh_faces)): + sucess = rh_b_mesh_faces[j].Transform(o_transforms[i]) + if sucess: + rh_test_mesh.Append(rh_b_mesh_faces[j]) + rh_meshes.append(rh_test_mesh) 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] # different association depending on the type of beam @@ -125,15 +148,16 @@ def RunScript(self, unassociated_clusters=df_clouds, angle_threshold=i_angle_threshold, association_threshold=i_association_threshold, - angle_association_threshold=i_angle_association_threshold + angle_association_threshold=i_angle_association_threshold, + maximum_face_segment_distance=i_maximum_face_segment_distance ) df_new_asssociated_cluster_faces_per_beam.append(df_new_asssociated_cluster_faces) for i, df_b in enumerate(df_beams): o_face_clusters.append([]) rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] - for rh_mesh in rh_b_mesh_faces: - rh_mesh.Transform(o_transforms[i]) + for j in range(len(rh_b_mesh_faces)): + rh_b_mesh_faces[j].Transform(o_transforms[i]) 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] dfb_segmentation.DFSegmentation.clean_unassociated_clusters( @@ -144,7 +168,8 @@ def RunScript(self, reference_mesh=[df_b_mesh_faces], angle_threshold=i_angle_threshold, association_threshold=i_association_threshold, - angle_association_threshold=i_angle_association_threshold + angle_association_threshold=i_angle_association_threshold, + maximum_face_segment_distance=i_maximum_face_segment_distance ) o_face_clusters[-1] = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_new_asssociated_cluster_faces_per_beam[i]] @@ -155,6 +180,7 @@ def RunScript(self, df_clusters.append(df_asssociated_cluster) + o_beam_intermediary_clouds = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_clusters_temp] o_beam_clouds = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_clusters] for i, o_beam_cloud in enumerate(o_beam_clouds): @@ -164,4 +190,4 @@ def RunScript(self, o_face_clouds = th.list_to_tree(o_face_clusters) - return [o_beam_clouds, o_face_clouds, o_transforms] + return [o_beam_clouds, o_face_clouds, o_transforms, o_beam_intermediary_clouds, rh_meshes] diff --git a/src/gh/components/DF_CAD_segmentator/metadata.json b/src/gh/components/DF_CAD_segmentator/metadata.json index ddfe81bd..604e6237 100644 --- a/src/gh/components/DF_CAD_segmentator/metadata.json +++ b/src/gh/components/DF_CAD_segmentator/metadata.json @@ -72,6 +72,30 @@ "wireDisplay": "default", "sourceCount": 0, "typeHintID": "float" + }, + { + "name": "i_radius_normal_estimation", + "nickname": "i_radius_normal_estimation", + "description": "The radius used for normal estimation. Default is 0.01", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "float" + }, + { + "name": "i_max_correspondence_distance_icp", + "nickname": "i_max_correspondence_distance_icp", + "description": "The maximum correspondence distance for the ICP algorithm. Default is 0.1", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "float" } ], "outputParameters": [ From 0d5fda96260a6935f0fdc208b49ed44520156bad Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:06:54 +0100 Subject: [PATCH 05/10] feat: improve geometry center calculation --- src/gh/components/DF_pose_estimation/code.py | 28 +++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/gh/components/DF_pose_estimation/code.py b/src/gh/components/DF_pose_estimation/code.py index b1a9fab1..60af0e56 100644 --- a/src/gh/components/DF_pose_estimation/code.py +++ b/src/gh/components/DF_pose_estimation/code.py @@ -47,24 +47,20 @@ def RunScript(self, continue rh_face_normals.append(Rhino.Geometry.Vector3d(plane_normal[0], plane_normal[1], plane_normal[2])) - df_bb_points = df_cloud.get_axis_aligned_bounding_box() - df_bb_centroid = (df_bb_points[0] + df_bb_points[1]) / 2 - rh_bb_centroid = Rhino.Geometry.Point3d(df_bb_centroid[0], df_bb_centroid[1], df_bb_centroid[2]) - + df_bb_points = df_cloud.get_tight_bounding_box() + df_bb_centroid = sum(df_bb_points)/len(df_bb_points) + rh_tentative_bb_centroid = Rhino.Geometry.Point3d(df_bb_centroid[0], df_bb_centroid[1], df_bb_centroid[2]) new_xDirection, new_yDirection = df_poses.select_vectors(rh_face_normals, i_assembly.beams[i].plane.XAxis, i_assembly.beams[i].plane.YAxis) - if not new_yDirection: - df_beam_pc = dfb_geometry.DFPointCloud() - for face_cloud in face_clouds: - df_face_cloud = df_cvt_bindings.cvt_rhcloud_2_dfcloud(face_cloud) - df_beam_pc.add_points(df_face_cloud) - corners = df_beam_pc.get_tight_bounding_box() - rh_corners = [Rhino.Geometry.Point3d(pt[0], pt[1], pt[2]) for pt in corners] - plane = Rhino.Geometry.Plane.CreateFromPoints(rh_corners[0],rh_corners[1],rh_corners[2]) - box = Rhino.Geometry.Box(plane, rh_corners) - longest_edge = sorted(box.ToBrep().Edges, key=lambda e: e.GetLength())[-1] - longest_edge_direction = longest_edge.TangentAtEnd - new_yDirection = Rhino.Geometry.Vector3d.CrossProduct(new_xDirection, longest_edge_direction) + rh_tentative_plane = Rhino.Geometry.Plane(rh_tentative_bb_centroid, new_yDirection, new_xDirection) + + rh_beam_cloud = Rhino.Geometry.PointCloud() + for face_cloud in face_clouds: + rh_beam_cloud.Merge(face_cloud) + + rh_bbox = rh_beam_cloud.GetBoundingBox(rh_tentative_plane) + rh_bbox.Transform(Rhino.Geometry.Transform.PlaneToPlane(Rhino.Geometry.Plane.WorldXY, rh_tentative_plane)) + rh_bb_centroid = rh_bbox.Center pose = df_poses.DFPose( origin = [rh_bb_centroid.X, rh_bb_centroid.Y, rh_bb_centroid.Z], From 9fef73ca475ab31505e50a1707e1e6245d29ad49 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:00:11 +0100 Subject: [PATCH 06/10] feat: update and cleanup script with new registration step prior to association --- src/gh/components/DF_CAD_segmentator/code.py | 84 ++++---------------- 1 file changed, 16 insertions(+), 68 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index fb296308..19d505fd 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -9,7 +9,7 @@ from diffCheck.diffcheck_bindings import dfb_segmentation -from diffCheck.diffcheck_bindings import dfb_geometry, dfb_registrations, dfb_transformation +from diffCheck.diffcheck_bindings import dfb_geometry, dfb_registrations from diffCheck import df_cvt_bindings @@ -24,7 +24,7 @@ def RunScript(self, i_angle_association_threshold: float, i_maximum_face_segment_distance: float, i_radius_normal_estimation: float, - i_max_correspondence_distance_icp: float,): + i_max_correspondence_distance_icp: float): if i_clouds is None or i_assembly is None: self.AddRuntimeMessage(RML.Warning, "Please provide a cloud and an assembly to segment.") @@ -38,60 +38,17 @@ def RunScript(self, if i_radius_normal_estimation is None: i_radius_normal_estimation = 0.01 o_face_clusters = [] - df_clusters_temp = [] + o_transforms = [] df_clusters = [] # we make a deepcopy of the input clouds - df_clouds_copy = [df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud.Duplicate()) for cloud in i_clouds] df_clouds = [df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud.Duplicate()) for cloud in i_clouds] df_merged_cloud = dfb_geometry.DFPointCloud() + df_merged_cloud.remove_statistical_outliers(100, 1.5) for pc in df_clouds: df_merged_cloud.add_points(pc) df_beams = i_assembly.beams - df_asssociated_cluster_faces_per_beam = [] - - for df_b in df_beams: - rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] - 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] - - # different association depending on the type of beam - df_asssociated_cluster_faces = dfb_segmentation.DFSegmentation.associate_clusters( - is_roundwood=df_b.is_roundwood, - discriminate_points=True, - reference_mesh=df_b_mesh_faces, - unassociated_clusters=df_clouds_copy, - angle_threshold=i_angle_threshold, - association_threshold=i_association_threshold , - angle_association_threshold=i_angle_association_threshold, - maximum_face_segment_distance=i_maximum_face_segment_distance - ) - df_asssociated_cluster_faces_per_beam.append(df_asssociated_cluster_faces) - - for i, df_b in enumerate(df_beams): - rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] - 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] - - dfb_segmentation.DFSegmentation.clean_unassociated_clusters( - is_roundwood=df_b.is_roundwood, - discriminate_points=True, - unassociated_clusters=df_clouds_copy, - associated_clusters=[df_asssociated_cluster_faces_per_beam[i]], - reference_mesh=[df_b_mesh_faces], - angle_threshold=i_angle_threshold, - association_threshold=i_association_threshold , - angle_association_threshold=i_angle_association_threshold, - maximum_face_segment_distance=i_maximum_face_segment_distance - ) - - df_asssociated_cluster = dfb_geometry.DFPointCloud() - for df_associated_face in df_asssociated_cluster_faces_per_beam[i]: - df_asssociated_cluster.add_points(df_associated_face) - - df_clusters_temp.append(df_asssociated_cluster) - # Now with the df_clusters, we sample a point cloud on the beam of the assembly, and perform an ICP to align the beam mesh to the point cloud. - # Then we re-compute the association on the scan with thigher thresholds to have a better segmentation. - o_transforms = [] rh_meshes = [] for i, df_b in enumerate(df_beams): rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] @@ -106,22 +63,14 @@ def RunScript(self, df_sampled_cloud.estimate_normals(use_cilantro_evaluator=False, search_radius = i_radius_normal_estimation, ) - cluster_without_normals = df_clusters_temp[i] - cluster_without_normals.estimate_normals(use_cilantro_evaluator=False, - search_radius = i_radius_normal_estimation, - ) - df_merged_cloud.remove_statistical_outliers(100, 1.5) - self.AddRuntimeMessage(RML.Warning, f"size of pc: {cluster_without_normals.get_num_points()}") - if cluster_without_normals.get_num_points() != 0: - transform = dfb_registrations.DFRefinedRegistration.O3DICP( - source=df_sampled_cloud, - target=df_merged_cloud, - max_correspondence_distance= i_max_correspondence_distance_icp, - max_iteration = 1000 + transform = dfb_registrations.DFRefinedRegistration.O3DICP( + source=df_sampled_cloud, + target=df_merged_cloud, + max_correspondence_distance= i_max_correspondence_distance_icp, + max_iteration = 1000 ) - else: - transform = dfb_transformation.DFTransformation() + df_xform = transform.transformation_matrix rh_xform = Rhino.Geometry.Transform() for i in range(4): @@ -129,7 +78,7 @@ def RunScript(self, rh_xform[i, j] = df_xform[i, j] o_transforms.append(rh_xform) - df_new_asssociated_cluster_faces_per_beam = [] + df_asssociated_cluster_faces_per_beam = [] for i, df_b in enumerate(df_beams): rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] rh_test_mesh = Rhino.Geometry.Mesh() @@ -151,7 +100,7 @@ def RunScript(self, angle_association_threshold=i_angle_association_threshold, maximum_face_segment_distance=i_maximum_face_segment_distance ) - df_new_asssociated_cluster_faces_per_beam.append(df_new_asssociated_cluster_faces) + df_asssociated_cluster_faces_per_beam.append(df_new_asssociated_cluster_faces) for i, df_b in enumerate(df_beams): o_face_clusters.append([]) @@ -164,7 +113,7 @@ def RunScript(self, is_roundwood=df_b.is_roundwood, discriminate_points=True, unassociated_clusters=df_clouds, - associated_clusters=[df_new_asssociated_cluster_faces_per_beam[i]], + associated_clusters=[df_asssociated_cluster_faces_per_beam[i]], reference_mesh=[df_b_mesh_faces], angle_threshold=i_angle_threshold, association_threshold=i_association_threshold, @@ -172,15 +121,14 @@ def RunScript(self, maximum_face_segment_distance=i_maximum_face_segment_distance ) - o_face_clusters[-1] = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_new_asssociated_cluster_faces_per_beam[i]] + o_face_clusters[-1] = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_asssociated_cluster_faces_per_beam[i]] df_asssociated_cluster = dfb_geometry.DFPointCloud() - for df_associated_face in df_new_asssociated_cluster_faces_per_beam[i]: + for df_associated_face in df_asssociated_cluster_faces_per_beam[i]: df_asssociated_cluster.add_points(df_associated_face) df_clusters.append(df_asssociated_cluster) - o_beam_intermediary_clouds = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_clusters_temp] o_beam_clouds = [df_cvt_bindings.cvt_dfcloud_2_rhcloud(cluster) for cluster in df_clusters] for i, o_beam_cloud in enumerate(o_beam_clouds): @@ -190,4 +138,4 @@ def RunScript(self, o_face_clouds = th.list_to_tree(o_face_clusters) - return [o_beam_clouds, o_face_clouds, o_transforms, o_beam_intermediary_clouds, rh_meshes] + return [o_beam_clouds, o_face_clouds] From 6c84650ba29321641aee5a6bd912185336e4a1ac Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:51:08 +0100 Subject: [PATCH 07/10] feat: get rig of angleAssociationThreshold parameter that is now unused --- src/diffCheck/segmentation/DFSegmentation.cc | 4 +--- src/diffCheck/segmentation/DFSegmentation.hh | 4 ---- src/diffCheckBindings.cc | 2 -- src/gh/components/DF_CAD_segmentator/code.py | 5 ----- src/gh/components/DF_CAD_segmentator/metadata.json | 12 ------------ 5 files changed, 1 insertion(+), 26 deletions(-) diff --git a/src/diffCheck/segmentation/DFSegmentation.cc b/src/diffCheck/segmentation/DFSegmentation.cc index 8a6878e8..d79cba5b 100644 --- a/src/diffCheck/segmentation/DFSegmentation.cc +++ b/src/diffCheck/segmentation/DFSegmentation.cc @@ -102,7 +102,6 @@ namespace diffCheck::segmentation std::vector> &clusters, double angleThreshold, double associationThreshold, - double angleAssociationThreshold, double maximumFaceSegmentDistance) { std::vector> faceSegments = std::vector>(); @@ -362,7 +361,6 @@ namespace diffCheck::segmentation std::vector>> meshes, double angleThreshold, double associationThreshold, - double angleAssociationThreshold, double maximumFaceSegmentDistance) { if (unassociatedClusters.size() == 0) @@ -471,7 +469,7 @@ namespace diffCheck::segmentation double currentDistance = (clusterCenter - faceCenter).norm() * std::abs(std::cos(clusterNormalToJunctionLineAngle)) / std::min(std::abs(clusterNormal.dot(faceNormal)), 0.05) ; - if (std::abs(sin(acos(faceNormal.dot(clusterNormal)))) < angleThreshold && currentDistance < maximumFaceSegmentDistance && currentDistance * (angleAssociationThreshold + std::abs(faceNormal.dot((faceCenter - clusterCenter) / (faceCenter - clusterCenter).norm()))) < distance) + if (std::abs(sin(acos(faceNormal.dot(clusterNormal)))) < angleThreshold && currentDistance < maximumFaceSegmentDistance && currentDistance * (std::abs(faceNormal.dot((faceCenter - clusterCenter) / (faceCenter - clusterCenter).norm()))) < distance) { goodMeshIndex = meshIndex; goodFaceIndex = faceIndex; diff --git a/src/diffCheck/segmentation/DFSegmentation.hh b/src/diffCheck/segmentation/DFSegmentation.hh index 7b2e45f3..a11b465d 100644 --- a/src/diffCheck/segmentation/DFSegmentation.hh +++ b/src/diffCheck/segmentation/DFSegmentation.hh @@ -33,7 +33,6 @@ namespace diffCheck::segmentation * @param clusters the vector of clusters from cilantro to associate with the mesh faces of the reference mesh * @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. * @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. - * @param angleAssociationThreshold a number to indicate how much distance in the plane of the face should be favored, compared to distance orthogonal to the face normal. If set to 0, any face in the same plane as the face will be considered as having a distance of 0. If set to a high value (e.g. 1000000), no difference will be made between distance in the plane of the face and orthogonal to it. Default is 0.5 * @param maximumFaceSegmentDistance the maximum distance a segment's center of mass can be perpendicularly to a mesh face * @return std::shared_ptr The unified segments */ @@ -44,7 +43,6 @@ namespace diffCheck::segmentation std::vector> &clusters, double angleThreshold = 0.1, double associationThreshold = 0.1, - double angleAssociationThreshold = 0.5, double maximumFaceSegmentDistance = 0.05); /** @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. @@ -55,7 +53,6 @@ namespace diffCheck::segmentation * @param meshes the mesh faces for all the model. This is used to associate the clusters to the mesh faces. * @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. * @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. - * @param angleAssociationThreshold a number to indicate how much distance in the plane of the face should be favored, compared to distance orthogonal to the face normal. If set to 0, any face in the same plane as the face will be considered as having a distance of 0. If set to a high value (e.g. 1000000), no difference will be made between distance in the plane of the face and orthogonal to it. Default is 0.5 * @param maximumFaceSegmentDistance the maximum distance a segment's center of mass can be perpendicularly to a mesh face * @return void */ @@ -67,7 +64,6 @@ namespace diffCheck::segmentation std::vector>> meshes, double angleThreshold = 0.1, double associationThreshold = 0.1, - double angleAssociationThreshold = 0.5, double maximumFaceSegmentDistance = 0.05); }; } // namespace diffCheck::segmentation \ No newline at end of file diff --git a/src/diffCheckBindings.cc b/src/diffCheckBindings.cc index e0666d25..590c1662 100644 --- a/src/diffCheckBindings.cc +++ b/src/diffCheckBindings.cc @@ -231,7 +231,6 @@ PYBIND11_MODULE(diffcheck_bindings, m) { py::arg("unassociated_clusters"), py::arg("angle_threshold") = 0.1, py::arg("association_threshold") = 0.1, - py::arg("angle_association_threshold") = 0.5, py::arg("maximum_face_segment_distance") = 0.05) .def_static("clean_unassociated_clusters", &diffCheck::segmentation::DFSegmentation::CleanUnassociatedClusters, @@ -242,6 +241,5 @@ PYBIND11_MODULE(diffcheck_bindings, m) { py::arg("reference_mesh"), py::arg("angle_threshold") = 0.1, py::arg("association_threshold") = 0.1, - py::arg("angle_association_threshold") = 0.5, py::arg("maximum_face_segment_distance") = 0.05); } diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index 19d505fd..8316cdd0 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -21,7 +21,6 @@ def RunScript(self, i_assembly, i_angle_threshold: float, i_association_threshold: float, - i_angle_association_threshold: float, i_maximum_face_segment_distance: float, i_radius_normal_estimation: float, i_max_correspondence_distance_icp: float): @@ -33,8 +32,6 @@ def RunScript(self, i_angle_threshold = 0.1 if i_association_threshold is None: i_association_threshold = 0.1 - if i_angle_association_threshold is None: - i_angle_association_threshold = 0.5 if i_radius_normal_estimation is None: i_radius_normal_estimation = 0.01 o_face_clusters = [] @@ -97,7 +94,6 @@ def RunScript(self, unassociated_clusters=df_clouds, angle_threshold=i_angle_threshold, association_threshold=i_association_threshold, - angle_association_threshold=i_angle_association_threshold, maximum_face_segment_distance=i_maximum_face_segment_distance ) df_asssociated_cluster_faces_per_beam.append(df_new_asssociated_cluster_faces) @@ -117,7 +113,6 @@ def RunScript(self, reference_mesh=[df_b_mesh_faces], angle_threshold=i_angle_threshold, association_threshold=i_association_threshold, - angle_association_threshold=i_angle_association_threshold, maximum_face_segment_distance=i_maximum_face_segment_distance ) diff --git a/src/gh/components/DF_CAD_segmentator/metadata.json b/src/gh/components/DF_CAD_segmentator/metadata.json index 604e6237..8cabe4bb 100644 --- a/src/gh/components/DF_CAD_segmentator/metadata.json +++ b/src/gh/components/DF_CAD_segmentator/metadata.json @@ -61,18 +61,6 @@ "sourceCount": 0, "typeHintID": "float" }, - { - "name": "i_angle_association_threshold", - "nickname": "i_angle_association_threshold", - "description": "A number to indicate how much distance in the plane of the face should be favored, compared to distance orthogonal to the face normal. Default is 0.5", - "optional": true, - "allowTreeAccess": true, - "showTypeHints": true, - "scriptParamAccess": "item", - "wireDisplay": "default", - "sourceCount": 0, - "typeHintID": "float" - }, { "name": "i_radius_normal_estimation", "nickname": "i_radius_normal_estimation", From c7509778dd878f94072169dc876d0332a573b459 Mon Sep 17 00:00:00 2001 From: DamienGilliard <127743632+DamienGilliard@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:24:01 +0100 Subject: [PATCH 08/10] feat: add i_make_registration parameter to component --- src/gh/components/DF_CAD_segmentator/code.py | 40 +++++++++++-------- .../DF_CAD_segmentator/metadata.json | 12 ++++++ 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index 8316cdd0..d101602a 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -23,6 +23,7 @@ def RunScript(self, i_association_threshold: float, i_maximum_face_segment_distance: float, i_radius_normal_estimation: float, + i_make_registration: bool, i_max_correspondence_distance_icp: float): if i_clouds is None or i_assembly is None: @@ -34,8 +35,10 @@ def RunScript(self, i_association_threshold = 0.1 if i_radius_normal_estimation is None: i_radius_normal_estimation = 0.01 + if i_make_registration is None: + i_make_registration = True o_face_clusters = [] - o_transforms = [] + transforms = [] df_clusters = [] # we make a deepcopy of the input clouds df_clouds = [df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud.Duplicate()) for cloud in i_clouds] @@ -60,27 +63,30 @@ def RunScript(self, df_sampled_cloud.estimate_normals(use_cilantro_evaluator=False, search_radius = i_radius_normal_estimation, ) - - transform = dfb_registrations.DFRefinedRegistration.O3DICP( - source=df_sampled_cloud, - target=df_merged_cloud, - max_correspondence_distance= i_max_correspondence_distance_icp, - max_iteration = 1000 - ) - - df_xform = transform.transformation_matrix - rh_xform = Rhino.Geometry.Transform() - for i in range(4): - for j in range(4): - rh_xform[i, j] = df_xform[i, j] - o_transforms.append(rh_xform) + if i_make_registration: + transform = dfb_registrations.DFRefinedRegistration.O3DICP( + source=df_sampled_cloud, + target=df_merged_cloud, + max_correspondence_distance= i_max_correspondence_distance_icp, + max_iteration = 1000 + ) + + df_xform = transform.transformation_matrix + rh_xform = Rhino.Geometry.Transform() + for i in range(4): + for j in range(4): + rh_xform[i, j] = df_xform[i, j] + + else: + rh_xform = Rhino.Geometry.Transform(1) + transforms.append(rh_xform) df_asssociated_cluster_faces_per_beam = [] for i, df_b in enumerate(df_beams): rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] rh_test_mesh = Rhino.Geometry.Mesh() for j in range(len(rh_b_mesh_faces)): - sucess = rh_b_mesh_faces[j].Transform(o_transforms[i]) + sucess = rh_b_mesh_faces[j].Transform(transforms[i]) if sucess: rh_test_mesh.Append(rh_b_mesh_faces[j]) rh_meshes.append(rh_test_mesh) @@ -102,7 +108,7 @@ def RunScript(self, o_face_clusters.append([]) rh_b_mesh_faces = [df_b_f.to_mesh() for df_b_f in df_b.side_faces] for j in range(len(rh_b_mesh_faces)): - rh_b_mesh_faces[j].Transform(o_transforms[i]) + rh_b_mesh_faces[j].Transform(transforms[i]) 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] dfb_segmentation.DFSegmentation.clean_unassociated_clusters( diff --git a/src/gh/components/DF_CAD_segmentator/metadata.json b/src/gh/components/DF_CAD_segmentator/metadata.json index 8cabe4bb..7a46563d 100644 --- a/src/gh/components/DF_CAD_segmentator/metadata.json +++ b/src/gh/components/DF_CAD_segmentator/metadata.json @@ -73,6 +73,18 @@ "sourceCount": 0, "typeHintID": "float" }, + { + "name": "i_make_registration", + "nickname": "i_make_registration", + "description": "Whether to make a registration of the clusters on the mesh faces. Default is True.", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "bool" + }, { "name": "i_max_correspondence_distance_icp", "nickname": "i_max_correspondence_distance_icp", From 510b24290e7e3ab997e27ad54485a84afdc5d946 Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Wed, 18 Feb 2026 18:03:37 +0100 Subject: [PATCH 09/10] FIX add missing input to metadata --- src/gh/components/DF_CAD_segmentator/metadata.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/gh/components/DF_CAD_segmentator/metadata.json b/src/gh/components/DF_CAD_segmentator/metadata.json index 7a46563d..2146f6a1 100644 --- a/src/gh/components/DF_CAD_segmentator/metadata.json +++ b/src/gh/components/DF_CAD_segmentator/metadata.json @@ -61,6 +61,18 @@ "sourceCount": 0, "typeHintID": "float" }, + { + "name": "i_maximum_face_segment_distance", + "nickname": "i_maximum_face_segment_distance", + "description": "The maximum correspondence distance for the segmentation. Default is 0.1", + "optional": false, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "float" + }, { "name": "i_radius_normal_estimation", "nickname": "i_radius_normal_estimation", From e5115ba690ae287996e80335f6f7b9d88c3310dd Mon Sep 17 00:00:00 2001 From: eleniv3d <43600924+eleniv3d@users.noreply.github.com> Date: Wed, 18 Feb 2026 18:07:35 +0100 Subject: [PATCH 10/10] FIX add default values for i_max_correspondence_distance_icp and i_maximum_face_segment_distance --- src/gh/components/DF_CAD_segmentator/code.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/gh/components/DF_CAD_segmentator/code.py b/src/gh/components/DF_CAD_segmentator/code.py index d101602a..13371761 100644 --- a/src/gh/components/DF_CAD_segmentator/code.py +++ b/src/gh/components/DF_CAD_segmentator/code.py @@ -37,6 +37,11 @@ def RunScript(self, i_radius_normal_estimation = 0.01 if i_make_registration is None: i_make_registration = True + if i_max_correspondence_distance_icp is None: + i_max_correspondence_distance_icp = 0.1 + if i_maximum_face_segment_distance is None: + i_maximum_face_segment_distance = 0.1 + o_face_clusters = [] transforms = [] df_clusters = []