diff --git a/src/diffCheck/geometry/DFPointCloud.cc b/src/diffCheck/geometry/DFPointCloud.cc index d00fac65..5b016eda 100644 --- a/src/diffCheck/geometry/DFPointCloud.cc +++ b/src/diffCheck/geometry/DFPointCloud.cc @@ -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 &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(); @@ -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(pointCloud)->Cvt2O3DPointCloud(); + auto O3DResultPointCloud = std::make_shared(); + + open3d::geometry::KDTreeFlann threeDTree; + threeDTree.SetGeometry(*O3DTargetPointCloud); + std::vector indices; + std::vector 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(pointCloud)->Cvt2O3DPointCloud(); + auto O3DResultPointCloud = std::make_shared(); + + open3d::geometry::KDTreeFlann threeDTree; + threeDTree.SetGeometry(*O3DTargetPointCloud); + std::vector indices; + std::vector 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(); diff --git a/src/diffCheck/geometry/DFPointCloud.hh b/src/diffCheck/geometry/DFPointCloud.hh index b3f0a3be..8a65d380 100644 --- a/src/diffCheck/geometry/DFPointCloud.hh +++ b/src/diffCheck/geometry/DFPointCloud.hh @@ -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 &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 @@ -136,6 +157,24 @@ namespace diffCheck::geometry * /// */ std::vector 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 /** diff --git a/src/diffCheckBindings.cc b/src/diffCheckBindings.cc index 434f5da2..1e5ad07e 100644 --- a/src/diffCheckBindings.cc +++ b/src/diffCheckBindings.cc @@ -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")) @@ -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&)) + &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) diff --git a/src/gh/components/DF_cloud_difference/code.py b/src/gh/components/DF_cloud_difference/code.py new file mode 100644 index 00000000..0d54f9e1 --- /dev/null +++ b/src/gh/components/DF_cloud_difference/code.py @@ -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] diff --git a/src/gh/components/DF_cloud_difference/icon.png b/src/gh/components/DF_cloud_difference/icon.png new file mode 100644 index 00000000..cba0ffbf Binary files /dev/null and b/src/gh/components/DF_cloud_difference/icon.png differ diff --git a/src/gh/components/DF_cloud_difference/metadata.json b/src/gh/components/DF_cloud_difference/metadata.json new file mode 100644 index 00000000..ec7dfd40 --- /dev/null +++ b/src/gh/components/DF_cloud_difference/metadata.json @@ -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 + } + ] + } +} \ No newline at end of file diff --git a/src/gh/components/DF_cloud_intersection/code.py b/src/gh/components/DF_cloud_intersection/code.py new file mode 100644 index 00000000..8ebe1b4e --- /dev/null +++ b/src/gh/components/DF_cloud_intersection/code.py @@ -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] diff --git a/src/gh/components/DF_cloud_intersection/icon.png b/src/gh/components/DF_cloud_intersection/icon.png new file mode 100644 index 00000000..6eeb70e5 Binary files /dev/null and b/src/gh/components/DF_cloud_intersection/icon.png differ diff --git a/src/gh/components/DF_cloud_intersection/metadata.json b/src/gh/components/DF_cloud_intersection/metadata.json new file mode 100644 index 00000000..e12fd3ff --- /dev/null +++ b/src/gh/components/DF_cloud_intersection/metadata.json @@ -0,0 +1,64 @@ +{ + "name": "DFCloudIntersection", + "nickname": "Intersection", + "category": "diffCheck", + "subcategory": "Cloud", + "description": "Intersects points from two point clouds based on a distance threshold.", + "exposure": 4, + "instanceGuid": "b1a87021-dc4d-4844-86e0-8dcf55965ac6", + "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 intersect 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 intersect 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 close enough.", + "optional": true, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "float" + } + ], + "outputParameters": [ + { + "name": "o_cloud", + "nickname": "o_cloud", + "description": "The resulting cloud after intersection.", + "optional": false, + "sourceCount": 0, + "graft": false + } + ] + } +} \ No newline at end of file diff --git a/src/gh/components/DF_cloud_split/code.py b/src/gh/components/DF_cloud_split/code.py new file mode 100644 index 00000000..263136cb --- /dev/null +++ b/src/gh/components/DF_cloud_split/code.py @@ -0,0 +1,42 @@ +"""Crops a point cloud by giving the bounding box or a brep.""" +from diffCheck import df_cvt_bindings as df_cvt + +import numpy as np + +import Rhino + +from ghpythonlib.componentbase import executingcomponent as component + +TOL = Rhino.RhinoDoc.ActiveDoc.ModelAbsoluteTolerance + +class DFCloudSplit(component): + def __init__(self): + super(DFCloudSplit, self).__init__() + + def RunScript(self, + i_cloud: Rhino.Geometry.PointCloud, + i_boundary: Rhino.Geometry.Brep): + + if i_boundary.IsBox(): + vertices = i_boundary.Vertices + bb_as_array = [np.asarray([vertice.Location.X, vertice.Location.Y, vertice.Location.Z]) for vertice in vertices] + df_cloud = df_cvt.cvt_rhcloud_2_dfcloud(i_cloud) + df_cloud_copy = df_cloud.duplicate() + df_cloud.crop(bb_as_array) + df_cloud_copy.subtract_points(df_cloud, TOL) + o_pts_out = df_cvt.cvt_dfcloud_2_rhcloud(df_cloud_copy) + o_pts_in = df_cvt.cvt_dfcloud_2_rhcloud(df_cloud) + + else: + pts_in = [] + pts_out = [] + for pc_item in i_cloud: + point = Rhino.Geometry.Point3d(pc_item.X, pc_item.Y, pc_item.Z) + if i_boundary.IsPointInside(point, TOL, True): + pts_in.append(point) + else: + pts_out.append(point) + o_pts_in = Rhino.Geometry.PointCloud(pts_in) + o_pts_out = Rhino.Geometry.PointCloud(pts_out) + + return [o_pts_in, o_pts_out] diff --git a/src/gh/components/DF_cloud_split/icon.png b/src/gh/components/DF_cloud_split/icon.png new file mode 100644 index 00000000..2d21368a Binary files /dev/null and b/src/gh/components/DF_cloud_split/icon.png differ diff --git a/src/gh/components/DF_cloud_split/metadata.json b/src/gh/components/DF_cloud_split/metadata.json new file mode 100644 index 00000000..c4ca6852 --- /dev/null +++ b/src/gh/components/DF_cloud_split/metadata.json @@ -0,0 +1,60 @@ +{ + "name": "DFCloudSplit", + "nickname": "Split", + "category": "diffCheck", + "subcategory": "Cloud", + "description": "Splits a point cloud using a boundary volume.", + "exposure": 4, + "instanceGuid": "f0461287-b1aa-47ec-87c4-0f03924cea24", + "ghpython": { + "hideOutput": true, + "hideInput": true, + "isAdvancedMode": true, + "marshalOutGuids": true, + "iconDisplay": 2, + "inputParameters": [ + { + "name": "i_cloud", + "nickname": "i_cloud", + "description": "The point cloud to split.", + "optional": false, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "pointcloud" + }, + { + "name": "i_boundary", + "nickname": "i_boundary", + "description": "The brep boundary to split the point cloud with. If a box is provided, computation will be faster. If a generic brep is provided, it will be used but may be slower.", + "optional": false, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "item", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "brep" + } + ], + "outputParameters": [ + { + "name": "o_cloud_inside", + "nickname": "o_cloud_inside", + "description": "the points inside the splitting region.", + "optional": false, + "sourceCount": 0, + "graft": false + }, + { + "name": "o_cloud_outside", + "nickname": "o_cloud_outside", + "description": "the points outside the splitting region.", + "optional": false, + "sourceCount": 0, + "graft": false + } + ] + } +} \ No newline at end of file diff --git a/src/gh/components/DF_cloud_union/code.py b/src/gh/components/DF_cloud_union/code.py new file mode 100644 index 00000000..2c341023 --- /dev/null +++ b/src/gh/components/DF_cloud_union/code.py @@ -0,0 +1,28 @@ +"""Merges point clouds together.""" +import diffCheck +from diffCheck.diffcheck_bindings import dfb_geometry as df_geometry +import Rhino + +import System + +from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML + +from ghpythonlib.componentbase import executingcomponent as component + +TOL = Rhino.RhinoDoc.ActiveDoc.ModelAbsoluteTolerance + + +class DFCloudUnion(component): + def RunScript(self, + i_clouds: System.Collections.Generic.List[Rhino.Geometry.PointCloud]): + if i_clouds is None or len(i_clouds) == 0: + ghenv.Component.AddRuntimeMessage(RML.Warning, "No point clouds provided. Please connect point clouds to the input.") # noqa: F821 + return None + + merged_cloud = df_geometry.DFPointCloud() + for cloud in i_clouds: + df_cloud = diffCheck.df_cvt_bindings.cvt_rhcloud_2_dfcloud(cloud) + merged_cloud.add_points(df_cloud) + + o_cloud = diffCheck.df_cvt_bindings.cvt_dfcloud_2_rhcloud(merged_cloud) + return [o_cloud] diff --git a/src/gh/components/DF_cloud_union/icon.png b/src/gh/components/DF_cloud_union/icon.png new file mode 100644 index 00000000..bda1f58c Binary files /dev/null and b/src/gh/components/DF_cloud_union/icon.png differ diff --git a/src/gh/components/DF_cloud_union/metadata.json b/src/gh/components/DF_cloud_union/metadata.json new file mode 100644 index 00000000..da4d63cc --- /dev/null +++ b/src/gh/components/DF_cloud_union/metadata.json @@ -0,0 +1,41 @@ +{ + "name": "DFCloudUnion", + "nickname": "DFCloudUnion", + "category": "diffCheck", + "subcategory": "Cloud", + "description": "This component merges a series of point clouds into a unique point cloud.", + "exposure": 4, + "instanceGuid": "1e5e3ce8-1eb8-4227-9456-016f3cedd235", + "ghpython": { + "hideOutput": true, + "hideInput": true, + "isAdvancedMode": true, + "marshalOutGuids": true, + "iconDisplay": 2, + "inputParameters": [ + { + "name": "i_clouds", + "nickname": "i_clouds", + "description": "The point clouds to merge.", + "optional": false, + "allowTreeAccess": true, + "showTypeHints": true, + "scriptParamAccess": "list", + "wireDisplay": "default", + "sourceCount": 0, + "typeHintID": "pointcloud", + "flatten": true + } + ], + "outputParameters": [ + { + "name": "o_cloud", + "nickname": "o_cloud", + "description": "The merged point clouds.", + "optional": false, + "sourceCount": 0, + "graft": false + } + ] + } +} \ No newline at end of file