Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3c90378
feat: add a component to crop point clouds
DamienGilliard Jun 17, 2025
a514125
feat: add cropping capability to DFPointCloud
DamienGilliard Jun 17, 2025
f07431f
feat: add binding for cropping capability of point cloud
DamienGilliard Jun 17, 2025
a0289fc
fix: simplify cropping component
DamienGilliard Jul 22, 2025
a98b80f
feat: add point cloud subtraction method and implement it in component
DamienGilliard Jul 22, 2025
fcc1936
feat: add point cloud duplication for python binding
DamienGilliard Jul 30, 2025
d65fb17
feat: add capacity to crop with any brep, and keep trace of in and ou…
DamienGilliard Jul 30, 2025
10d1943
feat: add point cloud intersection calculation
DamienGilliard Jul 30, 2025
02b4756
feat: add point cloud intersection calculation component
DamienGilliard Jul 30, 2025
bbbee07
feat: add point cloud merge component
DamienGilliard Jul 30, 2025
718a3a6
feat: add point cloud intersection calculation to DFPointCloud
DamienGilliard Jul 30, 2025
e7dad06
fix: rename SubstractCloud to CloudDifference and fix mistakes in met…
DamienGilliard Aug 4, 2025
5308e44
fix: rename MergeCloud to CloudUnion and fix mistakes in metadata
DamienGilliard Aug 4, 2025
164ef82
fix: metadata in CropCloud and IntersectCloud
DamienGilliard Aug 4, 2025
1baf875
fix: small typos again in the metadata
DamienGilliard Aug 4, 2025
8cdd175
feat: add possibility to crop pc with any box
DamienGilliard Aug 4, 2025
3eebaa5
feat: improve cropping component following @eleniv3d 's comments
DamienGilliard Aug 4, 2025
bc42fe2
fix: rename CloudCrop to CloudSplit
DamienGilliard Aug 4, 2025
a1d5266
FIX minor cosmetic changes
eleniv3d Oct 2, 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
118 changes: 118 additions & 0 deletions src/diffCheck/geometry/DFPointCloud.cc
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,44 @@ namespace diffCheck::geometry
this->Normals.push_back(normal);
}

void DFPointCloud::Crop(const Eigen::Vector3d &minBound, const Eigen::Vector3d &maxBound)
{
auto O3DPointCloud = this->Cvt2O3DPointCloud();
auto O3DPointCloudCropped = O3DPointCloud->Crop(open3d::geometry::AxisAlignedBoundingBox(minBound, maxBound));
this->Points.clear();
for (auto &point : O3DPointCloudCropped->points_)
this->Points.push_back(point);
this->Colors.clear();
for (auto &color : O3DPointCloudCropped->colors_)
this->Colors.push_back(color);
this->Normals.clear();
for (auto &normal : O3DPointCloudCropped->normals_)
this->Normals.push_back(normal);
}

void DFPointCloud::Crop(const std::vector<Eigen::Vector3d> &corners)
{
if (corners.size() != 8)
throw std::invalid_argument("The corners vector must contain exactly 8 points.");
open3d::geometry::OrientedBoundingBox obb = open3d::geometry::OrientedBoundingBox::CreateFromPoints(corners);
auto O3DPointCloud = this->Cvt2O3DPointCloud();
auto O3DPointCloudCropped = O3DPointCloud->Crop(obb);
this->Points.clear();
for (auto &point : O3DPointCloudCropped->points_)
this->Points.push_back(point);
this->Colors.clear();
for (auto &color : O3DPointCloudCropped->colors_)
this->Colors.push_back(color);
this->Normals.clear();
for (auto &normal : O3DPointCloudCropped->normals_)
this->Normals.push_back(normal);
}

DFPointCloud DFPointCloud::Duplicate() const
{
return DFPointCloud(this->Points, this->Colors, this->Normals);
}

void DFPointCloud::UniformDownsample(int everyKPoints)
{
auto O3DPointCloud = this->Cvt2O3DPointCloud();
Expand Down Expand Up @@ -258,6 +296,86 @@ namespace diffCheck::geometry
return bboxPts;
}

