gtsam_points
Loading...
Searching...
No Matches
min_cut_impl.hpp
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2025 Kenji Koide (k.koide@aist.go.jp)
3
4// The min-cut code is inspired by the following PCL code:
5// https://github.com/PointCloudLibrary/pcl/blob/master/segmentation/include/pcl/segmentation/impl/min_cut_segmentation.hpp
6
7/*
8 * Software License Agreement (BSD License)
9 *
10 * Point Cloud Library (PCL) - www.pointclouds.org
11 *
12 * All rights reserved.
13 *
14 * Redistribution and use in source and binary forms, with or without
15 * modification, are permitted provided that the following conditions
16 * are met:
17 *
18 * * Redistributions of source code must retain the above copyright
19 * notice, this list of conditions and the following disclaimer.
20 * * Redistributions in binary form must reproduce the above
21 * copyright notice, this list of conditions and the following
22 * disclaimer in the documentation and/or other materials provided
23 * with the distribution.
24 * * Neither the name of the copyright holder(s) nor the names of its
25 * contributors may be used to endorse or promote products derived
26 * from this software without specific prior written permission.
27 *
28 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
29 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
31 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
32 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
33 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
34 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
35 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
36 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
37 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
38 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
39 * POSSIBILITY OF SUCH DAMAGE.
40 *
41 * $Id:$
42 *
43 */
44
45#include <gtsam_points/types/frame_traits.hpp>
46#include <gtsam_points/segmentation/min_cut.hpp>
47
48#include <boost/graph/adjacency_list.hpp>
49#include <boost/graph/graph_traits.hpp>
50#include <boost/graph/one_bit_color_map.hpp>
51#include <boost/graph/stoer_wagner_min_cut.hpp>
52#include <boost/graph/boykov_kolmogorov_max_flow.hpp>
53#include <boost/graph/graph_utility.hpp>
54#include <boost/property_map/property_map.hpp>
55
56namespace gtsam_points {
57
58template <typename PointCloud>
59MinCutResult min_cut_(const PointCloud& points, const NearestNeighborSearch& search, const size_t source_pt_index, const MinCutParams& params) {
60 const size_t INVALID_INDEX = std::numeric_limits<size_t>::max();
61
62 // Find k-nearest neighbors for all points
63 std::vector<size_t> all_neighbors(frame::size(points) * params.k_neighbors);
64 std::vector<double> all_neighbor_sq_dists(frame::size(points) * params.k_neighbors);
65#pragma omp parallel for num_threads(params.num_threads) schedule(guided, 4)
66 for (size_t i = 0; i < frame::size(points); i++) {
67 size_t* k_neighbors = all_neighbors.data() + i * params.k_neighbors;
68 double* k_sq_dists = all_neighbor_sq_dists.data() + i * params.k_neighbors;
69 const size_t num_found = search.knn_search(frame::point(points, i).data(), params.k_neighbors, k_neighbors, k_sq_dists);
70
71 if (num_found != params.k_neighbors) {
72 std::cerr << "warning : Not enough neighbors. num_found=" << num_found << " k_neighbors=" << params.k_neighbors << std::endl;
73 std::fill(k_neighbors + num_found, k_neighbors + params.k_neighbors, INVALID_INDEX);
74 }
75 }
76
77 // clang-format off
78 using Traits = boost::adjacency_list_traits<boost::vecS, boost::vecS, boost::directedS>;
79 using Graph = boost::adjacency_list<boost::vecS, boost::vecS, boost::directedS,
80 boost::property<boost::vertex_index_t, long,//
81 boost::property<boost::vertex_color_t, boost::default_color_type,//
82 boost::property<boost::vertex_distance_t, long,//
83 boost::property<boost::vertex_predecessor_t, Traits::edge_descriptor>>>>, //
84 boost::property<boost::edge_capacity_t, double,//
85 boost::property<boost::edge_residual_capacity_t, double,//
86 boost::property<boost::edge_reverse_t, Traits::edge_descriptor>>>>;
87 using EdgeDescriptor = Traits::edge_descriptor;
88 // clang-format on
89
90 // Graph construction
91 Graph g(frame::size(points));
92 boost::property_map<Graph, boost::edge_capacity_t>::type capacity = get(boost::edge_capacity, g);
93 boost::property_map<Graph, boost::edge_residual_capacity_t>::type residual_capacity = get(boost::edge_residual_capacity, g);
94 boost::property_map<Graph, boost::edge_reverse_t>::type reverse_edges = get(boost::edge_reverse, g);
95 boost::property_map<Graph, boost::vertex_color_t>::type color = get(boost::vertex_color, g);
96
97 const auto add_edge = [&](size_t source, size_t target, double weight) {
98 const auto edge = boost::add_edge(source, target, g);
99 const auto reverse_edge = boost::add_edge(target, source, g);
100 if (!edge.second || !reverse_edge.second) {
101 return;
102 }
103
104 capacity[edge.first] = weight;
105 capacity[reverse_edge.first] = 0.0;
106 reverse_edges[edge.first] = reverse_edge.first;
107 reverse_edges[reverse_edge.first] = edge.first;
108 };
109
110 const double inv_dist_sq_sigma = 1.0 / (params.distance_sigma * params.distance_sigma);
111 const double inv_angle_sq_sigma = 1.0 / (params.angle_sigma * params.angle_sigma);
112
113 std::vector<size_t> sink_pt_indices;
114 for (size_t i = 0; i < frame::size(points); i++) {
115 const size_t* neighbors = all_neighbors.data() + i * params.k_neighbors;
116 const double* neighbor_sq_dists = all_neighbor_sq_dists.data() + i * params.k_neighbors;
117
118 const Eigen::Vector4d& normal = frame::normal(points, i);
119
120 const double dist_from_source = (frame::point(points, i) - frame::point(points, source_pt_index)).norm();
121
122 // Mark as foreground if the point is within the foreground mask radius
123 if (i != source_pt_index && dist_from_source < params.foreground_mask_radius) {
124 add_edge(i, source_pt_index, params.foreground_weight);
125 }
126 // Mark as background if the point is outside the background mask radius
127 if (dist_from_source > params.background_mask_radius) {
128 sink_pt_indices.emplace_back(i);
129 }
130
131 // Add connectivity edges to the neighbors
132 for (size_t k = 0; k < params.k_neighbors; k++) {
133 if (neighbors[k] == INVALID_INDEX) {
134 continue; // Skip invalid neighbors
135 }
136
137 const double weight_dist = std::exp(-neighbor_sq_dists[k] * inv_dist_sq_sigma);
138 const double normal_diff = std::acos(std::abs(normal.dot(frame::normal(points, neighbors[k]))));
139 const double weight_angle = std::exp(-normal_diff * normal_diff * inv_angle_sq_sigma);
140 add_edge(i, neighbors[k], weight_dist * weight_angle);
141 }
142 }
143
144 if (sink_pt_indices.empty()) {
145 std::cerr << "No sink points" << std::endl;
146 return MinCutResult();
147 }
148
149 size_t sink_pt_index = sink_pt_indices.front();
150 for (size_t i = 1; i < sink_pt_indices.size(); i++) {
151 add_edge(sink_pt_indices[i], sink_pt_index, params.background_weight);
152 }
153
154 // Run the min-cut algorithm
155 MinCutResult result;
156 result.source_index = source_pt_index;
157 result.sink_index = sink_pt_index;
158 result.max_flow = boost::boykov_kolmogorov_max_flow(g, source_pt_index, sink_pt_index);
159
160 // Extract the cluster
161 const auto source_color = color[source_pt_index];
162 for (size_t i = 0; i < frame::size(points); i++) {
163 if (color[i] == source_color) {
164 result.cluster_indices.emplace_back(i);
165 }
166 }
167
168 return result;
169}
170
171template <typename PointCloud>
172MinCutResult min_cut_(const PointCloud& points, const NearestNeighborSearch& search, const Eigen::Vector4d& source_pt, const MinCutParams& params) {
173 size_t source_pt_index;
174 double sq_dist;
175 if (!search.knn_search(source_pt.data(), 1, &source_pt_index, &sq_dist)) {
176 std::cerr << "Failed to find the source point" << std::endl;
177 return MinCutResult();
178 }
179
180 if (sq_dist > params.foreground_mask_radius * params.foreground_mask_radius) {
181 std::cerr << "warning: The source point is too far from the point cloud" << std::endl;
182 }
183
184 return min_cut_(points, search, source_pt_index, params);
185}
186
187} // namespace gtsam_points