ed_sensor_integration
src/kinect/segmenter.cpp
Go to the documentation of this file.
1 #include "ed/kinect/segmenter.h"
2 
4 #include <geolib/Shape.h>
5 
6 #include <rgbd/image.h>
7 #include <rgbd/view.h>
8 
9 #include <ed/world_model.h>
10 #include <ed/entity.h>
11 
12 #include <ros/console.h>
13 
14 // Clustering
15 #include <queue>
16 #include <ed/convex_hull_calc.h>
17 #include <sys/resource.h>
18 
19 #include <opencv2/ml.hpp>
20 #include <pcl/point_types.h>
21 #include <pcl/point_cloud.h>
22 #include <pcl/segmentation/extract_clusters.h>
23 
24 #include <Eigen/Dense>
25 
27 #include <bmm/bayesian_mixture_model.hpp>
28 #include <chrono>
29 
30 // ----------------------------------------------------------------------------------------------------
31 
33  : config_(config)
34 {
35  if (config_.readArray("surface_label_map"))
36  {
37  while (config_.nextArrayItem())
38  {
39  std::string entity_name, yolo_label;
40  if (config_.value("entity", entity_name) && config_.value("yolo_label", yolo_label))
41  surface_label_map_[entity_name] = yolo_label;
42  }
43  config_.endArray();
44  }
45 
46  ROS_INFO("Starting async initialization of segmentation pipeline (YOLO & SAM)...");
47  // Note: Atomic flag does not prevent 2 threads from initializing the pipeline simultaneously.
48  // This is not needed as the pipeline should only be initialized once.
49  init_thread_ = std::thread([this, config]() mutable {
50  this->sam_pipeline_.initialize(config);
51  ROS_INFO("Async initialization of segmentation pipeline is complete!");
52  });
53 }
54 
55 // ----------------------------------------------------------------------------------------------------
56 
58 {
59  if (init_thread_.joinable())
60  {
62  }
63 }
64 
65 // ----------------------------------------------------------------------------------------------------
66 
67 namespace
68 {
69 // Internal constants (tuning thresholds)
70 constexpr std::size_t MIN_FILTERED_POINTS = 10;
71 constexpr double MIN_RETENTION_RATIO = 0.10; // 10%
72 constexpr std::size_t MIN_CLUSTER_POINTS = 100;
73 
74 class DepthRenderer : public geo::RenderResult
75 {
76 
77 public:
78 
79  DepthRenderer(cv::Mat& z_buffer_)
80  : geo::RenderResult(z_buffer_.cols, z_buffer_.rows), z_buffer(z_buffer_)
81  {
82  }
83 
84  void renderPixel(int x, int y, float depth, int /*i_triangle*/)
85  {
86  float& old_depth = z_buffer.at<float>(y, x);
87  if (old_depth == 0 || depth < old_depth)
88  {
89  old_depth = depth;
90  }
91  }
92 
93  cv::Mat& z_buffer;
94 };
95 
96 }
97 
98 // ----------------------------------------------------------------------------------------------------
99 
100 void Segmenter::removeBackground(cv::Mat& depth_image, const ed::WorldModel& world, const geo::DepthCamera& cam,
101  const geo::Pose3D& sensor_pose, double background_padding)
102 {
103  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
104  // Render the world model as seen by the depth sensor
105 
106  cv::Mat depth_model(depth_image.rows, depth_image.cols, CV_32FC1, 0.0);
107 
108  DepthRenderer res(depth_model);
109  for(ed::WorldModel::const_iterator it = world.begin(); it != world.end(); ++it)
110  {
111  const ed::EntityConstPtr& e = *it;
112  if (!e->visual() || !e->has_pose())
113  continue;
114 
115  geo::RenderOptions opt;
116  opt.setMesh(e->visual()->getMesh(), sensor_pose.inverse() * e->pose());
117  cam.render(opt, res);
118  }
119 
120  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
121  // Filter all points that can be associated with the rendered depth image
122 
123  unsigned int size = depth_image.rows * depth_image.cols;
124  for(unsigned int i = 0; i < size; ++i)
125  {
126  float& ds = depth_image.at<float>(i);
127  float dm = depth_model.at<float>(i);
128  if (dm > 0 && ds > 0 && ds > dm - background_padding)
129  ds = 0;
130  }
131 }
132 
133 // ----------------------------------------------------------------------------------------------------
134 
136 {
137 
138 public:
139 
140  MinMaxRenderer(int width, int height) : geo::RenderResult(width, height)
141  {
142  min_buffer = cv::Mat(height, width, CV_32FC1, 0.0);
143  max_buffer = cv::Mat(height, width, CV_32FC1, 0.0);
144  }
145 
146  void renderPixel(int x, int y, float depth, int /*i_triangle*/)
147  {
148  // TODO: now the renderer can only deal with convex meshes, which means
149  // that at each pixel there can only be one minimum and one maximum pixel
150  // There is an easy solution for concave meshes: determine which side
151  // the triangle points (away or to the camera) and test the pixel in the depth
152  // image to be on the correct side. ... etc ...
153 
154  float& d_min = min_buffer.at<float>(y, x);
155  float& d_max = max_buffer.at<float>(y, x);
156 
157  if (d_min == 0 || depth < d_min)
158  d_min = depth;
159 
160  d_max = std::max(d_max, depth);
161  }
162 
163  cv::Mat min_buffer;
164  cv::Mat max_buffer;
165 
166 };
167 
168 // ----------------------------------------------------------------------------------------------------
169 
171  const geo::Pose3D& shape_pose, cv::Mat& filtered_depth_image) const
172 {
173  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
174  // Render shape
175 
176  const cv::Mat& depth_image = image.getDepthImage();
177 
178  rgbd::View view(image, depth_image.cols);
179  const geo::DepthCamera& cam_model = view.getRasterizer();
180 
181  MinMaxRenderer res(depth_image.cols, depth_image.rows);
182 
183  geo::RenderOptions opt;
184  opt.setBackFaceCulling(false);
185  opt.setMesh(shape.getMesh(), shape_pose);
186 
187  cam_model.render(opt, res);
188 
189  filtered_depth_image = cv::Mat(depth_image.rows, depth_image.cols, CV_32FC1, 0.0);
190 
191  for(int i = 0; i < depth_image.cols * depth_image.rows; ++i)
192  {
193  float d = depth_image.at<float>(i);
194  if (d <= 0)
195  continue;
196 
197  float d_min = res.min_buffer.at<float>(i);
198  float d_max = res.max_buffer.at<float>(i);
199 
200  if (d_min > 0 && d_max > 0 && d >= d_min && d <= d_max)
201  filtered_depth_image.at<float>(i) = d;
202  }
203 }
204 
205 // ----------------------------------------------------------------------------------------------------
206 
207 cv::Mat Segmenter::preprocessRGBForSegmentation(const cv::Mat& rgb_image,
208  const cv::Mat& filtered_depth_image) const
209 {
210  cv::Mat masked_rgb = cv::Mat::zeros(rgb_image.size(), rgb_image.type());
211  for (int y = 0; y < rgb_image.rows; ++y) {
212  const float* depth_row = filtered_depth_image.ptr<float>(y); // fast
213  cv::Vec3b* out_row = masked_rgb.ptr<cv::Vec3b>(y);
214  const cv::Vec3b* rgb_row = rgb_image.ptr<cv::Vec3b>(y);
215  for (int x = 0; x < rgb_image.cols; ++x) {
216  if (depth_row[x] > 0.f) {
217  out_row[x] = rgb_row[x];
218  }
219  }
220  }
221  return masked_rgb;
222 }
223 // ----------------------------------------------------------------------------------------------------
224 
225 SegmentationResult Segmenter::cluster(const cv::Mat& depth_image, const geo::DepthCamera& cam_model,
226  const geo::Pose3D& sensor_pose, std::vector<EntityUpdate>& clusters, const cv::Mat& rgb_image,
227  const std::string& area_description, bool verbose)
228 {
229  int width = depth_image.cols;
230  int height = depth_image.rows;
231  ROS_DEBUG("Cluster with depth image of size %i, %i", width, height);
232 
233  // Extract area name and entity from area_description (e.g. "on_top_of" and "dinner_table" from "on_top_of dinner_table")
234  std::string area_name;
235  std::string area_entity;
236  std::string ignore_label = "";
237 
238  // this code block is an example for manually ignoring the supporting surface when looking for objects on top of it.
239 
240  //index of the space that separates the 2 words in the area description (e.g. "on_top_of dinner_table")
241  // const std::size_t i_space = area_description.find(' ');
242  // if (i_space != std::string::npos)
243  // {
244  // area_name = area_description.substr(0, i_space);
245  // area_entity = area_description.substr(i_space + 1);
246 
247  // if (area_name == "on_top_of")
248  // {
249  // const auto it = surface_label_map_.find(area_entity);
250  // if (it != surface_label_map_.end())
251  // {
252  // ignore_label = it->second;
253  // }
254  // }
255  // }
256 
257  // Pass verbose flag to measure latency in the segmentation pipeline
258  SegmentationResult seg_result = sam_pipeline_.process(rgb_image.clone(), depth_image, ignore_label, verbose);
259  std::vector<cv::Mat>& masks = seg_result.masks;
260 
261  ROS_DEBUG("Creating clusters");
262 
263  // Pre-allocate temporary storage (one per mask, avoid push_back races)
264  std::vector<EntityUpdate> temp_clusters(masks.size());
265  std::vector<bool> valid_cluster(masks.size(), false);
266 
267  // BMM point cloud denoising
268  GMMParams params;
269  if (config_.readGroup("bmm"))
270  {
271  config_.value("psi0", params.psi0, tue::config::OPTIONAL);
272  config_.value("nu0", params.nu0, tue::config::OPTIONAL);
273  config_.value("alpha", params.alpha, tue::config::OPTIONAL);
274  config_.value("kappa0", params.kappa0, tue::config::OPTIONAL);
275  config_.endGroup();
276  }
277  // BMM timing: per-mask latencies stored for aggregation (thread-safe via indexing)
278  std::vector<double> bmm_latencies_ms(masks.size(), 0.0);
279 
280  // Parallel loop - each iteration is independent
281  #pragma omp parallel for schedule(dynamic)
282  for (size_t i = 0; i < masks.size(); ++i)
283  {
284  //Resize mask if needed so for the image to be the same size as the depth image (in case SAM produces a different size mask)
285  const cv::Mat& mask_orig = masks[i];
286 
287  // Empty mask: SAM failed for this box (placeholder pushed to preserve alignment)
288  if (mask_orig.empty())
289  {
290  ROS_DEBUG("Skipping cluster %zu: SAM returned empty mask (segmentation failure for this box)", i);
291  continue;
292  }
293  // When looking on_top_of a surface, skip the supporting surface itself so it is
294  // never extracted as a cluster or passed through BMM. The entity-to-YOLO-label
295  // mapping is read from the "surface_label_map" config array.
296  if (area_name == "on_top_of" && i < seg_result.labels.size())
297  {
298  const auto it = surface_label_map_.find(area_entity);
299  if (it != surface_label_map_.end() && seg_result.labels[i] == it->second)
300  {
301  ROS_WARN("Skipping cluster %zu: label '%s' is the supporting surface for area '%s' and area description '%s'",
302  i, seg_result.labels[i].c_str(), area_name.c_str(), area_description.c_str());
303  continue;
304  }
305  }
306 
307  cv::Mat mask;
308  if (mask_orig.rows != height || mask_orig.cols != width)
309  cv::resize(mask_orig, mask, cv::Size(width, height), 0, 0, cv::INTER_NEAREST);
310  else
311  mask = mask_orig;
312 
313  EntityUpdate cluster; // local to this thread
314 
315  // Extract points from mask
316  for(int y = 0; y < mask.rows; ++y) {
317  for(int x = 0; x < mask.cols; ++x) {
318  if (mask.at<unsigned char>(y, x) > 0) {
319  unsigned int pixel_idx = y * width + x;
320  float d = depth_image.at<float>(pixel_idx);
321 
322  if (d > 0 && std::isfinite(d)) {
323  cluster.pixel_indices.push_back(pixel_idx);
324  cluster.points.push_back(cam_model.project2Dto3D(x, y) * d);
325  }
326  }
327  }
328  }
329 
330  // Skip small clusters (< 100 points based on MIN_CLUSTER_POINTS)
331  if (cluster.pixel_indices.size() < MIN_CLUSTER_POINTS) {
332  if (verbose)
333  {
334  const std::string label = (i < seg_result.labels.size()) ? seg_result.labels[i] : "?";
335  ROS_WARN("We reject cluster %zu with label '%s' because it has only %zu points", i, label.c_str(), cluster.pixel_indices.size());
336  }
337  continue; // valid_cluster[i] remains false
338  }
339 
340  if (verbose)
341  {
342  const std::string label = (i < seg_result.labels.size()) ? seg_result.labels[i] : "?";
343  ROS_WARN("Cluster %zu with label '%s': %zu points", i, label.c_str(), cluster.points.size());
344  }
345 
346  // BMM inference with timing
347  auto bmm_start = std::chrono::high_resolution_clock::now();
348 
349  MAPGMM gmm(2, cluster.points, params); // 2 components: object + outliers
350  gmm.fit(cluster.points, sensor_pose);
351 
353  bmm_latencies_ms[i] = std::chrono::duration<double, std::milli>(bmm_end - bmm_start).count();
354 
355  // Get component assignments and inlier component
356  std::vector<int> labels = gmm.get_labels();
357  int inlier_component = gmm.get_inlier_component();
358 
359  // Filter points
360  std::vector<geo::Vec3> filtered_points;
361  std::vector<geo::Vec3> outlier_points; // Only populate if needed
362 
363  for (size_t j = 0; j < labels.size(); j++)
364  {
365  if (labels[j] == inlier_component)
366  {
367  filtered_points.push_back(cluster.points[j]);
368  }
369  else if (verbose)
370  {
371  outlier_points.push_back(cluster.points[j]);
372  }
373  }
374 
375  // Safety check: only use filtered points if we retained enough
376  if (filtered_points.size() > MIN_FILTERED_POINTS &&
377  filtered_points.size() > MIN_RETENTION_RATIO * cluster.points.size()) {
378  // Use filtered points
379  cluster.points = filtered_points;
380  if (verbose)
381  {
382  cluster.outlier_points = outlier_points;
383  // Transform outlier points to map frame
384  // for (size_t j = 0; j < cluster.outlier_points.size(); ++j) {
385  // cluster.outlier_points[j] = sensor_pose * cluster.outlier_points[j];
386  // }
387  }
388  }
389  else
390  {
391  // Safety check failed - keep original unfiltered points
392  // Don't populate outlier_points since we're not using the GMM result
393  ROS_DEBUG("GMM filtering rejected: retained %zu/%zu points",
394  filtered_points.size(), cluster.points.size());
395  }
396 
397  // Calculate convex hull
398  float z_min = 1e9;
399  float z_max = -1e9;
400  std::vector<geo::Vec2f> points_2d(cluster.points.size());
401 
402  for(unsigned int j = 0; j < cluster.points.size(); ++j)
403  {
404  const geo::Vec3& p = cluster.points[j];
405 
406  // Transform sensor point to map frame
407  geo::Vector3 p_map = sensor_pose * p;
408  points_2d[j] = geo::Vec2f(p_map.x, p_map.y);
409  z_min = std::min<float>(z_min, p_map.z);
410  z_max = std::max<float>(z_max, p_map.z);
411  }
412 
413  ed::convex_hull::create(points_2d, z_min, z_max, cluster.chull, cluster.pose_map);
414  cluster.chull.complete = false;
415 
416  // Assign YOLO classification label to this cluster
417  if (i < seg_result.labels.size())
418  {
419  cluster.label = seg_result.labels[i];
420  cluster.classification_confidence = seg_result.confidences[i];
421  if (verbose)
422  {
423  ROS_INFO("Cluster %zu classified as '%s' with confidence %.2f", i, cluster.label.c_str(), cluster.classification_confidence);
424  }
425  }
426 
427  // Store in thread-safe pre-allocated array
428  temp_clusters[i] = cluster;
429  valid_cluster[i] = true;
430  }
431 
432  // Sequential section: collect valid clusters
433  clusters.clear();
434  clusters.reserve(masks.size());
435  for (size_t i = 0; i < temp_clusters.size(); ++i) {
436  if (valid_cluster[i]) {
437  clusters.push_back(std::move(temp_clusters[i]));
438  }
439  }
440 
441  // Log BMM latency statistics if verbose is enabled
442  if (verbose)
443  {
444  double bmm_total_ms = 0.0;
445  double bmm_max_ms = 0.0;
446  int bmm_count = 0;
447  for (size_t i = 0; i < bmm_latencies_ms.size(); ++i)
448  {
449  if (bmm_latencies_ms[i] > 0.0)
450  {
451  bmm_total_ms += bmm_latencies_ms[i];
452  bmm_max_ms = std::max(bmm_max_ms, bmm_latencies_ms[i]);
453  bmm_count++;
454  }
455  }
456  double bmm_avg_ms = (bmm_count > 0) ? (bmm_total_ms / bmm_count) : 0.0;
457 
458  ROS_WARN("\n=== BMM Latency (parallel execution) ===\n");
459  ROS_WARN(" BMM fits: %d masks\n", bmm_count);
460  ROS_WARN(" BMM avg: %7.2f ms/mask\n", bmm_avg_ms);
461  ROS_WARN(" BMM max: %7.2f ms (slowest mask)\n", bmm_max_ms);
462  ROS_WARN(" BMM total: %7.2f ms (sequential equivalent)\n", bmm_total_ms);
463  }
464 
465  return seg_result;
466 }
geo::Vector3::y
const real & y() const
MinMaxRenderer::MinMaxRenderer
MinMaxRenderer(int width, int height)
Definition: src/kinect/segmenter.cpp:140
SegmentationResult
Result of the segmentation pipeline containing masks, bounding boxes, and YOLO classification info fo...
Definition: sam_seg_module.h:34
SegmentationResult::labels
std::vector< std::string > labels
Definition: sam_seg_module.h:38
std::string
EntityUpdate
collection structure for laser entities
Definition: kinect/entity_update.h:10
Segmenter::preprocessRGBForSegmentation
cv::Mat preprocessRGBForSegmentation(const cv::Mat &rgb_image, const cv::Mat &filtered_depth_image) const
Preprocess RGB image for segmentation. This function masks the RGB image with the filtered depth imag...
Definition: src/kinect/segmenter.cpp:207
DepthCamera.h
geo::RenderOptions::setMesh
void setMesh(const geo::Mesh &mesh, const geo::Pose3D &pose=geo::Pose3D::identity())
geo
sam_seg_module.h
ed::convex_hull::create
void create(const std::vector< geo::Vec2f > &points, float z_min, float z_max, ConvexHull &chull, geo::Pose3D &pose)
std::vector::reserve
T reserve(T... args)
Segmenter::config_
tue::Configuration config_
Definition: segmenter.h:78
Segmenter::removeBackground
void removeBackground(cv::Mat &depth_image, const ed::WorldModel &world, const geo::DepthCamera &cam, const geo::Pose3D &sensor_pose, double background_padding)
Definition: src/kinect/segmenter.cpp:100
std::vector< EntityUpdate >
std::unordered_map::find
T find(T... args)
std::vector::size
T size(T... args)
geo::RenderOptions::setBackFaceCulling
void setBackFaceCulling(bool b)
std::chrono::duration
rgb_image
cv::Mat rgb_image
cam
geo::DepthCamera cam
geo::Vec3T
Shape.h
geo::DepthCamera::render
void render(const RenderOptions &opt, RenderResult &res) const
MinMaxRenderer::max_buffer
cv::Mat max_buffer
Definition: src/kinect/segmenter.cpp:164
geo::Transform3T
Segmenter::cluster
SegmentationResult cluster(const cv::Mat &depth_image, const geo::DepthCamera &cam_model, const geo::Pose3D &sensor_pose, std::vector< EntityUpdate > &clusters, const cv::Mat &rgb_image, const std::string &area_description="", bool verbose=false)
Cluster the depth image into segments. Applies new algorithm which is the YOLO - SAM - BMM depth segm...
Definition: src/kinect/segmenter.cpp:225
geo::Shape::getMesh
virtual const Mesh & getMesh() const
queue
geo::Vec2f
Vec2T< float > Vec2f
ed::WorldModel::begin
const_iterator begin() const
ed::WorldModel::const_iterator
EntityIterator const_iterator
SamSegPipeline::initialize
void initialize(tue::Configuration &config)
Initialize the YOLO and SAM models from configuration.
Definition: sam_seg_module.cpp:31
std::vector::clear
T clear(T... args)
ed::EntityConstPtr
shared_ptr< const Entity > EntityConstPtr
Segmenter::surface_label_map_
std::unordered_map< std::string, std::string > surface_label_map_
Definition: segmenter.h:81
ed::WorldModel::end
const_iterator end() const
tue::config::ReaderWriter::readArray
bool readArray(const std::string &name, const RequiredOrOptional opt=OPTIONAL)
std::vector::push_back
T push_back(T... args)
image
cv::Mat image
tue::config::ReaderWriter
segmenter.h
geo::Transform3T::inverse
Transform3T inverse() const
SegmentationResult::masks
std::vector< cv::Mat > masks
Definition: sam_seg_module.h:36
std::thread::joinable
T joinable(T... args)
rgbd::Image
rgbd::View::getRasterizer
const geo::DepthCamera & getRasterizer() const
std::isfinite
T isfinite(T... args)
image.h
geo::RenderResult
std::thread
geo::RenderOptions
chrono
Segmenter::calculatePointsWithin
void calculatePointsWithin(const rgbd::Image &image, const geo::Shape &shape, const geo::Pose3D &shape_pose, cv::Mat &filtered_depth_image) const
Definition: src/kinect/segmenter.cpp:170
SegmentationResult::confidences
std::vector< float > confidences
Definition: sam_seg_module.h:39
std::string::c_str
T c_str(T... args)
tue::config::ReaderWriter::endGroup
bool endGroup()
tue::config::OPTIONAL
OPTIONAL
geo::Vector3
geo::DepthCamera::project2Dto3D
geo::Vec3T< T > project2Dto3D(int x, int y) const
ed::WorldModel
geo::Vector3::z
const real & z() const
Segmenter::~Segmenter
~Segmenter()
Definition: src/kinect/segmenter.cpp:57
tue::config::ReaderWriter::endArray
bool endArray()
Segmenter::Segmenter
Segmenter(tue::Configuration config)
Definition: src/kinect/segmenter.cpp:32
view.h
tue::config::ReaderWriter::value
bool value(const std::string &name, T &value, RequiredOrOptional opt=REQUIRED)
world_model.h
MinMaxRenderer
Definition: src/kinect/segmenter.cpp:135
Segmenter::init_thread_
std::thread init_thread_
Definition: segmenter.h:83
MinMaxRenderer::min_buffer
cv::Mat min_buffer
Definition: src/kinect/segmenter.cpp:163
Segmenter::sam_pipeline_
SamSegPipeline sam_pipeline_
Definition: segmenter.h:82
SamSegPipeline::process
SegmentationResult process(const cv::Mat &img, const cv::Mat &depth_image=cv::Mat(), const std::string &ignore_label="", bool verbose=false)
Process the input image and generate segmentation masks.
Definition: sam_seg_module.cpp:71
tue::config::ReaderWriter::readGroup
bool readGroup(const std::string &name, const RequiredOrOptional opt=OPTIONAL)
geo::Vector3::x
const real & x() const
std::chrono::duration::count
T count(T... args)
geo::DepthCamera
std::size_t
std::unordered_map::end
T end(T... args)
geo::RenderResult::RenderResult
RenderResult(int width, int height)
std::max
T max(T... args)
entity.h
MinMaxRenderer::renderPixel
void renderPixel(int x, int y, float depth, int)
Definition: src/kinect/segmenter.cpp:146
convex_hull_calc.h
depth_image
cv::Mat depth_image
rgbd::View
tue::config::ReaderWriter::nextArrayItem
bool nextArrayItem()
std::thread::join
T join(T... args)
geo::Shape
config
tue::config::ReaderWriter config
geo::RenderResult::renderPixel
virtual void renderPixel(int x, int y, float depth, int i_triangle)=0
std::chrono::high_resolution_clock::now
T now(T... args)