void DFPointCloud::SubtractPoints(const DFPointCloud &pointCloud, double distanceThreshold)
{
if (this->Points.size() == 0 || pointCloud.Points.size() == 0)
throw std::invalid_argument("One of the point clouds is empty.");

auto O3DSourcePointCloud = this->Cvt2O3DPointCloud();
auto O3DTargetPointCloud = std::make_shared<DFPointCloud>(pointCloud)->Cvt2O3DPointCloud();
auto O3DResultPointCloud = std::make_shared<open3d::geometry::PointCloud>();

open3d::geometry::KDTreeFlann threeDTree;
threeDTree.SetGeometry(*O3DTargetPointCloud);
std::vector<int> indices;
std::vector<double> distances;
for (const auto &point : O3DSourcePointCloud->points_)
{
threeDTree.SearchRadius(point, distanceThreshold, indices, distances);
if (indices.empty())
{
O3DResultPointCloud->points_.push_back(point);
if (O3DSourcePointCloud->HasColors())
{
O3DResultPointCloud->colors_.push_back(O3DSourcePointCloud->colors_[&point - &O3DSourcePointCloud->points_[0]]);
}
if (O3DSourcePointCloud->HasNormals())
{
O3DResultPointCloud->normals_.push_back(O3DSourcePointCloud->normals_[&point - &O3DSourcePointCloud->points_[0]]);
}
}
}
this->Points.clear();
for (auto &point : O3DResultPointCloud->points_)
this->Points.push_back(point);
if (O3DResultPointCloud->HasColors())
{
this->Colors.clear();
for (auto &color : O3DResultPointCloud->colors_){this->Colors.push_back(color);};
}
if (O3DResultPointCloud->HasNormals())
{
this->Normals.clear();
for (auto &normal : O3DResultPointCloud->normals_){this->Normals.push_back(normal);};
}
}

diffCheck::geometry::DFPointCloud DFPointCloud::Intersect(const DFPointCloud &pointCloud, double distanceThreshold)
{
if (this->Points.size() == 0 || pointCloud.Points.size() == 0)
throw std::invalid_argument("One of the point clouds is empty.");

auto O3DSourcePointCloud = this->Cvt2O3DPointCloud();
auto O3DTargetPointCloud = std::make_shared<DFPointCloud>(pointCloud)->Cvt2O3DPointCloud();
auto O3DResultPointCloud = std::make_shared<open3d::geometry::PointCloud>();

open3d::geometry::KDTreeFlann threeDTree;
threeDTree.SetGeometry(*O3DTargetPointCloud);
std::vector<int> indices;
std::vector<double> distances;
for (const auto &point : O3DSourcePointCloud->points_)
{
threeDTree.SearchRadius(point, distanceThreshold, indices, distances);
if (!indices.empty())
{
O3DResultPointCloud->points_.push_back(point);
if (O3DSourcePointCloud->HasColors())
{
O3DResultPointCloud->colors_.push_back(O3DSourcePointCloud->colors_[&point - &O3DSourcePointCloud->points_[0]]);
}
if (O3DSourcePointCloud->HasNormals())
{
O3DResultPointCloud->normals_.push_back(O3DSourcePointCloud->normals_[&point - &O3DSourcePointCloud->points_[0]]);
}
}
}
diffCheck::geometry::DFPointCloud result;
result.Points = O3DResultPointCloud->points_;
result.Colors = O3DResultPointCloud->colors_;
result.Normals = O3DResultPointCloud->normals_;
return result;
}

