Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d5ed24e
feat: add principal axes calculation for pointclouds for pose estimation
DamienGilliard Apr 9, 2025
f43e197
fix: add cilantro KMeans clustering to main header
DamienGilliard Apr 9, 2025
4809f30
feat: add empty component for axis calculation. code must be written
DamienGilliard Apr 9, 2025
3486fa4
feat: add python binding for axis calculation
DamienGilliard Apr 9, 2025
ad8e895
feat: add poses module for pose historic saving
DamienGilliard May 7, 2025
4d5c71b
feat-wip: add code to main axis calculation component. OK but reliabi…
DamienGilliard May 7, 2025
256fd88
feat: add 'save' mathod to DFPoses classes to dump it in a JSON
DamienGilliard May 9, 2025
3971d40
fix: update the metadata of the axis calculation component
DamienGilliard May 9, 2025
13c4759
feat: make pose calculation component leaner
DamienGilliard May 9, 2025
78231f7
feat: slight refactor of df_poses module
DamienGilliard May 9, 2025
d967ea8
fix: simplify k-means clustering implementation
DamienGilliard May 9, 2025
9aadeff
Merge branch 'patch_ws' into implement_pca
DamienGilliard May 9, 2025
188dc81
feat: add unit test to KMeans clustering of normals
DamienGilliard May 10, 2025
56a80d5
fix: marameter name from singular to plural, and corresponding list a…
DamienGilliard May 16, 2025
0d98ad0
fix: have new elements pick-up the indexes of the already-existing el…
DamienGilliard May 28, 2025
00ceefa
fix: typo in nickname of i_clouds
DamienGilliard May 28, 2025
961b28b
fix: committing insignificant changes to example file because ignorin…
DamienGilliard Aug 10, 2025
3828e8e
fix: improvements to code component and metadata following @eleniv3d …
DamienGilliard Aug 10, 2025
2181ad9
feat: add plane property to DFBeam
DamienGilliard Aug 11, 2025
cedfcc1
feat: add max id to CAD segmentation to allow segmentation during fab…
DamienGilliard Aug 11, 2025
953f5f4
feat: remove the i_stop_after_id parameter because it is moved to a d…
DamienGilliard Sep 2, 2025
adf1576
feat: create new truncate_assembly component
DamienGilliard Sep 2, 2025
f7016b4
fix: main axis of DFBeam computed such that the main axes are also th…
DamienGilliard Sep 2, 2025
de7b580
fix: add ghpythonlib. to the overrides of mypy
DamienGilliard Sep 2, 2025
5745db8
feat: update DFMainPCAxes to save the new poses only when user trigge…
DamienGilliard Sep 2, 2025
a4e54d2
fix: name of class in truncate_assembly component
DamienGilliard Sep 9, 2025
a637b5d
fix: remove unused function parameter in DF_CAD_segmentator component
DamienGilliard Sep 9, 2025
2513713
fix: change index in TruncateAssembly component
DamienGilliard Oct 2, 2025
a851476
fix: change sign of index change in TruncateAssembly component
DamienGilliard Oct 2, 2025
b00e145
fix: output the history as ghtree
DamienGilliard Oct 15, 2025
6b38623
feat: add fallback to obb when knn on normals gives insufficiently di…
DamienGilliard Oct 15, 2025
daff8bb
ADD check i_assembly has enough beams and if there is an error on a c…
eleniv3d Oct 20, 2025
067903e
FIX rename files assossiated with Pose Estimation component to have t…
eleniv3d Oct 20, 2025
1fad289
FIX typo and use not all sticky but a single namespaced key df_poses
eleniv3d Oct 20, 2025
2995041
FIX remove re-assignying new_xDirection and fix projection to use tth…
eleniv3d Oct 20, 2025
f29ec53
Merge pull request #163 from diffCheckOrg/implement_pca_patch/improve…
eleniv3d Oct 20, 2025
22d4fda
Merge branch 'release/2.0.0' into implement_pca
DamienGilliard Oct 20, 2025
4d2f3b1
fix: small merge issue
DamienGilliard Oct 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified doc/_static/example_files/subtractive_gh_v1.gh
Binary file not shown.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ module = [
"GH_IO.*",
"clr.*",
"diffcheck_bindings",
"diffCheck.diffcheck_bindings"
"diffCheck.diffcheck_bindings",
"ghpythonlib.*"
]
ignore_missing_imports = true

Expand Down
3 changes: 3 additions & 0 deletions src/diffCheck.hh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
#include <loguru.hpp>

#include <cilantro/cilantro.hpp>
#include <cilantro/clustering/kmeans.hpp>

#include <Eigen/Dense>

// diffCheck includes
#include "diffCheck/log.hh"
Expand Down
7 changes: 7 additions & 0 deletions src/diffCheck/IOManager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/diffCheck/IOManager.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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
70 changes: 70 additions & 0 deletions src/diffCheck/geometry/DFPointCloud.cc
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,76 @@ namespace diffCheck::geometry
this->Normals.push_back(normal);
}

std::vector<Eigen::Vector3d> DFPointCloud::GetPrincipalAxes(int nComponents)
{
std::vector<Eigen::Vector3d> 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<double, 3, Eigen::Dynamic> normalMatrix(3, this->Normals.size());
for (size_t i = 0; i < this->Normals.size(); ++i)
{
normalMatrix.col(i) = this->Normals[i].cast<double>();
}

cilantro::KMeans<double, 3> kmeans(normalMatrix);
kmeans.cluster(nComponents);

const cilantro::VectorSet3d& centroids = kmeans.getClusterCentroids();
const std::vector<size_t>& assignments = kmeans.getPointToClusterIndexMap();
std::vector<int> clusterSizes(nComponents, 0);
for (size_t i = 0; i < assignments.size(); ++i)
{
clusterSizes[assignments[i]]++;
}
// Sort clusters by size
std::vector<std::pair<int, Eigen::Vector3d>> 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)
{
if(principalAxes.size() == 0)
{
principalAxes.push_back(sortedClustersBySize[i].second);
}
else
{
bool isAlreadyPresent = false;
for (const auto& axis : principalAxes)
{
double dotProduct = std::abs(axis.dot(sortedClustersBySize[i].second));
if (std::abs(dotProduct) > 0.7) // Threshold to consider as similar direction
{
isAlreadyPresent = true;
break;
}
}
if (!isAlreadyPresent)
{
principalAxes.push_back(sortedClustersBySize[i].second);
}
}
}
if (principalAxes.size() < 2) // Fallback to OBB if k-means fails to provide enough distinct axes
{
open3d::geometry::OrientedBoundingBox obb = this->Cvt2O3DPointCloud()->GetOrientedBoundingBox();
principalAxes = {obb.R_.col(0), obb.R_.col(1), obb.R_.col(2)};
}
return principalAxes;
}

