Skip to content

Commit eb3ab9a

Browse files
authored
Merge pull request #155 from diffCheckOrg/feature/point_cloud_utils
Feature/point cloud utils
2 parents f684ca0 + a1d5266 commit eb3ab9a

File tree

15 files changed

+528
-0
lines changed

15 files changed

+528
-0
lines changed

src/diffCheck/geometry/DFPointCloud.cc

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,44 @@ namespace diffCheck::geometry
216216
this->Normals.push_back(normal);
217217
}
218218

219+
void DFPointCloud::Crop(const Eigen::Vector3d &minBound, const Eigen::Vector3d &maxBound)
220+
{
221+
auto O3DPointCloud = this->Cvt2O3DPointCloud();
222+
auto O3DPointCloudCropped = O3DPointCloud->Crop(open3d::geometry::AxisAlignedBoundingBox(minBound, maxBound));
223+
this->Points.clear();
224+
for (auto &point : O3DPointCloudCropped->points_)
225+
this->Points.push_back(point);
226+
this->Colors.clear();
227+
for (auto &color : O3DPointCloudCropped->colors_)
228+
this->Colors.push_back(color);
229+
this->Normals.clear();
230+
for (auto &normal : O3DPointCloudCropped->normals_)
231+
this->Normals.push_back(normal);
232+
}
233+
234+
void DFPointCloud::Crop(const std::vector<Eigen::Vector3d> &corners)
235+
{
236+
if (corners.size() != 8)
237+
throw std::invalid_argument("The corners vector must contain exactly 8 points.");
238+
open3d::geometry::OrientedBoundingBox obb = open3d::geometry::OrientedBoundingBox::CreateFromPoints(corners);
239+
auto O3DPointCloud = this->Cvt2O3DPointCloud();
240+
auto O3DPointCloudCropped = O3DPointCloud->Crop(obb);
241+
this->Points.clear();
242+
for (auto &point : O3DPointCloudCropped->points_)
243+
this->Points.push_back(point);
244+
this->Colors.clear();
245+
for (auto &color : O3DPointCloudCropped->colors_)
246+
this->Colors.push_back(color);
247+
this->Normals.clear();
248+
for (auto &normal : O3DPointCloudCropped->normals_)
249+
this->Normals.push_back(normal);
250+
}
251+
252+
DFPointCloud DFPointCloud::Duplicate() const
253+
{
254+
return DFPointCloud(this->Points, this->Colors, this->Normals);
255+
}
256+
219257
void DFPointCloud::UniformDownsample(int everyKPoints)
220258
{
221259
auto O3DPointCloud = this->Cvt2O3DPointCloud();
@@ -258,6 +296,86 @@ namespace diffCheck::geometry
258296
return bboxPts;
259297
}
260298