void DFPointCloud::ApplyTransformation(const diffCheck::transformation::DFTransformation &transformation)
{
auto O3DPointCloud = this->Cvt2O3DPointCloud();
Expand Down
39 changes: 39 additions & 0 deletions src/diffCheck/geometry/DFPointCloud.hh
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,27 @@ namespace diffCheck::geometry
*/
void RemoveStatisticalOutliers(int nbNeighbors, double stdRatio);

/**
* @brief Crop the point cloud to a bounding box defined by the min and max bounds
*
* @param minBound the minimum bound of the bounding box as an Eigen::Vector3d
* @param maxBound the maximum bound of the bounding box as an Eigen::Vector3d
*/
void Crop(const Eigen::Vector3d &minBound, const Eigen::Vector3d &maxBound);

/**
* @brief Crop the point cloud to a bounding box defined by the 8 corners of the box
* @param corners the 8 corners of the bounding box as a vector of Eigen::Vector3d
*/
void Crop(const std::vector<Eigen::Vector3d> &corners);

/**
* @brief Get the duplicate of the point cloud. This is mainly used in the python bindings
*
* @return DFPointCloud a copy of the point cloud
*/
diffCheck::geometry::DFPointCloud Duplicate() const;

public: ///< Downsamplers
/**
* @brief Downsample the point cloud with voxel grid
Expand Down Expand Up @@ -136,6 +157,24 @@ namespace diffCheck::geometry
* ///
*/
std::vector<Eigen::Vector3d> GetTightBoundingBox();

public: ///< Point cloud subtraction and intersection
/**
* @brief Subtract the points, colors and normals from another point cloud when they are too close to the points of another point cloud.
*
* @param pointCloud the other point cloud to subtract from this one
* @param distanceThreshold the distance threshold to consider a point as too close. Default is 0.01.
*/
void SubtractPoints(const DFPointCloud &pointCloud, double distanceThreshold = 0.01);

/**
* @brief Intersect the points, colors and normals from another point cloud when they are close enough to the points of another point cloud. Is the point cloud interpretation of a boolean intersection.
*
* @param pointCloud the other point cloud to intersect with this one
* @param distanceThreshold the distance threshold to consider a point as too close. Default is 0.01.
* @return diffCheck::geometry::DFPointCloud the intersected point cloud
*/
diffCheck::geometry::DFPointCloud Intersect(const DFPointCloud &pointCloud, double distanceThreshold = 0.01);

public: ///< Transformers
/**
Expand Down
18 changes: 18 additions & 0 deletions src/diffCheckBindings.cc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ PYBIND11_MODULE(diffcheck_bindings, m) {
.def("downsample_by_size", &diffCheck::geometry::DFPointCloud::DownsampleBySize,
py::arg("target_size"))

.def("subtract_points", &diffCheck::geometry::DFPointCloud::SubtractPoints,
py::arg("point_cloud"), py::arg("distance_threshold"))

.def("intersect", &diffCheck::geometry::DFPointCloud::Intersect,
py::arg("point_cloud"), py::arg("distance_threshold"))

.def("apply_transformation", &diffCheck::geometry::DFPointCloud::ApplyTransformation,
py::arg("transformation"))

Expand All @@ -55,6 +61,18 @@ PYBIND11_MODULE(diffcheck_bindings, m) {
.def("remove_statistical_outliers", &diffCheck::geometry::DFPointCloud::RemoveStatisticalOutliers,
py::arg("nb_neighbors"), py::arg("std_ratio"))

.def("crop",
(void (diffCheck::geometry::DFPointCloud::*)(const Eigen::Vector3d&, const Eigen::Vector3d&))
&diffCheck::geometry::DFPointCloud::Crop,
py::arg("min_bound"), py::arg("max_bound"))

.def("crop",
(void (diffCheck::geometry::DFPointCloud::*)(const std::vector<Eigen::Vector3d>&))
&diffCheck::geometry::DFPointCloud::Crop,
py::arg("corners"))

.def("duplicate", &diffCheck::geometry::DFPointCloud::Duplicate)

.def("load_from_PLY", &diffCheck::geometry::DFPointCloud::LoadFromPLY)
.def("save_to_PLY", &diffCheck::geometry::DFPointCloud::SaveToPLY)

Expand Down
27 changes: 27 additions & 0 deletions src/gh/components/DF_cloud_difference/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from diffCheck import df_cvt_bindings as df_cvt

import Rhino
from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML

from ghpythonlib.componentbase import executingcomponent as component


class DFCloudDifference(component):
def __init__(self):
super(DFCloudDifference, self).__init__()

def RunScript(self,
i_cloud_A: Rhino.Geometry.PointCloud,
i_cloud_B: Rhino.Geometry.PointCloud,
i_distance_threshold: float):
df_cloud = df_cvt.cvt_rhcloud_2_dfcloud(i_cloud_A)
df_cloud_substract = df_cvt.cvt_rhcloud_2_dfcloud(i_cloud_B)
if i_distance_threshold is None:
ghenv.Component.AddRuntimeMessage(RML.Warning, "Distance threshold not defined. 0.01 used as default value.")# noqa: F821
i_distance_threshold = 0.01
if i_distance_threshold <= 0:
ghenv.Component.AddRuntimeMessage(RML.Warning, "Distance threshold must be greater than 0. Please provide a valid distance threshold.")# noqa: F821
return None
df_cloud.subtract_points(df_cloud_substract, i_distance_threshold)
rh_cloud = df_cvt.cvt_dfcloud_2_rhcloud(df_cloud)
return [rh_cloud]
Binary file added src/gh/components/DF_cloud_difference/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions src/gh/components/DF_cloud_difference/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"name": "DFCloudDifference",
"nickname": "Difference",
"category": "diffCheck",
"subcategory": "Cloud",
"description": "Subtracts points from a point cloud based on a distance threshold.",
"exposure": 4,
"instanceGuid": "9ef299aa-76dc-4417-9b95-2a374e2b36af",
"ghpython": {
"hideOutput": true,
"hideInput": true,
"isAdvancedMode": true,
"marshalOutGuids": true,
"iconDisplay": 2,
"inputParameters": [
{
"name": "i_cloud_A",
"nickname": "i_cloud_A",
"description": "The point cloud to subtract from.",
"optional": false,
"allowTreeAccess": true,
"showTypeHints": true,
"scriptParamAccess": "item",
"wireDisplay": "default",
"sourceCount": 0,
"typeHintID": "pointcloud"
},
{
"name": "i_cloud_B",
"nickname": "i_cloud_B",
"description": "The point cloud to subtract with.",
"optional": false,
"allowTreeAccess": true,
"showTypeHints": true,
"scriptParamAccess": "item",
"wireDisplay": "default",
"sourceCount": 0,
"typeHintID": "pointcloud"
},
{
"name": "i_distance_threshold",
"nickname": "i_distance_threshold",
"description": "The distance threshold to consider a point as too close.",
"optional": true,
"allowTreeAccess": true,
"showTypeHints": true,
"scriptParamAccess": "item",
"wireDisplay": "default",
"sourceCount": 0,
"typeHintID": "float"
}
],
"outputParameters": [
{
"name": "o_cloud_in",
"nickname": "o_cloud",
"description": "The resulting cloud after subtraction.",
"optional": false,
"sourceCount": 0,
"graft": false
}
]
}
}
27 changes: 27 additions & 0 deletions src/gh/components/DF_cloud_intersection/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from diffCheck import df_cvt_bindings as df_cvt

import Rhino
from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML

from ghpythonlib.componentbase import executingcomponent as component


class DFCloudIntersection(component):
def __init__(self):
super(DFCloudIntersection, self).__init__()

def RunScript(self,
i_cloud_A: Rhino.Geometry.PointCloud,
i_cloud_B: Rhino.Geometry.PointCloud,
i_distance_threshold: float):
df_cloud = df_cvt.cvt_rhcloud_2_dfcloud(i_cloud_A)
df_cloud_intersect = df_cvt.cvt_rhcloud_2_dfcloud(i_cloud_B)
if i_distance_threshold is None:
ghenv.Component.AddRuntimeMessage(RML.Warning, "Distance threshold not defined. 0.01 used as default value.")# noqa: F821
i_distance_threshold = 0.01
if i_distance_threshold <= 0:
ghenv.Component.AddRuntimeMessage(RML.Warning, "Distance threshold must be greater than 0. Please provide a valid distance threshold.")# noqa: F821
return None
df_intersection = df_cloud.intersect(df_cloud_intersect, i_distance_threshold)
rh_cloud = df_cvt.cvt_dfcloud_2_rhcloud(df_intersection)
return [rh_cloud]
Binary file added src/gh/components/DF_cloud_intersection/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading