Skip to content

Commit 2ebd0a7

Browse files
kongsgardyxlao
authored andcommitted
Add point cloud plane segmentation (#1287)
* Add point cloud plane segmentation * Simplify the RANSAC implementation * Add point cloud plane segmentation example * Add function 'GetPlaneFromPoints' * Add unit test * Fix minor issues * Fix Python formatting
1 parent afe0834 commit 2ebd0a7

File tree

7 files changed

+305
-13
lines changed

7 files changed

+305
-13
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Open3D: www.open3d.org
2+
# The MIT License (MIT)
3+
# See license file or visit www.open3d.org for details
4+
5+
# examples/Python/Basic/pointcloud_plane_segmentation.py
6+
7+
import numpy as np
8+
import open3d as o3d
9+
10+
if __name__ == "__main__":
11+
pcd = o3d.io.read_point_cloud("../../TestData/fragment.pcd")
12+
13+
print(
14+
"Find the plane model and the inliers of the largest planar segment in the point cloud."
15+
)
16+
plane_model, inliers = pcd.segment_plane(distance_threshold=0.01,
17+
ransac_n=3,
18+
num_iterations=250)
19+
20+
[a, b, c, d] = plane_model
21+
print(f"Plane model: {a:.2f}x + {b:.2f}y + {c:.2f}z + {d:.2f} = 0")
22+
23+
inlier_cloud = pcd.select_down_sample(inliers)
24+
inlier_cloud.paint_uniform_color([1.0, 0, 0])
25+
26+
outlier_cloud = pcd.select_down_sample(inliers, invert=True)
27+
28+
o3d.visualization.draw_geometries([inlier_cloud, outlier_cloud])

src/Open3D/Geometry/PointCloud.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,20 @@ class PointCloud : public Geometry3D {
211211
size_t min_points,
212212
bool print_progress = false) const;
213213

214+
/// \brief Segment PointCloud plane using the RANSAC algorithm.
215+
///
216+
/// \param distance_threshold Max distance a point can be from the plane
217+
/// model, and still be considered an inlier.
218+
/// \param ransac_n Number of initial points to be considered inliers in
219+
/// each iteration.
220+
/// \param num_iterations Number of iterations.
221+
/// \return Returns the plane model ax + by + cz + d = 0 and the indices of
222+
/// the plane inliers.
223+
std::tuple<Eigen::Vector4d, std::vector<size_t>> SegmentPlane(
224+
const double distance_threshold = 0.01,
225+
const int ransac_n = 3,
226+
const int num_iterations = 100) const;
227+
214228
/// Factory function to create a pointcloud from a depth image and a camera
215229
/// model (PointCloudFactory.cpp)
216230
/// The input depth image can be either a float image, or a uint16_t image.
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// ----------------------------------------------------------------------------
2+
// - Open3D: www.open3d.org -
3+
// ----------------------------------------------------------------------------
4+
// The MIT License (MIT)
5+
//
6+
// Copyright (c) 2019 www.open3d.org
7+
//
8+
// Permission is hereby granted, free of charge, to any person obtaining a copy
9+
// of this software and associated documentation files (the "Software"), to deal
10+
// in the Software without restriction, including without limitation the rights
11+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
// copies of the Software, and to permit persons to whom the Software is
13+
// furnished to do so, subject to the following conditions:
14+
//
15+
// The above copyright notice and this permission notice shall be included in
16+
// all copies or substantial portions of the Software.
17+
//
18+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24+
// IN THE SOFTWARE.
25+
// ----------------------------------------------------------------------------
26+
27+
#include "Open3D/Geometry/PointCloud.h"
28+
29+
#include <Eigen/Dense>
30+
#include <algorithm>
31+
#include <iterator>
32+
#include <numeric>
33+
#include <random>
34+
#include <unordered_set>
35+
36+
#include "Open3D/Geometry/TriangleMesh.h"
37+
#include "Open3D/Utility/Console.h"
38+
39+
namespace open3d {
40+
namespace geometry {
41+
42+
/// \class RANSACResult
43+
///
44+
/// \brief Stores the current best result in the RANSAC algorithm.
45+
class RANSACResult {
46+
public:
47+
RANSACResult() : fitness_(0), inlier_rmse_(0) {}
48+
~RANSACResult() {}
49+
50+
public:
51+
double fitness_;
52+
double inlier_rmse_;
53+
};
54+
55+
// Calculates the number of inliers given a list of points and a plane model,
56+
// and the total distance between the inliers and the plane. These numbers are
57+
// then used to evaluate how well the plane model fits the given points.
58+
RANSACResult EvaluateRANSACBasedOnDistance(
59+
const std::vector<Eigen::Vector3d> &points,
60+
const Eigen::Vector4d plane_model,
61+
std::vector<size_t> &inliers,
62+
double distance_threshold,
63+
double error) {
64+
RANSACResult result;
65+
66+
for (size_t idx = 0; idx < points.size(); ++idx) {
67+
Eigen::Vector4d point(points[idx](0), points[idx](1), points[idx](2),
68+
1);
69+
double distance = std::abs(plane_model.dot(point));
70+
71+
if (distance < distance_threshold) {
72+
error += distance;
73+
inliers.emplace_back(idx);
74+
}
75+
}
76+
77+
size_t inlier_num = inliers.size();
78+
if (inlier_num == 0) {
79+
result.fitness_ = 0;
80+
result.inlier_rmse_ = 0;
81+
} else {
82+
result.fitness_ = (double)inlier_num / (double)points.size();
83+
result.inlier_rmse_ = error / std::sqrt((double)inlier_num);
84+
}
85+
return result;
86+
}
87+
88+
// Find the plane such that the summed squared distance from the
89+
// plane to all points is minimized.
90+
//
91+
// Reference:
92+
// https://www.ilikebigbits.com/2015_03_04_plane_from_points.html
93+
Eigen::Vector4d GetPlaneFromPoints(const std::vector<Eigen::Vector3d> &points,
94+
const std::vector<size_t> &inliers) {
95+
Eigen::Vector3d centroid(0, 0, 0);
96+
for (size_t idx : inliers) {
97+
centroid += points[idx];
98+
}
99+
centroid /= double(inliers.size());
100+
101+
double xx = 0, xy = 0, xz = 0, yy = 0, yz = 0, zz = 0;
102+
103+
for (size_t idx : inliers) {
104+
Eigen::Vector3d r = points[idx] - centroid;
105+
xx += r(0) * r(0);
106+
xy += r(0) * r(1);
107+
xz += r(0) * r(2);
108+
yy += r(1) * r(1);
109+
yz += r(1) * r(2);
110+
zz += r(2) * r(2);
111+
}
112+
113+
double det_x = yy * zz - yz * yz;
114+
double det_y = xx * zz - xz * xz;
115+
double det_z = xx * yy - xy * xy;
116+
117+
Eigen::Vector3d abc;
118+
if (det_x > det_y && det_x > det_z) {
119+
abc = Eigen::Vector3d(det_x, xz * yz - xy * zz, xy * yz - xz * yy);
120+
} else if (det_y > det_z) {
121+
abc = Eigen::Vector3d(xz * yz - xy * zz, det_y, xy * xz - yz * xx);
122+
} else {
123+
abc = Eigen::Vector3d(xy * yz - xz * yy, xy * xz - yz * xx, det_z);
124+
}
125+
126+
double norm = abc.norm();
127+
// Return invalid plane if the points don't span a plane.
128+
if (norm == 0) {
129+
return Eigen::Vector4d(0, 0, 0, 0);
130+
}
131+
abc /= abc.norm();
132+
double d = -abc.dot(centroid);
133+
return Eigen::Vector4d(abc(0), abc(1), abc(2), d);
134+
}
135+
136+
std::tuple<Eigen::Vector4d, std::vector<size_t>> PointCloud::SegmentPlane(
137+
const double distance_threshold /* = 0.01 */,
138+
const int ransac_n /* = 3 */,
139+
const int num_iterations /* = 100 */) const {
140+
RANSACResult result;
141+
double error = 0;
142+
143+
// Initialize the plane model ax + by + cz + d = 0.
144+
Eigen::Vector4d plane_model = Eigen::Vector4d(0, 0, 0, 0);
145+
// Initialize the best plane model.
146+
Eigen::Vector4d best_plane_model = Eigen::Vector4d(0, 0, 0, 0);
147+
148+
// Initialize consensus set.
149+
std::vector<size_t> inliers;
150+
151+
size_t num_points = points_.size();
152+
std::vector<size_t> indices(num_points);
153+
std::iota(std::begin(indices), std::end(indices), 0);
154+
155+
std::random_device rd;
156+
std::mt19937 rng(rd());
157+
158+
// Return if ransac_n is less than the required plane model parameters.
159+
if (ransac_n < 3) {
160+
utility::LogError(
161+
"ransac_n should be set to higher than or equal to 3.");
162+
return std::make_tuple(best_plane_model, inliers);
163+
}
164+
165+
for (int itr = 0; itr < num_iterations; itr++) {
166+
for (int i = 0; i < ransac_n; ++i) {
167+
std::swap(indices[i], indices[rng() % num_points]);
168+
}
169+
inliers.clear();
170+
for (int idx = 0; idx < ransac_n; ++idx) {
171+
inliers.emplace_back(indices[idx]);
172+
}
173+
174+
// Fit model to num_model_parameters randomly selected points among the
175+
// inliers.
176+
plane_model = TriangleMesh::ComputeTrianglePlane(
177+
points_[inliers[0]], points_[inliers[1]], points_[inliers[2]]);
178+
if (plane_model.isZero(0)) {
179+
continue;
180+
}
181+
182+
error = 0;
183+
inliers.clear();
184+
auto this_result = EvaluateRANSACBasedOnDistance(
185+
points_, plane_model, inliers, distance_threshold, error);
186+
if (this_result.fitness_ > result.fitness_ ||
187+
(this_result.fitness_ == result.fitness_ &&
188+
this_result.inlier_rmse_ < result.inlier_rmse_)) {
189+
result = this_result;
190+
best_plane_model = plane_model;
191+
}
192+
}
193+
194+
// Find the final inliers using best_plane_model.
195+
inliers.clear();
196+
for (size_t idx = 0; idx < points_.size(); ++idx) {
197+
Eigen::Vector4d point(points_[idx](0), points_[idx](1), points_[idx](2),
198+
1);
199+
double distance = std::abs(best_plane_model.dot(point));
200+
201+
if (distance < distance_threshold) {
202+
inliers.emplace_back(idx);
203+
}
204+
}
205+
206+
// Improve best_plane_model using the final inliers.
207+
best_plane_model = GetPlaneFromPoints(points_, inliers);
208+
209+
utility::LogDebug("RANSAC | Inliers: {:d}, Fitness: {:e}, RMSE: {:e}",
210+
inliers.size(), result.fitness_, result.inlier_rmse_);
211+
return std::make_tuple(best_plane_model, inliers);
212+
}
213+
214+
} // namespace geometry
215+
} // namespace open3d

src/Open3D/Registration/Registration.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ RegistrationResult RegistrationRANSACBasedOnCorrespondence(
236236
result = this_result;
237237
}
238238
}
239-
utility::LogDebug("RANSAC: Fitness {:.4f}, RMSE {:.4f}", result.fitness_,
239+
utility::LogDebug("RANSAC: Fitness {:e}, RMSE {:e}", result.fitness_,
240240
result.inlier_rmse_);
241241
return result;
242242
}
@@ -368,7 +368,7 @@ RegistrationResult RegistrationRANSACBasedOnFeatureMatching(
368368
}
369369
#endif
370370
utility::LogDebug("total_validation : {:d}", total_validation);
371-
utility::LogDebug("RANSAC: Fitness {:.4f}, RMSE {:.4f}", result.fitness_,
371+
utility::LogDebug("RANSAC: Fitness {:e}, RMSE {:e}", result.fitness_,
372372
result.inlier_rmse_);
373373
return result;
374374
}

src/Python/open3d_pybind/geometry/pointcloud.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ void pybind_pointcloud(py::module &m) {
172172
"Spatial Databases with Noise', 1996. Returns a list of point "
173173
"labels, -1 indicates noise according to the algorithm.",
174174
"eps"_a, "min_points"_a, "print_progress"_a = false)
175+
.def("segment_plane", &geometry::PointCloud::SegmentPlane,
176+
"Segments a plane in the point cloud using the RANSAC "
177+
"algorithm.",
178+
"distance_threshold"_a, "ransac_n"_a, "num_iterations"_a)
175179
.def_static(
176180
"create_from_depth_image",
177181
&geometry::PointCloud::CreateFromDepthImage,
@@ -294,6 +298,15 @@ void pybind_pointcloud(py::module &m) {
294298
{"min_points", "Minimum number of points to form a cluster."},
295299
{"print_progress",
296300
"If true the progress is visualized in the console."}});
301+
docstring::ClassMethodDocInject(
302+
m, "PointCloud", "segment_plane",
303+
{{"distance_threshold",
304+
"Max distance a point can be from the plane model, and still be "
305+
"considered an inlier."},
306+
{"ransac_n",
307+
"Number of initial points to be considered inliers in each "
308+
"iteration."},
309+
{"num_iterations", "Number of iterations."}});
297310
docstring::ClassMethodDocInject(m, "PointCloud", "create_from_depth_image");
298311
docstring::ClassMethodDocInject(m, "PointCloud", "create_from_rgbd_image");
299312
}

0 commit comments

Comments
 (0)