299+
void DFPointCloud::SubtractPoints(const DFPointCloud &pointCloud, double distanceThreshold)
300+
{
301+
if (this->Points.size() == 0 || pointCloud.Points.size() == 0)
302+
throw std::invalid_argument("One of the point clouds is empty.");
303+
304+
auto O3DSourcePointCloud = this->Cvt2O3DPointCloud();
305+
auto O3DTargetPointCloud = std::make_shared<DFPointCloud>(pointCloud)->Cvt2O3DPointCloud();
306+
auto O3DResultPointCloud = std::make_shared<open3d::geometry::PointCloud>();
307+
308+
open3d::geometry::KDTreeFlann threeDTree;
309+
threeDTree.SetGeometry(*O3DTargetPointCloud);
310+
std::vector<int> indices;
311+
std::vector<double> distances;
312+
for (const auto &point : O3DSourcePointCloud->points_)
313+
{
314+
threeDTree.SearchRadius(point, distanceThreshold, indices, distances);
315+
if (indices.empty())
316+
{
317+
O3DResultPointCloud->points_.push_back(point);
318+
if (O3DSourcePointCloud->HasColors())
319+
{
320+
O3DResultPointCloud->colors_.push_back(O3DSourcePointCloud->colors_[&point - &O3DSourcePointCloud->points_[0]]);
321+
}
322+
if (O3DSourcePointCloud->HasNormals())
323+
{
324+
O3DResultPointCloud->normals_.push_back(O3DSourcePointCloud->normals_[&point - &O3DSourcePointCloud->points_[0]]);
325+
}
326+
}
327+
}
328+
this->Points.clear();
329+
for (auto &point : O3DResultPointCloud->points_)
330+
this->Points.push_back(point);
331+
if (O3DResultPointCloud->HasColors())
332+
{
333+
this->Colors.clear();
334+
for (auto &color : O3DResultPointCloud->colors_){this->Colors.push_back(color);};
335+
}
336+
if (O3DResultPointCloud->HasNormals())
337+
{
338+
this->Normals.clear();
339+
for (auto &normal : O3DResultPointCloud->normals_){this->Normals.push_back(normal);};
340+
}
341+
}
342+
343+
diffCheck::geometry::DFPointCloud DFPointCloud::Intersect(const DFPointCloud &pointCloud, double distanceThreshold)
344+
{
345+
if (this->Points.size() == 0 || pointCloud.Points.size() == 0)
346+
throw std::invalid_argument("One of the point clouds is empty.");
347+
348+
auto O3DSourcePointCloud = this->Cvt2O3DPointCloud();
349+
auto O3DTargetPointCloud = std::make_shared<DFPointCloud>(pointCloud)->Cvt2O3DPointCloud();
350+
auto O3DResultPointCloud = std::make_shared<open3d::geometry::PointCloud>();
351+
352+
open3d::geometry::KDTreeFlann threeDTree;
353+
threeDTree.SetGeometry(*O3DTargetPointCloud);
354+
std::vector<int> indices;
355+
std::vector<double> distances;
356+
for (const auto &point : O3DSourcePointCloud->points_)
357+
{
358+
threeDTree.SearchRadius(point, distanceThreshold, indices, distances);
359+
if (!indices.empty())
360+
{
361+
O3DResultPointCloud->points_.push_back(point);
362+
if (O3DSourcePointCloud->HasColors())
363+
{
364+
O3DResultPointCloud->colors_.push_back(O3DSourcePointCloud->colors_[&point - &O3DSourcePointCloud->points_[0]]);
365+
}
366+
if (O3DSourcePointCloud->HasNormals())
367+
{
368+
O3DResultPointCloud->normals_.push_back(O3DSourcePointCloud->normals_[&point - &O3DSourcePointCloud->points_[0]]);
369+
}
370+
}
371+
}
372+
diffCheck::geometry::DFPointCloud result;
373+
result.Points = O3DResultPointCloud->points_;
374+
result.Colors = O3DResultPointCloud->colors_;
375+
result.Normals = O3DResultPointCloud->normals_;
376+
return result;
377+
}
378+
261379
void DFPointCloud::ApplyTransformation(const diffCheck::transformation::DFTransformation &transformation)
262380
{
263381
auto O3DPointCloud = this->Cvt2O3DPointCloud();

src/diffCheck/geometry/DFPointCloud.hh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,27 @@ namespace diffCheck::geometry
8989
*/
9090
void RemoveStatisticalOutliers(int nbNeighbors, double stdRatio);
9191

92+
/**
93+
* @brief Crop the point cloud to a bounding box defined by the min and max bounds
94+
*
95+
* @param minBound the minimum bound of the bounding box as an Eigen::Vector3d
96+
* @param maxBound the maximum bound of the bounding box as an Eigen::Vector3d
97+
*/
98+
void Crop(const Eigen::Vector3d &minBound, const Eigen::Vector3d &maxBound);
99+
100+
/**
101+
* @brief Crop the point cloud to a bounding box defined by the 8 corners of the box
102+
* @param corners the 8 corners of the bounding box as a vector of Eigen::Vector3d
103+
*/
104+
void Crop(const std::vector<Eigen::Vector3d> &corners);
105+
106+
/**
107+
* @brief Get the duplicate of the point cloud. This is mainly used in the python bindings
108+
*
109+
* @return DFPointCloud a copy of the point cloud
110+
*/
111+
diffCheck::geometry::DFPointCloud Duplicate() const;
112+
92113
public: ///< Downsamplers
93114
/**
94115
* @brief Downsample the point cloud with voxel grid
@@ -136,6 +157,24 @@ namespace diffCheck::geometry
136157
* ///
137158
*/
138159
std::vector<Eigen::Vector3d> GetTightBoundingBox();
160+
161+
public: ///< Point cloud subtraction and intersection
162+
/**
163+
* @brief Subtract the points, colors and normals from another point cloud when they are too close to the points of another point cloud.
164+
*
165+
* @param pointCloud the other point cloud to subtract from this one
166+
* @param distanceThreshold the distance threshold to consider a point as too close. Default is 0.01.
167+
*/
168+
void SubtractPoints(const DFPointCloud &pointCloud, double distanceThreshold = 0.01);
169+
170+
/**
171+
* @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.
172+
*
173+
* @param pointCloud the other point cloud to intersect with this one
174+
* @param distanceThreshold the distance threshold to consider a point as too close. Default is 0.01.
175+
* @return diffCheck::geometry::DFPointCloud the intersected point cloud
176+
*/
177+
diffCheck::geometry::DFPointCloud Intersect(const DFPointCloud &pointCloud, double distanceThreshold = 0.01);
139178

140179
public: ///< Transformers
141180
/**

src/diffCheckBindings.cc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ PYBIND11_MODULE(diffcheck_bindings, m) {
4141
.def("downsample_by_size", &diffCheck::geometry::DFPointCloud::DownsampleBySize,
4242
py::arg("target_size"))
4343

44+
.def("subtract_points", &diffCheck::geometry::DFPointCloud::SubtractPoints,
45+
py::arg("point_cloud"), py::arg("distance_threshold"))
46+
47+
.def("intersect", &diffCheck::geometry::DFPointCloud::Intersect,
48+
py::arg("point_cloud"), py::arg("distance_threshold"))
49+
4450
.def("apply_transformation", &diffCheck::geometry::DFPointCloud::ApplyTransformation,
4551
py::arg("transformation"))
4652

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

64+
.def("crop",
65+
(void (diffCheck::geometry::DFPointCloud::*)(const Eigen::Vector3d&, const Eigen::Vector3d&))
66+
&diffCheck::geometry::DFPointCloud::Crop,
67+
py::arg("min_bound"), py::arg("max_bound"))
68+
69+
.def("crop",
70+
(void (diffCheck::geometry::DFPointCloud::*)(const std::vector<Eigen::Vector3d>&))
71+
&diffCheck::geometry::DFPointCloud::Crop,
72+
py::arg("corners"))
73+
74+
.def("duplicate", &diffCheck::geometry::DFPointCloud::Duplicate)
75+
5876
.def("load_from_PLY", &diffCheck::geometry::DFPointCloud::LoadFromPLY)
5977
.def("save_to_PLY", &diffCheck::geometry::DFPointCloud::SaveToPLY)
6078

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from diffCheck import df_cvt_bindings as df_cvt
2+
3+
import Rhino
4+
from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML
5+
6+
from ghpythonlib.componentbase import executingcomponent as component
7+
8+
9+
class DFCloudDifference(component):
10+
def __init__(self):
11+
super(DFCloudDifference, self).__init__()
12+
13+
def RunScript(self,
14+
i_cloud_A: Rhino.Geometry.PointCloud,
15+
i_cloud_B: Rhino.Geometry.PointCloud,
16+
i_distance_threshold: float):
17+
df_cloud = df_cvt.cvt_rhcloud_2_dfcloud(i_cloud_A)
18+
df_cloud_substract = df_cvt.cvt_rhcloud_2_dfcloud(i_cloud_B)
19+
if i_distance_threshold is None:
20+
ghenv.Component.AddRuntimeMessage(RML.Warning, "Distance threshold not defined. 0.01 used as default value.")# noqa: F821
21+
i_distance_threshold = 0.01
22+
if i_distance_threshold <= 0:
23+
ghenv.Component.AddRuntimeMessage(RML.Warning, "Distance threshold must be greater than 0. Please provide a valid distance threshold.")# noqa: F821
24+
return None
25+
df_cloud.subtract_points(df_cloud_substract, i_distance_threshold)
26+
rh_cloud = df_cvt.cvt_dfcloud_2_rhcloud(df_cloud)
27+
return [rh_cloud]
15.1 KB
Loading
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"name": "DFCloudDifference",
3+
"nickname": "Difference",
4+
"category": "diffCheck",
5+
"subcategory": "Cloud",
6+
"description": "Subtracts points from a point cloud based on a distance threshold.",
7+
"exposure": 4,
8+
"instanceGuid": "9ef299aa-76dc-4417-9b95-2a374e2b36af",
9+
"ghpython": {
10+
"hideOutput": true,
11+
"hideInput": true,
12+
"isAdvancedMode": true,
13+
"marshalOutGuids": true,
14+
"iconDisplay": 2,
15+
"inputParameters": [
16+
{
17+
"name": "i_cloud_A",
18+
"nickname": "i_cloud_A",
19+
"description": "The point cloud to subtract from.",
20+
"optional": false,
21+
"allowTreeAccess": true,
22+
"showTypeHints": true,
23+
"scriptParamAccess": "item",
24+
"wireDisplay": "default",
25+
"sourceCount": 0,
26+
"typeHintID": "pointcloud"
27+
},
28+
{
29+
"name": "i_cloud_B",
30+
"nickname": "i_cloud_B",
31+
"description": "The point cloud to subtract with.",
32+
"optional": false,
33+
"allowTreeAccess": true,
34+
"showTypeHints": true,
35+
"scriptParamAccess": "item",
36+
"wireDisplay": "default",
37+
"sourceCount": 0,
38+
"typeHintID": "pointcloud"
39+
},
40+
{
41+
"name": "i_distance_threshold",
42+
"nickname": "i_distance_threshold",
43+
"description": "The distance threshold to consider a point as too close.",
44+
"optional": true,
45+
"allowTreeAccess": true,
46+
"showTypeHints": true,
47+
"scriptParamAccess": "item",
48+
"wireDisplay": "default",
49+
"sourceCount": 0,
50+
"typeHintID": "float"
51+
}
52+
],
53+
"outputParameters": [
54+
{
55+
"name": "o_cloud_in",
56+
"nickname": "o_cloud",
57+
"description": "The resulting cloud after subtraction.",
58+
"optional": false,
59+
"sourceCount": 0,
60+
"graft": false
61+
}
62+
]
63+
}
64+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from diffCheck import df_cvt_bindings as df_cvt
2+
3+
import Rhino
4+
from Grasshopper.Kernel import GH_RuntimeMessageLevel as RML
5+
6+
from ghpythonlib.componentbase import executingcomponent as component
7+
8+
9+
class DFCloudIntersection(component):
10+
def __init__(self):
11+
super(DFCloudIntersection, self).__init__()
12+
13+
def RunScript(self,
14+
i_cloud_A: Rhino.Geometry.PointCloud,
15+
i_cloud_B: Rhino.Geometry.PointCloud,
16+
i_distance_threshold: float):
17+
df_cloud = df_cvt.cvt_rhcloud_2_dfcloud(i_cloud_A)
18+
df_cloud_intersect = df_cvt.cvt_rhcloud_2_dfcloud(i_cloud_B)
19+
if i_distance_threshold is None:
20+
ghenv.Component.AddRuntimeMessage(RML.Warning, "Distance threshold not defined. 0.01 used as default value.")# noqa: F821
21+
i_distance_threshold = 0.01
22+
if i_distance_threshold <= 0:
23+
ghenv.Component.AddRuntimeMessage(RML.Warning, "Distance threshold must be greater than 0. Please provide a valid distance threshold.")# noqa: F821
24+
return None
25+
df_intersection = df_cloud.intersect(df_cloud_intersect, i_distance_threshold)
26+
rh_cloud = df_cvt.cvt_dfcloud_2_rhcloud(df_intersection)
27+
return [rh_cloud]
14.9 KB
Loading

0 commit comments

Comments
 (0)