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 
29 // ----------------------------------------------------------------------------------------------------
30 
32  : config_(config)
33 {
34  if (config_.readArray("surface_label_map"))
35  {
36  while (config_.nextArrayItem())
37  {
38  std::string entity_name, yolo_label;
39  if (config_.value("entity", entity_name) && config_.value("yolo_label", yolo_label))
40  surface_label_map_[entity_name] = yolo_label;
41  }
42  config_.endArray();
43  }
44 }
45 
46 // ----------------------------------------------------------------------------------------------------
47 
49 {
50 }
51 
52 // ----------------------------------------------------------------------------------------------------
53 
54 namespace
55 {
56 // Internal constants (tuning thresholds)
57 constexpr std::size_t MIN_FILTERED_POINTS = 10;
58 constexpr double MIN_RETENTION_RATIO = 0.10; // 10%
59 constexpr std::size_t MIN_CLUSTER_POINTS = 100;
60 
61 class DepthRenderer : public geo::RenderResult
62 {
63 
64 public:
65 
66  DepthRenderer(cv::Mat& z_buffer_)
67  : geo::RenderResult(z_buffer_.cols, z_buffer_.rows), z_buffer(z_buffer_)
68  {
69  }
70 
71  void renderPixel(int x, int y, float depth, int /*i_triangle*/)
72  {
73  float& old_depth = z_buffer.at<float>(y, x);
74  if (old_depth == 0 || depth < old_depth)
75  {
76  old_depth = depth;
77  }
78  }
79 
80  cv::Mat& z_buffer;
81 };
82 
83 }
84 
85 // ----------------------------------------------------------------------------------------------------
86 
87 void Segmenter::removeBackground(cv::Mat& depth_image, const ed::WorldModel& world, const geo::DepthCamera& cam,
88  const geo::Pose3D& sensor_pose, double background_padding)
89 {
90  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
91  // Render the world model as seen by the depth sensor
92 
93  cv::Mat depth_model(depth_image.rows, depth_image.cols, CV_32FC1, 0.0);
94 
95  DepthRenderer res(depth_model);
96  for(ed::WorldModel::const_iterator it = world.begin(); it != world.end(); ++it)
97  {
98  const ed::EntityConstPtr& e = *it;
99  if (!e->visual() || !e->has_pose())
100  continue;
101 
102  geo::RenderOptions opt;
103  opt.setMesh(e->visual()->getMesh(), sensor_pose.inverse() * e->pose());
104  cam.render(opt, res);
105  }
106 
107  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
108  // Filter all points that can be associated with the rendered depth image
109 
110  unsigned int size = depth_image.rows * depth_image.cols;
111  for(unsigned int i = 0; i < size; ++i)
112  {
113  float& ds = depth_image.at<float>(i);
114  float dm = depth_model.at<float>(i);
115  if (dm > 0 && ds > 0 && ds > dm - background_padding)
116  ds = 0;
117  }
118 }
119 
120 // ----------------------------------------------------------------------------------------------------
121 
123 {
124 
125 public:
126 
127  MinMaxRenderer(int width, int height) : geo::RenderResult(width, height)
128  {
129  min_buffer = cv::Mat(height, width, CV_32FC1, 0.0);
130  max_buffer = cv::Mat(height, width, CV_32FC1, 0.0);
131  }
132 
133  void renderPixel(int x, int y, float depth, int /*i_triangle*/)
134  {
135  // TODO: now the renderer can only deal with convex meshes, which means
136  // that at each pixel there can only be one minimum and one maximum pixel
137  // There is an easy solution for concave meshes: determine which side
138  // the triangle points (away or to the camera) and test the pixel in the depth
139  // image to be on the correct side. ... etc ...
140 
141  float& d_min = min_buffer.at<float>(y, x);
142  float& d_max = max_buffer.at<float>(y, x);
143 
144  if (d_min == 0 || depth < d_min)
145  d_min = depth;
146 
147  d_max = std::max(d_max, depth);
148  }
149 
150  cv::Mat min_buffer;
151  cv::Mat max_buffer;
152 
153 };
154 
155 // ----------------------------------------------------------------------------------------------------
156 
158  const geo::Pose3D& shape_pose, cv::Mat& filtered_depth_image) const
159 {
160  // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
161  // Render shape
162 
163  const cv::Mat& depth_image = image.getDepthImage();
164 
165  rgbd::View view(image, depth_image.cols);
166  const geo::DepthCamera& cam_model = view.getRasterizer();
167 
168  MinMaxRenderer res(depth_image.cols, depth_image.rows);
169 
170  geo::RenderOptions opt;
171  opt.setBackFaceCulling(false);
172  opt.setMesh(shape.getMesh(), shape_pose);
173 
174  cam_model.render(opt, res);
175 
176  filtered_depth_image = cv::Mat(depth_image.rows, depth_image.cols, CV_32FC1, 0.0);
177 
178  for(int i = 0; i < depth_image.cols * depth_image.rows; ++i)
179  {
180  float d = depth_image.at<float>(i);
181  if (d <= 0)
182  continue;
183 
184  float d_min = res.min_buffer.at<float>(i);
185  float d_max = res.max_buffer.at<float>(i);
186 
187  if (d_min > 0 && d_max > 0 && d >= d_min && d <= d_max)
188  filtered_depth_image.at<float>(i) = d;
189  }
190 }
191 
192 // ----------------------------------------------------------------------------------------------------
193 
194 cv::Mat Segmenter::preprocessRGBForSegmentation(const cv::Mat& rgb_image,
195  const cv::Mat& filtered_depth_image) const
196 {
197  cv::Mat masked_rgb = cv::Mat::zeros(rgb_image.size(), rgb_image.type());
198  for (int y = 0; y < rgb_image.rows; ++y) {
199  const float* depth_row = filtered_depth_image.ptr<float>(y); // fast
200  cv::Vec3b* out_row = masked_rgb.ptr<cv::Vec3b>(y);
201  const cv::Vec3b* rgb_row = rgb_image.ptr<cv::Vec3b>(y);
202  for (int x = 0; x < rgb_image.cols; ++x) {
203  if (depth_row[x] > 0.f) {
204  out_row[x] = rgb_row[x];
205  }
206  }
207  }
208  return masked_rgb;
209 }
210 // ----------------------------------------------------------------------------------------------------
211 
212 SegmentationResult Segmenter::cluster(const cv::Mat& depth_image, const geo::DepthCamera& cam_model,
213  const geo::Pose3D& sensor_pose, std::vector<EntityUpdate>& clusters, const cv::Mat& rgb_image,
214  const std::string& area_description, bool verbose)
215 {
216  int width = depth_image.cols;
217  int height = depth_image.rows;
218  ROS_DEBUG("Cluster with depth image of size %i, %i", width, height);
219 
221  std::vector<cv::Mat>& masks = seg_result.masks;
222 
223  // Extract area name and entity from area_description (e.g. "on_top_of" and "dinner_table" from "on_top_of dinner_table")
224  std::string area_name;
225  std::string area_entity;
226  const std::size_t i_space = area_description.find(' ');
227  if (i_space != std::string::npos)
228  {
229  area_name = area_description.substr(0, i_space);
230  area_entity = area_description.substr(i_space + 1);
231  }
232 
233  ROS_DEBUG("Creating clusters");
234 
235  // Pre-allocate temporary storage (one per mask, avoid push_back races)
236  std::vector<EntityUpdate> temp_clusters(masks.size());
237  std::vector<bool> valid_cluster(masks.size(), false);
238 
239  // BMM point cloud denoising
240  GMMParams params;
241  if (config_.readGroup("bmm"))
242  {
243  config_.value("psi0", params.psi0, tue::config::OPTIONAL);
244  config_.value("nu0", params.nu0, tue::config::OPTIONAL);
245  config_.value("alpha", params.alpha, tue::config::OPTIONAL);
246  config_.value("kappa0", params.kappa0, tue::config::OPTIONAL);
247  config_.endGroup();
248  }
249  // BMM timing: per-mask latencies stored for aggregation (thread-safe via indexing)
250  std::vector<double> bmm_latencies_ms(masks.size(), 0.0);
251 
252  // Parallel loop - each iteration is independent
253  #pragma omp parallel for schedule(dynamic)
254  for (size_t i = 0; i < masks.size(); ++i)
255  {
256  //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)
257  const cv::Mat& mask_orig = masks[i];
258 
259  // Empty mask: SAM failed for this box (placeholder pushed to preserve alignment)
260  if (mask_orig.empty())
261  {
262  ROS_DEBUG("Skipping cluster %zu: SAM returned empty mask (segmentation failure for this box)", i);
263  continue;
264  }
265  // When looking on_top_of a surface, skip the supporting surface itself so it is
266  // never extracted as a cluster or passed through BMM. The entity-to-YOLO-label
267  // mapping is read from the "surface_label_map" config array.
268  if (area_name == "on_top_of" && i < seg_result.labels.size())
269  {
270  const auto it = surface_label_map_.find(area_entity);
271  if (it != surface_label_map_.end() && seg_result.labels[i] == it->second)
272  {
273  ROS_WARN("Skipping cluster %zu: label '%s' is the supporting surface for area '%s' and area description '%s'",
274  i, seg_result.labels[i].c_str(), area_name.c_str(), area_description.c_str());
275  continue;
276  }
277  }
278 
279  cv::Mat mask;
280  if (mask_orig.rows != height || mask_orig.cols != width)
281  cv::resize(mask_orig, mask, cv::Size(width, height), 0, 0, cv::INTER_NEAREST);
282  else
283  mask = mask_orig;
284 
285  EntityUpdate cluster; // local to this thread
286 
287  // Extract points from mask
288  for(int y = 0; y < mask.rows; ++y) {
289  for(int x = 0; x < mask.cols; ++x) {
290  if (mask.at<unsigned char>(y, x) > 0) {
291  unsigned int pixel_idx = y * width + x;
292  float d = depth_image.at<float>(pixel_idx);
293 
294  if (d > 0 && std::isfinite(d)) {
295  cluster.pixel_indices.push_back(pixel_idx);
296  cluster.points.push_back(cam_model.project2Dto3D(x, y) * d);
297  }
298  }
299  }
300  }
301 
302  // Skip small clusters (< 100 points based on MIN_CLUSTER_POINTS)
303  if (cluster.pixel_indices.size() < MIN_CLUSTER_POINTS) {
304  if (verbose)
305  {
306  const std::string label = (i < seg_result.labels.size()) ? seg_result.labels[i] : "?";
307  ROS_WARN("We reject cluster %zu with label '%s' because it has only %zu points", i, label.c_str(), cluster.pixel_indices.size());
308  }
309  continue; // valid_cluster[i] remains false
310  }
311 
312  if (verbose)
313  {
314  const std::string label = (i < seg_result.labels.size()) ? seg_result.labels[i] : "?";
315  ROS_WARN("Cluster %zu with label '%s': %zu points", i, label.c_str(), cluster.points.size());
316  }
317 
318 
319  MAPGMM gmm(2, cluster.points, params); // 2 components: object + outliers
320  gmm.fit(cluster.points, sensor_pose);
321  // Get component assignments and inlier component
322  std::vector<int> labels = gmm.get_labels();
323  int inlier_component = gmm.get_inlier_component();
324 
325  // Filter points
326  std::vector<geo::Vec3> filtered_points;
327  std::vector<geo::Vec3> outlier_points; // Only populate if needed
328 
329  for (size_t j = 0; j < labels.size(); j++)
330  {
331  if (labels[j] == inlier_component)
332  {
333  filtered_points.push_back(cluster.points[j]);
334  }
335  else if (verbose)
336  {
337  outlier_points.push_back(cluster.points[j]);
338  }
339  }
340 
341  // Safety check: only use filtered points if we retained enough
342  if (filtered_points.size() > MIN_FILTERED_POINTS &&
343  filtered_points.size() > MIN_RETENTION_RATIO * cluster.points.size()) {
344  // Use filtered points
345  cluster.points = filtered_points;
346  if (verbose)
347  {
348  cluster.outlier_points = outlier_points;
349  // Transform outlier points to map frame
350  // for (size_t j = 0; j < cluster.outlier_points.size(); ++j) {
351  // cluster.outlier_points[j] = sensor_pose * cluster.outlier_points[j];
352  // }
353  }
354  }
355  else
356  {
357  // Safety check failed - keep original unfiltered points
358  // Don't populate outlier_points since we're not using the GMM result
359  ROS_DEBUG("GMM filtering rejected: retained %zu/%zu points",
360  filtered_points.size(), cluster.points.size());
361  }
362 
363  // Calculate convex hull
364  float z_min = 1e9;
365  float z_max = -1e9;
366  std::vector<geo::Vec2f> points_2d(cluster.points.size());
367 
368  for(unsigned int j = 0; j < cluster.points.size(); ++j)
369  {
370  const geo::Vec3& p = cluster.points[j];
371 
372  // Transform sensor point to map frame
373  geo::Vector3 p_map = sensor_pose * p;
374  points_2d[j] = geo::Vec2f(p_map.x, p_map.y);
375  z_min = std::min<float>(z_min, p_map.z);
376  z_max = std::max<float>(z_max, p_map.z);
377  }
378 
379  ed::convex_hull::create(points_2d, z_min, z_max, cluster.chull, cluster.pose_map);
380  cluster.chull.complete = false;
381 
382  // Assign YOLO classification label to this cluster
383  if (i < seg_result.labels.size())
384  {
385  cluster.label = seg_result.labels[i];
386  cluster.classification_confidence = seg_result.confidences[i];
387  if (verbose)
388  {
389  ROS_INFO("Cluster %zu classified as '%s' with confidence %.2f", i, cluster.label.c_str(), cluster.classification_confidence);
390  }
391  }
392 
393  // Store in thread-safe pre-allocated array
394  temp_clusters[i] = cluster;
395  valid_cluster[i] = true;
396  }
397 
398  // Sequential section: collect valid clusters
399  clusters.clear();
400  clusters.reserve(masks.size());
401  for (size_t i = 0; i < temp_clusters.size(); ++i) {
402  if (valid_cluster[i]) {
403  clusters.push_back(std::move(temp_clusters[i]));
404  }
405  }
406 
407  return seg_result;
408 }
geo::Vector3::y
const real & y() const
MinMaxRenderer::MinMaxRenderer
MinMaxRenderer(int width, int height)
Definition: src/kinect/segmenter.cpp:127
SegmentationResult
Result of the segmentation pipeline containing masks, bounding boxes, and YOLO classification info fo...
Definition: sam_seg_module.h:19
SegmentationResult::labels
std::vector< std::string > labels
Definition: sam_seg_module.h:23
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:194
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:77
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:87
std::vector< EntityUpdate >
std::string::find
T find(T... args)
std::vector::size
T size(T... args)
geo::RenderOptions::setBackFaceCulling
void setBackFaceCulling(bool b)
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:151
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:212
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
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:80
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:21
rgbd::Image
rgbd::View::getRasterizer
const geo::DepthCamera & getRasterizer() const
std::isfinite
T isfinite(T... args)
image.h
geo::RenderResult
geo::RenderOptions
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:157
SegmentationResult::confidences
std::vector< float > confidences
Definition: sam_seg_module.h:24
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:48
tue::config::ReaderWriter::endArray
bool endArray()
Segmenter::Segmenter
Segmenter(tue::Configuration config)
Definition: src/kinect/segmenter.cpp:31
std::string::substr
T substr(T... args)
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:122
MinMaxRenderer::min_buffer
cv::Mat min_buffer
Definition: src/kinect/segmenter.cpp:150
tue::config::ReaderWriter::readGroup
bool readGroup(const std::string &name, const RequiredOrOptional opt=OPTIONAL)
geo::Vector3::x
const real & x() const
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)
SegmentationPipeline
SegmentationResult SegmentationPipeline(const cv::Mat &img, tue::Configuration &config)
Segmentation pipeline that processes the input image and generates segmentation masks.
Definition: sam_seg_module.cpp:11
entity.h
MinMaxRenderer::renderPixel
void renderPixel(int x, int y, float depth, int)
Definition: src/kinect/segmenter.cpp:133
convex_hull_calc.h
depth_image
cv::Mat depth_image
rgbd::View
tue::config::ReaderWriter::nextArrayItem
bool nextArrayItem()
geo::Shape
config
tue::config::ReaderWriter config
geo::RenderResult::renderPixel
virtual void renderPixel(int x, int y, float depth, int i_triangle)=0