ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
PickPointsInteractor.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
9 
10 #include <Eigen.h>
11 #include <Image.h>
12 #include <Logging.h>
13 #include <ecvMesh.h>
14 #include <ecvPointCloud.h>
15 
16 #include <unordered_map>
17 #include <unordered_set>
18 
19 #include "t/geometry/PointCloud.h"
26 
27 #define WANT_DEBUG_IMAGE 0
28 
29 #if WANT_DEBUG_IMAGE
30 #include <ImageIO.h>
31 #endif // WANT_DEBUG_IMAGE
32 
33 namespace cloudViewer {
34 namespace visualization {
35 namespace gui {
36 
37 namespace {
38 // Background color is white, so that index 0 can be black
39 static const Eigen::Vector4f kBackgroundColor = {1.0f, 1.0f, 1.0f, 1.0f};
40 static const std::string kSelectablePointsName = "__selectable_points";
41 // The maximum pickable point is one less than FFFFFF, because that would
42 // be white, which is the color of the background.
43 // static const unsigned int kNoIndex = 0x00ffffff; // unused, but real
44 static const unsigned int kMeshIndex = 0x00fffffe;
45 static const unsigned int kMaxPickableIndex = 0x00fffffd;
46 
47 inline bool IsValidIndex(uint32_t idx) { return (idx <= kMaxPickableIndex); }
48 
49 Eigen::Vector3d CalcIndexColor(uint32_t idx) {
50  const double red = double((idx & 0x00ff0000) >> 16) / 255.0;
51  const double green = double((idx & 0x0000ff00) >> 8) / 255.0;
52  const double blue = double((idx & 0x000000ff)) / 255.0;
53  return {red, green, blue};
54 }
55 
56 Eigen::Vector3d SetColorForIndex(uint32_t idx) {
57  return CalcIndexColor(std::min(kMaxPickableIndex, idx));
58 }
59 
60 uint32_t GetIndexForColor(geometry::Image *image, int x, int y) {
61  uint8_t *rgb = image->PointerAt<uint8_t>(x, y, 0);
62  const unsigned int red = (static_cast<unsigned int>(rgb[0]) << 16);
63  const unsigned int green = (static_cast<unsigned int>(rgb[1]) << 8);
64  const unsigned int blue = (static_cast<unsigned int>(rgb[2]));
65  return (red | green | blue);
66 }
67 
68 } // namespace
69 
70 // ----------------------------------------------------------------------------
72 private:
73  struct Obj {
74  std::string name;
75  size_t start_index;
76 
77  Obj(const std::string &n, size_t start)
78  : name(n), start_index(start) {};
79  };
80 
81 public:
82  void Clear() { objects_.clear(); }
83 
84  // start_index must be larger than all previously added items
85  void Add(const std::string &name, size_t start_index) {
86  if (!objects_.empty() && objects_.back().start_index >= start_index) {
88  "start_index {} must be larger than all previously added "
89  "objects {}.",
90  start_index, objects_.back().start_index);
91  }
92  objects_.emplace_back(name, start_index);
93  if (objects_[0].start_index != 0) {
95  "The first object's start_index must be 0, but got {}.",
96  objects_[0].start_index);
97  }
98  }
99 
100  const Obj &ObjectForIndex(size_t index) {
101  if (objects_.size() == 1) {
102  return objects_[0];
103  } else {
104  auto next = std::upper_bound(objects_.begin(), objects_.end(),
105  index, [](size_t value, const Obj &o) {
106  return value < o.start_index;
107  });
108  if (next == objects_.end()) {
109  utility::LogError("First object != 0");
110  }
111  if (next == objects_.end()) {
112  return objects_.back();
113 
114  } else {
115  --next;
116  return *next;
117  }
118  }
119  }
120 
121 private:
122  std::vector<Obj> objects_;
123 };
124 
125 // ----------------------------------------------------------------------------
127  rendering::Camera *camera) {
128  scene_ = scene;
129  camera_ = camera;
130  picking_scene_ =
131  std::make_shared<rendering::CloudViewerScene>(scene->GetRenderer());
132 
133  picking_scene_->SetDownsampleThreshold(SIZE_MAX); // don't downsample!
134  picking_scene_->SetBackground(kBackgroundColor);
135 
136  picking_scene_->GetView()->ConfigureForColorPicking();
137 }
138 
140 
142  point_size_ = px;
143  if (!points_.empty()) {
144  auto mat = MakeMaterial();
145  picking_scene_->GetScene()->OverrideMaterial(kSelectablePointsName,
146  mat);
147  }
148 }
149 
151  const std::vector<SceneWidget::PickableGeometry> &geometry) {
152  delete lookup_;
153  lookup_ = new SelectionIndexLookup();
154 
155  picking_scene_->ClearGeometry();
156  SetNeedsRedraw();
157 
158  // Record the points (for selection), and add a depth-write copy of any
159  // TriangleMesh so that occluded points are not selected.
160  points_.clear();
161  for (auto &pg : geometry) {
162  lookup_->Add(pg.name, points_.size());
163 
164  auto cloud = dynamic_cast<const ccPointCloud *>(pg.geometry);
165  auto tcloud =
166  dynamic_cast<const t::geometry::PointCloud *>(pg.tgeometry);
167  auto mesh = dynamic_cast<const ccMesh *>(pg.geometry);
168  auto tmesh =
169  dynamic_cast<const t::geometry::TriangleMesh *>(pg.tgeometry);
170 
171  if (cloud) {
172  std::vector<Eigen::Vector3d> temp_points = cloud->getEigenPoints();
173  points_.insert(points_.end(), temp_points.begin(),
174  temp_points.end());
175  } else if (mesh) {
176  std::vector<Eigen::Vector3d> temp_points = mesh->getEigenVertices();
177  points_.insert(points_.end(), temp_points.begin(),
178  temp_points.end());
179  } else if (tcloud || tmesh) {
180  const auto &tpoints = (tcloud ? tcloud->GetPoints()
181  : tmesh->GetVertexPositions());
182  const size_t n = tpoints.NumElements();
183  float *pts = (float *)tpoints.GetDataPtr();
184  points_.reserve(points_.size() + n);
185  for (size_t i = 0; i < n; i += 3) {
186  points_.emplace_back(double(pts[i]), double(pts[i + 1]),
187  double(pts[i + 2]));
188  }
189  }
190 
191  if (mesh || tmesh) {
192  // If we draw unlit with the background color, then if the mesh is
193  // drawn before the picking points the end effect is to just write
194  // to the depth buffer,and if we draw after the points then we paint
195  // over the occluded points. We paint with a special "mesh index"
196  // so that we can to enhanced picking if we hit a mesh index.
197  auto mesh_color = CalcIndexColor(kMeshIndex);
199  mat.shader = "unlitSolidColor"; // ignore any vertex colors!
200  mat.base_color = {float(mesh_color.x()), float(mesh_color.y()),
201  float(mesh_color.z()), 1.0f};
202  mat.sRGB_color = false;
203  if (mesh) {
204  picking_scene_->AddGeometry(pg.name, mesh, mat);
205  } else {
207  "PickPointsInteractor::SetPickableGeometry(): "
208  "CloudViewerScene cannot add a "
209  "t::geometry::TriangleMesh, "
210  "so points on the back side of the mesh '{}', will be "
211  "pickable",
212  pg.name);
213  // picking_scene_->AddGeometry(pg.name, tmesh, mat);
214  }
215  }
216  }
217 
218  if (points_.size() > kMaxPickableIndex) {
220  "Can only select from a maximumum of {} points; given {}",
221  kMaxPickableIndex, points_.size());
222  }
223 
224  if (!points_.empty()) { // Filament panics if an object has zero vertices
225  auto cloud = std::make_shared<ccPointCloud>(points_);
226  cloud->reserveTheRGBTable();
227  for (size_t i = 0; i < cloud->size(); ++i) {
228  cloud->addEigenColor(SetColorForIndex(uint32_t(i)));
229  }
230 
231  auto mat = MakeMaterial();
232  picking_scene_->AddGeometry(kSelectablePointsName, cloud.get(), mat);
233  picking_scene_->GetScene()->GeometryShadows(kSelectablePointsName,
234  false, false);
235  }
236 }
237 
238 void PickPointsInteractor::SetNeedsRedraw() { dirty_ = true; }
239 
241  return matrix_logic_;
242 }
243 
245  std::function<void(
246  const std::map<std::string,
247  std::vector<std::pair<size_t, Eigen::Vector3d>>>
248  &,
249  int)> f) {
250  on_picked_ = f;
251 }
252 
254  std::function<void(const std::vector<Eigen::Vector2i> &)> on_ui) {
255  on_ui_changed_ = on_ui;
256 }
257 
259  std::function<void()> on_poly_pick) {
260  on_started_poly_pick_ = on_poly_pick;
261 }
262 
264  if (e.type == MouseEvent::BUTTON_UP) {
265  if (e.modifiers & int(KeyModifier::ALT)) {
266  if (pending_.empty() || pending_.back().keymods == 0) {
267  pending_.push({{gui::Point(e.x, e.y)}, int(KeyModifier::ALT)});
268  if (on_ui_changed_) {
269  on_ui_changed_({});
270  }
271  } else {
272  pending_.back().polygon.push_back(gui::Point(e.x, e.y));
273  if (on_started_poly_pick_) {
274  on_started_poly_pick_();
275  }
276  if (on_ui_changed_) {
277  std::vector<Eigen::Vector2i> lines;
278  auto &polygon = pending_.back().polygon;
279  for (size_t i = 1; i < polygon.size(); ++i) {
280  auto &p0 = polygon[i - 1];
281  auto &p1 = polygon[i];
282  lines.push_back({p0.x, p0.y});
283  lines.push_back({p1.x, p1.y});
284  }
285  lines.push_back({polygon.back().x, polygon.back().y});
286  lines.push_back({polygon[0].x, polygon[0].y});
287  on_ui_changed_(lines);
288  }
289  }
290  } else {
291  pending_.push({{gui::Point(e.x, e.y)}, 0});
292  DoPick();
293  }
294  }
295 }
296 
298  if (e.type == KeyEvent::UP) {
299  if (e.key == KEY_ESCAPE) {
300  ClearPick();
301  }
302  }
303 }
304 
306  if (dirty_) {
307  SetNeedsRedraw();
308  auto *view = picking_scene_->GetView();
309  view->SetViewport(0, 0, // in case scene widget changed size
310  matrix_logic_.GetViewWidth(),
311  matrix_logic_.GetViewHeight());
312  view->GetCamera()->CopyFrom(camera_);
313  picking_scene_->GetRenderer().RenderToImage(
314  view, picking_scene_->GetScene(),
315  [this](std::shared_ptr<geometry::Image> img) {
316 #if WANT_DEBUG_IMAGE
317  std::cout << "[debug] Writing pick image to "
318  << "/tmp/debug.png" << std::endl;
319  io::WriteImage("/tmp/debug.png", *img);
320 #endif // WANT_DEBUG_IMAGE
321  this->OnPickImageDone(img);
322  });
323  } else {
324  OnPickImageDone(pick_image_);
325  }
326 }
327 
329  while (!pending_.empty()) {
330  pending_.pop();
331  }
332  if (on_ui_changed_) {
333  on_ui_changed_({});
334  }
335  SetNeedsRedraw();
336 }
337 
340  mat.shader = "unlitPolygonOffset";
341  mat.point_size = float(point_size_);
342  // We are not tonemapping, so src colors are RGB. This prevents the colors
343  // being converted from sRGB -> linear like normal.
344  mat.sRGB_color = false;
345  return mat;
346 }
347 
349  std::shared_ptr<geometry::Image> img) {
350  if (dirty_) {
351  pick_image_ = img;
352  dirty_ = false;
353  }
354 
355  if (on_ui_changed_) {
356  on_ui_changed_({});
357  }
358 
359  std::map<std::string, std::vector<std::pair<size_t, Eigen::Vector3d>>>
360  indices;
361  while (!pending_.empty()) {
362  PickInfo &info = pending_.back();
363  auto *img = pick_image_.get();
364  indices.clear();
365  if (info.polygon.size() == 1) {
366  const int x0 = info.polygon[0].x;
367  const int y0 = info.polygon[0].y;
368  struct Score { // this is a struct to force a default value
369  float score = 0;
370  };
371  std::unordered_map<unsigned int, Score> candidates;
372  auto clicked_idx = GetIndexForColor(img, x0, y0);
373  int radius;
374  // HACK: the color for kMeshIndex doesn't come back quite right.
375  // We shouldn't need to check if the index is out of range,
376  // but it does work.
377  if (clicked_idx == kMeshIndex || clicked_idx >= points_.size()) {
378  // We hit the middle of a triangle, try to find a nearby point
379  radius = 5 * point_size_;
380  } else {
381  // We either hit a point or an empty spot, so use a smaller
382  // radius. It looks weird to click on nothing in a point cloud
383  // and have a point get selected unless the point is really
384  // close.
385  radius = 2 * point_size_;
386  }
387  for (int y = y0 - radius; y < y0 + radius; ++y) {
388  for (int x = x0 - radius; x < x0 + radius; ++x) {
389  unsigned int idx = GetIndexForColor(img, x, y);
390  if (IsValidIndex(idx) && idx < points_.size()) {
391  float dist = std::sqrt(float((x - x0) * (x - x0) +
392  (y - y0) * (y - y0)));
393  candidates[idx].score += radius - dist;
394  }
395  }
396  }
397  if (!candidates.empty()) {
398  // Note that scores are (radius - dist), and since we take from
399  // a square pattern, a score can be negative. And multiple
400  // pixels of a point scoring negatively can make the negative up
401  // to -point_size^2.
402  float best_score = -1e30f;
403  unsigned int best_idx = (unsigned int)-1;
404  for (auto &idx_score : candidates) {
405  if (idx_score.second.score > best_score) {
406  best_score = idx_score.second.score;
407  best_idx = idx_score.first;
408  }
409  }
410  auto &o = lookup_->ObjectForIndex(best_idx);
411  size_t obj_idx = best_idx - o.start_index;
412  indices[o.name].push_back(std::pair<size_t, Eigen::Vector3d>(
413  obj_idx, points_[best_idx]));
414  }
415  } else {
416  // Use polygon fill algorithm to find the pixels that need to be
417  // checked.
418  // Good test cases: ______________ /|
419  // | / |\ / |
420  // | / | \ / |
421  // | /\ / | \/ |
422  // | / \ / | |
423  // | / \ / | _____|
424  // |/ \/ |____/
425  std::unordered_set<unsigned int> raw_indices;
426 
427  // Find the min/max y, so we can avoid excess looping.
428  int minY = 1000000, maxY = -1000000;
429  for (auto &p : info.polygon) {
430  minY = std::min(minY, p.y);
431  maxY = std::max(maxY, p.y);
432  }
433  // Duplicate the first point so for easy indexing
434  info.polygon.push_back(info.polygon[0]);
435  // Precalculate m and b (of y = mx + b)
436  const double kInf = 1e18;
437  std::vector<double> m, b;
438  m.reserve(info.polygon.size() - 1);
439  b.reserve(info.polygon.size() - 1);
440  for (size_t i = 1; i < info.polygon.size(); ++i) {
441  int m_denom = info.polygon[i].x - info.polygon[i - 1].x;
442  if (m_denom == 0) { // vertical line (x doesn't change)
443  m.push_back(kInf);
444  b.push_back(0.0);
445  continue;
446  }
447  m.push_back(double(info.polygon[i].y - info.polygon[i - 1].y) /
448  double(m_denom));
449  if (m.back() == 0.0) { // horiz line (y doesn't change)
450  b.push_back(info.polygon[i].y);
451  } else {
452  b.push_back(info.polygon[i].y -
453  m.back() * info.polygon[i].x);
454  }
455  }
456  // Loop through the rows of the polygon.
457  std::vector<bool> is_vert_corner(info.polygon.size(), false);
458  for (size_t i = 0; i < info.polygon.size() - 1; ++i) {
459  int prev = i - 1;
460  if (prev < 0) {
461  prev = info.polygon.size() - 2;
462  }
463  int next = i + 1;
464  int lastY = info.polygon[prev].y;
465  int thisY = info.polygon[i].y;
466  int nextY = info.polygon[next].y;
467  if ((thisY > lastY && thisY > nextY) ||
468  (thisY < lastY && thisY < nextY)) {
469  is_vert_corner[i] = true;
470  }
471  }
472  is_vert_corner.back() = is_vert_corner[0];
473  std::unordered_set<int> intersectionsX;
474  std::vector<int> sortedX;
475  intersectionsX.reserve(32);
476  sortedX.reserve(32);
477  for (int y = minY; y <= maxY; ++y) {
478  for (size_t i = 0; i < m.size(); ++i) {
479  if ((y < info.polygon[i].y && y < info.polygon[i + 1].y) ||
480  (y > info.polygon[i].y && y > info.polygon[i + 1].y)) {
481  continue;
482  }
483  if (m[i] == 0.0) { // horizontal
484  intersectionsX.insert({info.polygon[i].x});
485  intersectionsX.insert({info.polygon[i + 1].x});
486  } else if (m[i] == kInf) { // vertical
487  bool is_corner = (y == info.polygon[i].y);
488  intersectionsX.insert({info.polygon[i].x});
489  if (is_corner) {
490  intersectionsX.insert({info.polygon[i].x});
491  }
492  } else {
493  double x = (double(y) - b[i]) / m[i];
494  bool is_corner0 =
495  (y == info.polygon[i].y &&
496  std::abs(x - double(info.polygon[i].x)) < 0.5);
497  bool is_corner1 =
498  (y == info.polygon[i + 1].y &&
499  std::abs(x - double(info.polygon[i + 1].x)) <
500  0.5);
501  if ((is_corner0 && is_vert_corner[i]) ||
502  (is_corner1 && is_vert_corner[i + 1])) {
503  // We hit the corner, don't add, otherwise we will
504  // get half a segment.
505  } else {
506  intersectionsX.insert(int(std::round(x)));
507  }
508  }
509  }
510  for (auto x : intersectionsX) {
511  sortedX.push_back(x);
512  }
513  std::sort(sortedX.begin(), sortedX.end());
514 
515  // sortedX contains the horizontal line segment(s). This should
516  // be an even number, otherwise there is a problem. (Probably
517  // a corner got included)
518  if (sortedX.size() % 2 == 1) {
519  std::stringstream s;
520  for (size_t i = 0; i < info.polygon.size() - 1; ++i) {
521  s << "(" << info.polygon[i].x << ", "
522  << info.polygon[i].y << ") ";
523  }
525  "Internal error: Odd number of points for row "
526  "segments (should be even).");
527  utility::LogWarning("Polygon is: {}", s.str());
528  s.str("");
529  s << "{ ";
530  for (size_t i = 0; i < sortedX.size(); ++i) {
531  s << sortedX[i] << " ";
532  }
533  s << "}";
534  utility::LogWarning("y: {}, sortedX: {}", y, s.str());
535  // Recover: this is likely to give the wrong result, but
536  // better than the alternative of crashing.
537  sortedX.push_back(sortedX.back());
538  }
539 
540  // "Fill" the pixels on this row
541  for (size_t i = 0; i < sortedX.size(); i += 2) {
542  int startX = sortedX[i];
543  int endX = sortedX[i + 1];
544  for (int x = startX; x <= endX; ++x) {
545  unsigned int idx = GetIndexForColor(img, x, y);
546  if (IsValidIndex(idx) && idx < points_.size()) {
547  raw_indices.insert(idx);
548  }
549  }
550  }
551  intersectionsX.clear();
552  sortedX.clear();
553  }
554  // Now add everything that was "filled"
555  for (auto idx : raw_indices) {
556  auto &o = lookup_->ObjectForIndex(idx);
557  size_t obj_idx = idx - o.start_index;
558  indices[o.name].push_back(std::pair<size_t, Eigen::Vector3d>(
559  obj_idx, points_[idx]));
560  }
561  }
562 
563  pending_.pop();
564 
565  if (on_picked_ && !indices.empty()) {
566  on_picked_(indices, info.keymods);
567  }
568  }
569 }
570 
571 } // namespace gui
572 } // namespace visualization
573 } // namespace cloudViewer
std::shared_ptr< core::Tensor > image
std::string name
math::float4 next
boost::geometry::model::polygon< point_xy > polygon
Definition: TreeIso.cpp:37
Triangular mesh.
Definition: ecvMesh.h:35
A 3D cloud and its associated features (color, normals, scalar fields, etc.)
A point cloud contains a list of 3D points.
Definition: PointCloud.h:82
A triangle mesh contains vertices and triangles.
Definition: TriangleMesh.h:98
void SetOnUIChanged(std::function< void(const std::vector< Eigen::Vector2i > &)>)
void SetOnStartedPolygonPicking(std::function< void()> on_poly_pick)
Calls the provided function when polygon picking is initiated.
void SetPickableGeometry(const std::vector< SceneWidget::PickableGeometry > &geometry)
rendering::MatrixInteractorLogic & GetMatrixInteractor() override
PickPointsInteractor(rendering::CloudViewerScene *scene, rendering::Camera *camera)
void OnPickImageDone(std::shared_ptr< geometry::Image > img)
void SetOnPointsPicked(std::function< void(const std::map< std::string, std::vector< std::pair< size_t, Eigen::Vector3d >>> &, int)> f)
void Add(const std::string &name, size_t start_index)
#define LogWarning(...)
Definition: Logging.h:72
#define LogError(...)
Definition: Logging.h:60
int min(int a, int b)
Definition: cutil_math.h:53
__host__ __device__ int2 abs(int2 v)
Definition: cutil_math.h:1267
int max(int a, int b)
Definition: cutil_math.h:48
#define SIZE_MAX
static double dist(double x1, double y1, double x2, double y2)
Definition: lsd.c:207
Generic file read and write utility for python interface.
constexpr Rgb red(MAX, 0, 0)
constexpr Rgb blue(0, 0, MAX)
constexpr Rgb green(0, MAX, 0)