void DFPointCloud::Crop(const Eigen::Vector3d &minBound, const Eigen::Vector3d &maxBound)
{
auto O3DPointCloud = this->Cvt2O3DPointCloud();
Expand Down
10 changes: 10 additions & 0 deletions src/diffCheck/geometry/DFPointCloud.hh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

#include <cilantro/utilities/point_cloud.hpp>
#include <cilantro/core/nearest_neighbors.hpp>
#include <cilantro/clustering/kmeans.hpp>


namespace diffCheck::geometry
{
Expand Down Expand Up @@ -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<Eigen::Vector3d> the principal axes of the point cloud ordered by number of normals
*/
std::vector<Eigen::Vector3d> GetPrincipalAxes(int nComponents = 6);

/**
* @brief Crop the point cloud to a bounding box defined by the min and max bounds
*
Expand Down
2 changes: 2 additions & 0 deletions src/diffCheckBindings.cc
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ 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("crop",
(void (diffCheck::geometry::DFPointCloud::*)(const Eigen::Vector3d&, const Eigen::Vector3d&))
&diffCheck::geometry::DFPointCloud::Crop,
Expand Down
68 changes: 68 additions & 0 deletions src/gh/components/DF_pose_estimation/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#! python3

from diffCheck import df_cvt_bindings
from diffCheck import df_poses

import Rhino
from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML

from ghpythonlib.componentbase import executingcomponent as component
import System


class DFPoseEstimation(component):
def RunScript(self,
i_clouds: System.Collections.Generic.List[Rhino.Geometry.PointCloud],
i_assembly,
i_save: bool,
i_reset: bool):

# ensure assembly has enough beams
if len(i_assembly.beams) < len(i_clouds):
ghenv.Component.AddRuntimeMessage(RML.Warning, "Assembly has fewer beams than input clouds") # noqa: F821
return None, None

planes = []
all_poses_in_time = df_poses.DFPosesAssembly()
if i_reset:
all_poses_in_time.reset()
return None, None

all_poses_this_time = []
for i, cloud in enumerate(i_clouds):
try:
df_cloud = df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud)
if df_cloud is None:
return None, None
if not df_cloud.has_normals():
ghenv.Component.AddRuntimeMessage(RML.Error, f"Point cloud {i} has no normals. Please compute the normals.") # noqa: F821

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])

axes = df_cloud.get_principal_axes(3)
vectors = []
for axe in axes:
vectors.append(Rhino.Geometry.Vector3d(axe[0], axe[1], axe[2]))

new_xDirection, new_yDirection = df_poses.select_vectors(vectors, i_assembly.beams[i].plane.XAxis, i_assembly.beams[i].plane.YAxis)

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)
except Exception as e:
# Any unexpected error on this cloud, skip it and keep going
ghenv.Component.AddRuntimeMessage(RML.Error, f"Cloud {i}: processing failed ({e}); skipping.") # noqa: F821
planes.append(None)
all_poses_this_time.append(None)
continue

if i_save:
all_poses_in_time.add_step(all_poses_this_time)

return [planes, all_poses_in_time.to_gh_tree()]
Binary file added src/gh/components/DF_pose_estimation/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 84 additions & 0 deletions src/gh/components/DF_pose_estimation/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"name": "DFPoseEstimation",
"nickname": "PoseEsimation",
"category": "diffCheck",
"subcategory": "PointCloud",
"description": "This compoment calculates the pose of a list of point clouds.",
"exposure": 4,
"instanceGuid": "a13c4414-f5df-46e6-beae-7054bb9c3e72",
"ghpython": {
"hideOutput": true,
"hideInput": true,
"isAdvancedMode": true,
"marshalOutGuids": true,
"iconDisplay": 2,
"inputParameters": [
{
"name": "i_clouds",
"nickname": "i_clouds",
"description": "clouds whose pose is to be calculated",
"optional": false,
"allowTreeAccess": true,
"showTypeHints": true,
"scriptParamAccess": "list",
"wireDisplay": "default",
"sourceCount": 0,
"typeHintID": "pointcloud"
},
{
"name": "i_assembly",
"nickname": "i_assembly",
"description": "The DFAssembly corresponding to the list of clouds.",
"optional": false,
"allowTreeAccess": true,
"showTypeHints": true,
"scriptParamAccess": "item",
"wireDisplay": "default",
"sourceCount": 0,
"typeHintID": "ghdoc"
},
{
"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": "bool"
},
{
"name": "i_save",
"nickname": "i_save",
"description": "save the poses computed at this iteration",
"optional": true,
"allowTreeAccess": false,
"showTypeHints": true,
"scriptParamAccess": "item",
"wireDisplay": "default",
"sourceCount": 0,
"typeHintID": "bool"
}
],
"outputParameters": [
{
"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 per elements.",
"optional": false,
"sourceCount": 0,
"graft": false
}
]
}
}
15 changes: 15 additions & 0 deletions src/gh/components/DF_truncate_assembly/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from ghpythonlib.componentbase import executingcomponent as component

import diffCheck
import diffCheck.df_geometries

class DFTruncateAssembly(component):
def RunScript(self,
i_assembly,
i_truncate_index: int):
beams = i_assembly.beams[:i_truncate_index]
name = i_assembly.name

o_assembly = diffCheck.df_geometries.DFAssembly(name=name, beams=beams)
ghenv.Component.Message = f"number of beams: {len(o_assembly.beams)}" # noqa: F821
return o_assembly
Binary file added src/gh/components/DF_truncate_assembly/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions src/gh/components/DF_truncate_assembly/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "DFTruncateAssembly",
"nickname": "TruncateAssembly",
"category": "diffCheck",
"subcategory": "Structure",
"description": "This component truncates an assembly.",
"exposure": 4,
"instanceGuid": "cf8af97f-dd84-40b6-af44-bf6aca7b941b",
"ghpython": {
"hideOutput": true,
"hideInput": true,
"isAdvancedMode": true,
"marshalOutGuids": true,
"iconDisplay": 2,
"inputParameters": [
{
"name": "i_assembly",
"nickname": "i_assembly",
"description": "The assembly to be truncated.",
"optional": false,
"allowTreeAccess": true,
"showTypeHints": true,
"scriptParamAccess": "item",
"wireDisplay": "default",
"sourceCount": 0,
"typeHintID": "ghdoc"
},
{
"name": "i_truncate_index",
"nickname": "i_truncate_index",
"description": "The index at which to truncate the assembly.",
"optional": false,
"allowTreeAccess": false,
"showTypeHints": true,
"scriptParamAccess": "item",
"wireDisplay": "default",
"sourceCount": 0,
"typeHintID": "int"
}
],
"outputParameters": [
{
"name": "o_assembly",
"nickname": "o_assembly",
"description": "The resulting assembly after truncation.",
"optional": false,
"sourceCount": 0,
"graft": false
}
]
}
}
Loading
Loading