ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
MinimumOBB.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/src/Core/Matrix.h>
11 
16 
17 namespace cloudViewer {
18 namespace t {
19 namespace geometry {
20 namespace kernel {
21 namespace minimum_obb {
22 
23 namespace {
24 
25 // Helper struct using Eigen datatypes on the stack for OBB computation
26 struct EigenOBB {
27  EigenOBB()
28  : R_(Eigen::Matrix3d::Identity()),
29  extent_(Eigen::Vector3d::Zero()),
30  center_(Eigen::Vector3d::Zero()) {}
31  EigenOBB(const OrientedBoundingBox& obb)
34  obb.GetExtent().Reshape({3, 1}))),
36  obb.GetCenter().Reshape({3, 1}))) {}
37  double Volume() const { return extent_(0) * extent_(1) * extent_(2); }
38  operator OrientedBoundingBox() const {
41  obb.SetExtent(
43  {3}));
44  obb.SetCenter(
46  {3}));
47  return obb;
48  }
49  Eigen::Matrix3d R_;
50  Eigen::Vector3d extent_;
51  Eigen::Vector3d center_;
52 };
53 } // namespace
54 
56  bool robust) {
57  // ------------------------------------------------------------
58  // 0) Compute the convex hull of the input point cloud
59  // ------------------------------------------------------------
61  if (points_.GetShape(0) == 0) {
62  utility::LogError("Input point set is empty.");
63  return OrientedBoundingBox();
64  }
65  if (points_.GetShape(0) < 4) {
66  utility::LogError("Input point set has less than 4 points.");
67  return OrientedBoundingBox();
68  }
69  // copy to CPU here
70  PointCloud pcd(points_.To(core::Device()));
71  auto hull_mesh = pcd.ComputeConvexHull(robust);
72  if (hull_mesh.GetVertexPositions().NumElements() == 0) {
73  utility::LogError("Failed to compute convex hull.");
74  return OrientedBoundingBox();
75  }
76 
77  // Get convex hull vertices and triangles
78  const std::vector<Eigen::Vector3d>& hull_v =
80  hull_mesh.GetVertexPositions());
81  const std::vector<Eigen::Vector3i>& hull_t =
83  hull_mesh.GetTriangleIndices());
84  int num_vertices = static_cast<int>(hull_v.size());
85  int num_triangles = static_cast<int>(hull_t.size());
86 
88  hull_mesh.GetVertexPositions())
90  double min_volume = min_obb.Volume();
91 
92  // Handle degenerate planar cases up front.
93  if (num_vertices <= 3 || num_triangles < 1) { // Handle degenerate case
94  utility::LogError("Convex hull is degenerate.");
95  return OrientedBoundingBox();
96  }
97 
98  auto mapOBBToClosestIdentity = [&](EigenOBB obb) {
99  Eigen::Matrix3d& R = obb.R_;
100  Eigen::Vector3d& extent = obb.extent_;
101  Eigen::Vector3d col[3] = {R.col(0), R.col(1), R.col(2)};
102  Eigen::Vector3d ext = extent;
103  double best_score = -1e9;
104  Eigen::Matrix3d best_R = Eigen::Matrix3d::Identity();
105  Eigen::Vector3d best_extent{{-1, -1, -1}};
106  // Hard-coded permutations of indices [0,1,2]
107  static const std::array<std::array<int, 3>, 6> permutations = {
108  {{{0, 1, 2}},
109  {{0, 2, 1}},
110  {{1, 0, 2}},
111  {{1, 2, 0}},
112  {{2, 0, 1}},
113  {{2, 1, 0}}}};
114 
115  // Evaluate all 6 permutations × 8 sign flips = 48 candidates
116  for (const auto& p : permutations) {
117  for (int sign_bits = 0; sign_bits < 8; ++sign_bits) {
118  // Derive the sign of each axis from bits (0 => -1, 1 => +1)
119  // s0 is bit0, s1 is bit1, s2 is bit2 of sign_bits
120  const int s0 = (sign_bits & 1) ? 1 : -1;
121  const int s1 = (sign_bits & 2) ? 1 : -1;
122  const int s2 = (sign_bits & 4) ? 1 : -1;
123 
124  // Construct candidate columns
125  Eigen::Vector3d c0 = s0 * col[p[0]];
126  Eigen::Vector3d c1 = s1 * col[p[1]];
127  Eigen::Vector3d c2 = s2 * col[p[2]];
128 
129  // Score: how close are we to the identity?
130  // Since e_x = (1,0,0), e_y = (0,1,0), e_z = (0,0,1),
131  // we can skip dot products & do c0(0)+c1(1)+c2(2).
132  double score = c0(0) + c1(1) + c2(2);
133 
134  // If this orientation is better, update the best.
135  if (score > best_score) {
136  best_score = score;
137  best_R.col(0) = c0;
138  best_R.col(1) = c1;
139  best_R.col(2) = c2;
140 
141  // Re-permute extents: if the axis p[0] in old frame
142  // now goes to new X, etc.
143  best_extent(0) = ext(p[0]);
144  best_extent(1) = ext(p[1]);
145  best_extent(2) = ext(p[2]);
146  }
147  }
148  }
149 
150  // Update the OBB with the best orientation found
151  obb.R_ = best_R;
152  obb.extent_ = best_extent;
153  };
154 
155  // --------------------------------------------------------------------
156  // 1) Precompute vertex adjacency data, face normals, and edge data
157  // --------------------------------------------------------------------
158  std::vector<std::vector<int>> adjacency_data;
159  adjacency_data.reserve(num_vertices);
160  adjacency_data.insert(adjacency_data.end(), num_vertices,
161  std::vector<int>());
162 
163  std::vector<Eigen::Vector3d> face_normals;
164  face_normals.reserve(num_triangles);
165 
166  // Each edge is stored as (v0, v1).
167  std::vector<std::pair<int, int>> edges;
168  edges.reserve(num_vertices * 2);
169 
170  // Each edge knows which two faces it belongs to: (f0, f1).
171  std::vector<std::pair<int, int>> faces_for_edge;
172  faces_for_edge.reserve(num_vertices * 2);
173 
174  constexpr unsigned int empty_edge =
176  std::vector<unsigned int> vertex_pairs_to_edges(num_vertices * num_vertices,
177  empty_edge);
178 
179  for (int i = 0; i < num_triangles; ++i) {
180  const Eigen::Vector3i& tri = hull_t[i];
181  int t0 = tri(0), t1 = tri(1), t2 = tri(2);
182  int v0 = t2, v1 = t0;
183 
184  for (int j = 0; j < 3; ++j) {
185  v1 = tri(j);
186 
187  // Build Adjacency Data (vertex -> adjacent vertices)
188  adjacency_data[v0].push_back(v1);
189 
190  // Register Edges (edge -> neighbouring faces)
191  unsigned int& ref_idx1 =
192  vertex_pairs_to_edges[v0 * num_vertices + v1];
193  unsigned int& ref_idx2 =
194  vertex_pairs_to_edges[v1 * num_vertices + v0];
195  if (ref_idx1 == empty_edge) {
196  // Not registered yet
197  unsigned int new_idx = static_cast<unsigned int>(edges.size());
198  ref_idx1 = new_idx;
199  ref_idx2 = new_idx;
200  edges.emplace_back(v0, v1);
201  faces_for_edge.emplace_back(i, -1);
202  } else {
203  // Already existing, update the second face index
204  faces_for_edge[ref_idx1].second = i;
205  }
206 
207  v0 = v1;
208  }
209  // Compute Face Normal
210  auto n = (hull_v[t1] - hull_v[t0]).cross(hull_v[t2] - hull_v[t0]);
211  face_normals.push_back(n.normalized());
212  }
213 
214  // ------------------------------------------------------------
215  // 2) Precompute "antipodal vertices" for each edge of the hull
216  // ------------------------------------------------------------
217 
218  // Throughout the algorithm, internal edges can all be discarded.
219  auto isInternalEdge = [&](std::size_t iEdge) noexcept {
220  return (face_normals[faces_for_edge[iEdge].first].dot(
221  face_normals[faces_for_edge[iEdge].second]) >
222  1.0 - 1e-4);
223  };
224 
225  // Throughout the whole algorithm, this array stores an auxiliary structure
226  // for performing graph searches on the vertices of the convex hull.
227  // Conceptually each index of the array stores a boolean whether we have
228  // visited that vertex or not during the current search. However storing
229  // such booleans is slow, since we would have to perform a linear-time scan
230  // through this array before next search to reset each boolean to unvisited
231  // false state. Instead, store a number, called a "color" for each vertex to
232  // specify whether that vertex has been visited, and manage a global color
233  // counter flood_fill_visit_color that represents the visited vertices. At
234  // any given time, the vertices that have already been visited have the
235  // value flood_fill_visited[i] == flood_fill_visit_color in them. This gives
236  // a win that we can perform constant-time clears of the flood_fill_visited
237  // array, by simply incrementing the "color" counter to clear the array.
238 
239  int edge_size = edges.size();
240  std::vector<std::vector<int>> antipodal_points_for_edge(edge_size);
241  antipodal_points_for_edge.reserve(edge_size);
242 
243  std::vector<unsigned int> flood_fill_visited(num_vertices, 0u);
244  unsigned int flood_fill_visit_color = 1u;
245 
246  auto markVertexVisited = [&](int v) {
247  flood_fill_visited[v] = flood_fill_visit_color;
248  };
249 
250  auto haveVisitedVertex = [&](int v) -> bool {
251  return flood_fill_visited[v] == flood_fill_visit_color;
252  };
253 
254  auto clearGraphSearch = [&]() { ++flood_fill_visit_color; };
255 
256  auto isVertexAntipodalToEdge =
257  [&](int vi, const std::vector<int>& neighbors,
258  const Eigen::Vector3d& f1a,
259  const Eigen::Vector3d& f1b) noexcept -> bool {
260  constexpr double epsilon = 1e-4;
261  constexpr double degenerate_threshold = -5e-2;
262  double t_min = 0.0;
263  double t_max = 1.0;
264 
265  // Precompute values outside the loop for efficiency.
266  const auto& v = hull_v[vi];
267  Eigen::Vector3d f1b_f1a = f1b - f1a;
268 
269  // Iterate over each neighbor.
270  for (int neighbor_index : neighbors) {
271  const auto& neighbor = hull_v[neighbor_index];
272 
273  // Compute edge vector e = neighbor - v.
274  Eigen::Vector3d e = neighbor - v;
275 
276  // Compute dot products manually for efficiency.
277  double s = f1b_f1a.dot(e);
278  double n = f1b.dot(e);
279 
280  // Adjust t_min and t_max based on the value of s.
281  if (s > epsilon) {
282  t_max = std::min(t_max, n / s);
283  } else if (s < -epsilon) {
284  t_min = std::max(t_min, n / s);
285  } else if (n < -epsilon) {
286  // No feasible t if n is negative when s is nearly zero.
287  return false;
288  }
289 
290  // If the valid interval for t has degenerated, exit early.
291  if ((t_max - t_min) < degenerate_threshold) {
292  return false;
293  }
294  }
295  return true;
296  };
297 
298  auto extremeVertexConvex =
299  [&](auto& self, const Eigen::Vector3d& direction,
300  std::vector<unsigned int>& flood_fill_visited,
301  unsigned int flood_fill_visit_color,
302  double& most_extreme_distance, int starting_vertex) -> int {
303  // Compute dot product for the starting vertex.
304  double cur_dot = direction.dot(hull_v[starting_vertex]);
305 
306  // Cache neighbor list for the starting vertex.
307  const int* neighbors = &adjacency_data[starting_vertex][0];
308  const int* neighbors_end =
309  neighbors + adjacency_data[starting_vertex].size();
310 
311  // Mark starting vertex as visited.
312  flood_fill_visited[starting_vertex] = flood_fill_visit_color;
313 
314  // Traverse neighbors to find more extreme vertices.
315  int second_best = -1;
316  double second_best_dot = cur_dot - 1e-3;
317  while (neighbors != neighbors_end) {
318  int n = *neighbors++;
319  if (flood_fill_visited[n] != flood_fill_visit_color) {
320  double dot = direction.dot(hull_v[n]);
321  if (dot > cur_dot) {
322  // Found a new vertex with higher dot product.
323  starting_vertex = n;
324  cur_dot = dot;
325  flood_fill_visited[starting_vertex] =
326  flood_fill_visit_color;
327  neighbors = &adjacency_data[starting_vertex][0];
328  neighbors_end =
329  neighbors + adjacency_data[starting_vertex].size();
330  second_best = -1;
331  second_best_dot = cur_dot - 1e-3;
332  } else if (dot > second_best_dot) {
333  // Update second-best candidate.
334  second_best = n;
335  second_best_dot = dot;
336  }
337  }
338  }
339 
340  // Explore second-best neighbor recursively if valid.
341  if (second_best != -1 &&
342  flood_fill_visited[second_best] != flood_fill_visit_color) {
343  double second_most_extreme =
345  int second_try = self(self, direction, flood_fill_visited,
346  flood_fill_visit_color, second_most_extreme,
347  second_best);
348 
349  if (second_most_extreme > cur_dot) {
350  most_extreme_distance = second_most_extreme;
351  return second_try;
352  }
353  }
354 
355  most_extreme_distance = cur_dot;
356  return starting_vertex;
357  };
358 
359  // The currently best variant for establishing a spatially coherent
360  // traversal order.
361  std::vector<int> spatial_face_order;
362  spatial_face_order.reserve(num_triangles);
363  std::vector<int> spatial_edge_order;
364  spatial_edge_order.reserve(edge_size);
365 
366  // Initialize random number generator
367  std::random_device rd; // Obtain a random number from hardware
368  std::mt19937 rng(rd()); // Seed the generator
369  { // Explicit scope for variables that are not needed after this.
370 
371  std::vector<unsigned int> visited_edges(edge_size, 0u);
372  std::vector<unsigned int> visited_faces(num_triangles, 0u);
373 
374  std::vector<std::pair<int, int>> traverse_stack_edges;
375  traverse_stack_edges.reserve(edge_size);
376  traverse_stack_edges.emplace_back(0, adjacency_data[0].front());
377  while (!traverse_stack_edges.empty()) {
378  auto e = traverse_stack_edges.back();
379  traverse_stack_edges.pop_back();
380 
381  // Find edge index
382  int edge_idx =
383  vertex_pairs_to_edges[e.first * num_vertices + e.second];
384  if (visited_edges[edge_idx]) continue;
385  visited_edges[edge_idx] = 1;
386  auto& ff = faces_for_edge[edge_idx];
387  if (!visited_faces[ff.first]) {
388  visited_faces[ff.first] = 1;
389  spatial_face_order.push_back(ff.first);
390  }
391  if (!visited_faces[ff.second]) {
392  visited_faces[ff.second] = 1;
393  spatial_face_order.push_back(ff.second);
394  }
395 
396  // If not an internal edge, keep it
397  if (!isInternalEdge(edge_idx)) {
398  spatial_edge_order.push_back(edge_idx);
399  }
400 
401  int v0 = e.second;
402  size_t size_before = traverse_stack_edges.size();
403  for (int v1 : adjacency_data[v0]) {
404  int e1 = vertex_pairs_to_edges[v0 * num_vertices + v1];
405  if (visited_edges[e1]) continue;
406  traverse_stack_edges.push_back(std::make_pair(v0, v1));
407  }
408 
409  // Randomly shuffle newly added edges
410  int n_new_edges =
411  static_cast<int>(traverse_stack_edges.size() - size_before);
412  if (n_new_edges > 0) {
413  std::uniform_int_distribution<> distr(0, n_new_edges - 1);
414  int r = distr(rng);
415  std::swap(traverse_stack_edges.back(),
416  traverse_stack_edges[size_before + r]);
417  }
418  }
419  }
420 
421  // --------------------------------------------------------------------
422  // 3) Precompute "sidepodal edges" for each edge of the hull
423  // --------------------------------------------------------------------
424 
425  // Stores a memory of yet unvisited vertices for current graph search.
426  std::vector<int> traverse_stack;
427 
428  // Since we do several extreme vertex searches, and the search directions
429  // have a lot of spatial locality, always start the search for the next
430  // extreme vertex from the extreme vertex that was found during the previous
431  // iteration for the previous edge. This has been profiled to improve
432  // overall performance by as much as 15-25%.
433  int start_vertex = 0;
434 
435  // Precomputation: for each edge, we need to compute the list of potential
436  // antipodal points (points on the opposing face of an enclosing OBB of the
437  // face that is flush with the given edge of the polyhedron).
438  for (int edge_i : spatial_edge_order) {
439  auto [face_i_a, face_i_b] = faces_for_edge[edge_i];
440  const Eigen::Vector3d& f1a = face_normals[face_i_a];
441  const Eigen::Vector3d& f1b = face_normals[face_i_b];
442 
443  double dummy;
444  clearGraphSearch();
445  start_vertex = extremeVertexConvex(
446  extremeVertexConvex, -f1a, flood_fill_visited,
447  flood_fill_visit_color, dummy, start_vertex);
448  clearGraphSearch();
449 
450  traverse_stack.push_back(start_vertex);
451  markVertexVisited(start_vertex);
452  while (!traverse_stack.empty()) {
453  int v = traverse_stack.back();
454  traverse_stack.pop_back();
455  const auto& neighbors = adjacency_data[v];
456  if (isVertexAntipodalToEdge(v, neighbors, f1a, f1b)) {
457  if (edges[edge_i].first == v || edges[edge_i].second == v) {
458  return OrientedBoundingBox();
459  }
460  antipodal_points_for_edge[edge_i].push_back(v);
461  for (size_t j = 0; j < neighbors.size(); ++j) {
462  if (!haveVisitedVertex(neighbors[j])) {
463  traverse_stack.push_back(neighbors[j]);
464  markVertexVisited(neighbors[j]);
465  }
466  }
467  }
468  }
469 
470  // Robustness: If the above search did not find any antipodal points,
471  // add the first found extreme point at least, since it is always an
472  // antipodal point. This is known to occur very rarely due to numerical
473  // imprecision in the above loop over adjacent edges.
474  if (antipodal_points_for_edge[edge_i].empty()) {
475  // Getting here is most likely a bug. Fall back to linear scan,
476  // which is very slow.
477  for (int j = 0; j < num_vertices; ++j) {
478  if (isVertexAntipodalToEdge(j, adjacency_data[j], f1a, f1b)) {
479  antipodal_points_for_edge[edge_i].push_back(j);
480  }
481  }
482  }
483  }
484 
485  // Data structure for sidepodal vertices.
486  std::vector<unsigned char> sidepodal_vertices(edge_size * num_vertices, 0);
487 
488  // Stores for each edge i the list of all sidepodal edge indices j that it
489  // can form an OBB with.
490  std::vector<std::vector<int>> compatible_edges(edge_size);
491  compatible_edges.reserve(edge_size);
492 
493  // Compute all sidepodal edges for each edge by performing a graph search.
494  // The set of sidepodal edges is connected in the graph, which lets us avoid
495  // having to iterate over each edge pair of the convex hull.
496  for (int edge_i : spatial_edge_order) {
497  auto [face_i_a, face_i_b] = faces_for_edge[edge_i];
498  const Eigen::Vector3d& f1a = face_normals[face_i_a];
499  const Eigen::Vector3d& f1b = face_normals[face_i_b];
500 
501  // Pixar orthonormal basis code:
502  // https://graphics.pixar.com/library/OrthonormalB/paper.pdf
503  Eigen::Vector3d dead_direction = (f1a + f1b) * 0.5;
504  Eigen::Vector3d basis1, basis2;
505  double sign = std::copysign(1.0, dead_direction.z());
506  const double a = -1.0 / (sign + dead_direction.z());
507  const double b = dead_direction.x() * dead_direction.y() * a;
508  basis1 = Eigen::Vector3d(
509  1.0 + sign * dead_direction.x() * dead_direction.x() * a,
510  sign * b, -sign * dead_direction.x());
511  basis2 = Eigen::Vector3d(
512  b, sign + dead_direction.y() * dead_direction.y() * a,
513  -dead_direction.y());
514 
515  double dummy;
516  Eigen::Vector3d dir =
517  (f1a.cross(Eigen::Vector3d(0, 1, 0))).normalized();
518  if (dir.norm() < 1e-4) {
519  dir = Eigen::Vector3d(0, 0, 1); // If f1a is parallel to y-axis
520  }
521  clearGraphSearch();
522  start_vertex = extremeVertexConvex(
523  extremeVertexConvex, dir, flood_fill_visited,
524  flood_fill_visit_color, dummy, start_vertex);
525  clearGraphSearch();
526  traverse_stack.push_back(start_vertex);
527  while (!traverse_stack.empty()) {
528  int v = traverse_stack.back();
529  traverse_stack.pop_back();
530 
531  if (haveVisitedVertex(v)) continue;
532  markVertexVisited(v);
533 
534  // const auto& neighbors = adjacency_data[v];
535  for (int v_adj : adjacency_data[v]) {
536  if (haveVisitedVertex(v_adj)) continue;
537  int edge = vertex_pairs_to_edges[v * num_vertices + v_adj];
538  auto [face_i_a, face_i_b] = faces_for_edge[edge];
539  Eigen::Vector3d f1a_f1b = f1a - f1b;
540  Eigen::Vector3d f2a_f2b =
541  face_normals[face_i_a] - face_normals[face_i_b];
542 
543  double a2 = f1b.dot(face_normals[face_i_b]);
544  double b2 = f1a_f1b.dot(face_normals[face_i_b]);
545  double c2 = f2a_f2b.dot(f1b);
546  double d2 = f1a_f1b.dot(f2a_f2b);
547  double ab = a2 + b2;
548  double ac = a2 + c2;
549  double abcd = ab + c2 + d2;
550  double min_val = std::min({a2, ab, ac, abcd});
551  double max_val = std::max({a2, ab, ac, abcd});
552  bool are_edges_compatible_for_obb =
553  (min_val <= 0.0 && max_val >= 0.0);
554 
555  if (are_edges_compatible_for_obb) {
556  if (edge_i <= edge) {
557  if (!isInternalEdge(edge)) {
558  compatible_edges[edge_i].push_back(edge);
559  }
560 
561  sidepodal_vertices[edge_i * num_vertices +
562  edges[edge].first] = 1;
563  sidepodal_vertices[edge_i * num_vertices +
564  edges[edge].second] = 1;
565  if (edge_i != edge) {
566  if (!isInternalEdge(edge)) {
567  compatible_edges[edge].push_back(edge_i);
568  }
569  sidepodal_vertices[edge * num_vertices +
570  edges[edge_i].first] = 1;
571  sidepodal_vertices[edge * num_vertices +
572  edges[edge_i].second] = 1;
573  }
574  }
575  traverse_stack.push_back(v_adj);
576  }
577  }
578  }
579  }
580 
581  // --------------------------------------------------------------------
582  // 4) Test configurations where all three edges are on adjacent faces.
583  // --------------------------------------------------------------------
584 
585  // Take advantage of spatial locality: start the search for the extreme
586  // vertex from the extreme vertex that was found during the previous
587  // iteration for the previous edge. This speeds up the search since edge
588  // directions have some amount of spatial locality and the next extreme
589  // vertex is often close to the previous one. Track two hint variables since
590  // we are performing extreme vertex searches to two opposing directions at
591  // the same time.
592  int v_hint1 = 0;
593  int v_hint2 = 0;
594  int v_hint3 = 0;
595  int v_hint4 = 0;
596  int v_hint1_b = 0;
597  int v_hint2_b = 0;
598  int v_hint3_b = 0;
599 
600  // Stores a memory of yet unvisited vertices that are common sidepodal
601  // vertices to both currently chosen edges for current graph search.
602  std::vector<int> traverseStackCommonSidepodals;
603  traverseStackCommonSidepodals.reserve(num_vertices);
604  for (int edge_i : spatial_edge_order) {
605  auto [face_i_a, face_i_b] = faces_for_edge[edge_i];
606  const Eigen::Vector3d& f1a = face_normals[face_i_a];
607  const Eigen::Vector3d& f1b = face_normals[face_i_b];
608 
609  const auto& compatible_edges_i = compatible_edges[edge_i];
610  Eigen::Vector3d baseDir = 0.5 * (f1a + f1b);
611 
612  for (int edge_j : compatible_edges_i) {
613  if (edge_j <= edge_i) continue; // Remove symmetry.
614  auto [faceJ_a, faceJ_b] = faces_for_edge[edge_j];
615  const Eigen::Vector3d& f2a = face_normals[faceJ_a];
616  const Eigen::Vector3d& f2b = face_normals[faceJ_b];
617 
618  // Compute search direction
619  Eigen::Vector3d dead_dir = 0.5 * (f2a + f2b);
620  Eigen::Vector3d search_dir = baseDir.cross(dead_dir);
621  search_dir = search_dir.normalized();
622  if (search_dir.norm() < 1e-9) {
623  search_dir = f1a.cross(f2a);
624  search_dir = search_dir.normalized();
625  if (search_dir.norm() < 1e-9) {
626  search_dir =
627  (f1a.cross(Eigen::Vector3d(0, 1, 0))).normalized();
628  }
629  }
630 
631  double dummy;
632  clearGraphSearch();
633  v_hint1 = extremeVertexConvex(
634  extremeVertexConvex, search_dir, flood_fill_visited,
635  flood_fill_visit_color, dummy, v_hint1);
636  clearGraphSearch();
637  v_hint2 = extremeVertexConvex(
638  extremeVertexConvex, -search_dir, flood_fill_visited,
639  flood_fill_visit_color, dummy, v_hint2);
640 
641  int secondSearch = -1;
642  if (sidepodal_vertices[edge_j * num_vertices + v_hint1]) {
643  traverseStackCommonSidepodals.push_back(v_hint1);
644  } else {
645  traverse_stack.push_back(v_hint1);
646  }
647  if (sidepodal_vertices[edge_j * num_vertices + v_hint2]) {
648  traverseStackCommonSidepodals.push_back(v_hint2);
649  } else {
650  secondSearch = v_hint2;
651  }
652 
653  // Bootstrap to a good vertex that is sidepodal to both edges.
654  clearGraphSearch();
655  while (!traverse_stack.empty()) {
656  int v = traverse_stack.front();
657  traverse_stack.erase(traverse_stack.begin());
658  if (haveVisitedVertex(v)) continue;
659  markVertexVisited(v);
660  const auto& neighbors = adjacency_data[v];
661  for (int v_adj : neighbors) {
662  if (!haveVisitedVertex(v_adj) &&
663  sidepodal_vertices[edge_i * num_vertices + v_adj]) {
664  if (sidepodal_vertices[edge_j * num_vertices + v_adj]) {
665  traverse_stack.clear();
666  if (secondSearch != -1) {
667  traverse_stack.push_back(secondSearch);
668  secondSearch = -1;
669  markVertexVisited(v_adj);
670  }
671  traverseStackCommonSidepodals.push_back(v_adj);
672  break;
673  } else {
674  traverse_stack.push_back(v_adj);
675  }
676  }
677  }
678  }
679 
680  clearGraphSearch();
681  while (!traverseStackCommonSidepodals.empty()) {
682  int v = traverseStackCommonSidepodals.back();
683  traverseStackCommonSidepodals.pop_back();
684  if (haveVisitedVertex(v)) continue;
685  markVertexVisited(v);
686  const auto& neighbors = adjacency_data[v];
687  for (int v_adj : neighbors) {
688  int edge_k =
689  vertex_pairs_to_edges[v * num_vertices + v_adj];
690  int idx_i = edge_i * num_vertices + v_adj;
691  int idx_j = edge_j * num_vertices + v_adj;
692 
693  if (isInternalEdge(edge_k)) continue;
694 
695  if (sidepodal_vertices[idx_i] &&
696  sidepodal_vertices[idx_j]) {
697  if (!haveVisitedVertex(v_adj)) {
698  traverseStackCommonSidepodals.push_back(v_adj);
699  }
700  if (edge_j < edge_k) {
701  auto [faceK_a, faceK_b] = faces_for_edge[edge_k];
702  const Eigen::Vector3d& f3a = face_normals[faceK_a];
703  const Eigen::Vector3d& f3b = face_normals[faceK_b];
704 
705  std::vector<Eigen::Vector3d> n1 = {
706  Eigen::Vector3d::Zero(),
707  Eigen::Vector3d::Zero()};
708  std::vector<Eigen::Vector3d> n2 = {
709  Eigen::Vector3d::Zero(),
710  Eigen::Vector3d::Zero()};
711  std::vector<Eigen::Vector3d> n3 = {
712  Eigen::Vector3d::Zero(),
713  Eigen::Vector3d::Zero()};
714 
715  constexpr double eps = 1e-4;
716  constexpr double angle_eps = 1e-3;
717  int n_solutions = 0;
718 
719  {
720  // Precompute intermediate vectors for
721  // polynomial coefficients.
722  Eigen::Vector3d a = f1b;
723  Eigen::Vector3d b = f1a - f1b;
724  Eigen::Vector3d c = f2b;
725  Eigen::Vector3d d = f2a - f2b;
726  Eigen::Vector3d e = f3b;
727  Eigen::Vector3d f = f3a - f3b;
728 
729  // Compute polynomial coefficients.
730  double g = a.dot(c) * d.dot(e) -
731  a.dot(d) * c.dot(e);
732  double h = a.dot(c) * d.dot(f) -
733  a.dot(d) * c.dot(f);
734  double i = b.dot(c) * d.dot(e) -
735  b.dot(d) * c.dot(e);
736  double j = b.dot(c) * d.dot(f) -
737  b.dot(d) * c.dot(f);
738  double k = g * b.dot(e) - a.dot(e) * i;
739  double l = h * b.dot(e) + g * b.dot(f) -
740  a.dot(f) * i - a.dot(e) * j;
741  double m = h * b.dot(f) - a.dot(f) * j;
742  double s = l * l - 4 * m * k;
743 
744  // Handle degenerate or linear case.
745  if (std::abs(m) < 1e-5 || std::abs(s) < 1e-5) {
746  double v = -k / l;
747  double t = -(g + h * v) / (i + j * v);
748  double u = -(c.dot(e) + c.dot(f) * v) /
749  (d.dot(e) + d.dot(f) * v);
750  n_solutions = 0;
751 
752  // If we happened to divide by zero above,
753  // the following checks handle them.
754  if (v >= -eps && t >= -eps && u >= -eps &&
755  v <= 1.0 + eps && t <= 1.0 + eps &&
756  u <= 1.0 + eps) {
757  n1[0] = (a + b * t).normalized();
758  n2[0] = (c + d * u).normalized();
759  n3[0] = (e + f * v).normalized();
760  if (std::abs(n1[0].dot(n2[0])) <
761  angle_eps &&
762  std::abs(n1[0].dot(n3[0])) <
763  angle_eps &&
764  std::abs(n2[0].dot(n3[0])) <
765  angle_eps) {
766  n_solutions = 1;
767  } else {
768  n_solutions = 0;
769  }
770  }
771  } else {
772  // Discriminant negative: no solutions for v
773  if (s < 0.0) {
774  n_solutions = 0;
775  } else {
776  double sgn_l = l < 0 ? -1.0 : 1.0;
777  double V1 =
778  -(l + sgn_l * std::sqrt(s)) /
779  (2.0 * m);
780  double V2 = k / (m * V1);
781  double T1 =
782  -(g + h * V1) / (i + j * V1);
783  double T2 =
784  -(g + h * V2) / (i + j * V2);
785  double U1 =
786  -(c.dot(e) + c.dot(f) * V1) /
787  (d.dot(e) + d.dot(f) * V1);
788  double U2 =
789  -(c.dot(e) + c.dot(f) * V2) /
790  (d.dot(e) + d.dot(f) * V2);
791 
792  if (V1 >= -eps && T1 >= -eps &&
793  U1 >= -eps && V1 <= 1.0 + eps &&
794  T1 <= 1.0 + eps &&
795  U1 <= 1.0 + eps) {
796  n1[n_solutions] =
797  (a + b * T1).normalized();
798  n2[n_solutions] =
799  (c + d * U1).normalized();
800  n3[n_solutions] =
801  (e + f * V1).normalized();
802 
803  if (std::abs(n1[n_solutions].dot(
804  n2[n_solutions])) <
805  angle_eps &&
806  std::abs(n1[n_solutions].dot(
807  n3[n_solutions])) <
808  angle_eps &&
809  std::abs(n2[n_solutions].dot(
810  n3[n_solutions])) <
811  angle_eps)
812  ++n_solutions;
813  }
814  if (V2 >= -eps && T2 >= -eps &&
815  U2 >= -eps && V2 <= 1.0 + eps &&
816  T2 <= 1.0 + eps &&
817  U2 <= 1.0 + eps) {
818  n1[n_solutions] =
819  (a + b * T2).normalized();
820  n2[n_solutions] =
821  (c + d * U2).normalized();
822  n3[n_solutions] =
823  (e + f * V2).normalized();
824  if (std::abs(n1[n_solutions].dot(
825  n2[n_solutions])) <
826  angle_eps &&
827  std::abs(n1[n_solutions].dot(
828  n3[n_solutions])) <
829  angle_eps &&
830  std::abs(n2[n_solutions].dot(
831  n3[n_solutions])) <
832  angle_eps)
833  ++n_solutions;
834  }
835  if (s < 1e-4 && n_solutions == 2) {
836  n_solutions = 1;
837  }
838  }
839  }
840  }
841 
842  for (int s = 0; s < n_solutions; ++s) {
843  const auto& hull_v_i =
844  hull_v[edges[edge_i].first];
845  const auto& hull_v_j =
846  hull_v[edges[edge_j].first];
847  const auto& hull_v_k =
848  hull_v[edges[edge_k].first];
849  const auto& n1_ = n1[s];
850  const auto& n2_ = n2[s];
851  const auto& n3_ = n3[s];
852 
853  // Compute the most extreme points in each
854  // direction.
855  double max_n1 = n1_.dot(hull_v_i);
856  double max_n2 = n2_.dot(hull_v_j);
857  double max_n3 = n3_.dot(hull_v_k);
858  double min_n1 =
860  double min_n2 =
862  double min_n3 =
864 
865  const auto& antipodal_i =
866  antipodal_points_for_edge[edge_i];
867  const auto& antipodal_j =
868  antipodal_points_for_edge[edge_j];
869  const auto& antipodal_k =
870  antipodal_points_for_edge[edge_k];
871 
872  // Determine the minimum projections along each
873  // axis over respective antipodal sets.
874  for (int v_idx : antipodal_i) {
875  min_n1 = std::min(min_n1,
876  n1_.dot(hull_v[v_idx]));
877  }
878  for (int v_idx : antipodal_j) {
879  min_n2 = std::min(min_n2,
880  n2_.dot(hull_v[v_idx]));
881  }
882  for (int v_idx : antipodal_k) {
883  min_n3 = std::min(min_n3,
884  n3_.dot(hull_v[v_idx]));
885  }
886 
887  // Compute volume based on extents in the three
888  // principal directions.
889  double extent0 = max_n1 - min_n1;
890  double extent1 = max_n2 - min_n2;
891  double extent2 = max_n3 - min_n3;
892  double volume = extent0 * extent1 * extent2;
893 
894  // Update the minimum oriented bounding box if a
895  // smaller volume is found.
896  if (volume < min_volume) {
897  // Update rotation matrix columns.
898  min_obb.R_.col(0) = n1_;
899  min_obb.R_.col(1) = n2_;
900  min_obb.R_.col(2) = n3_;
901 
902  // Update extents.
903  min_obb.extent_(0) = extent0;
904  min_obb.extent_(1) = extent1;
905  min_obb.extent_(2) = extent2;
906 
907  // Compute the center of the OBB using
908  // midpoints along each axis.
909  min_obb.center_ =
910  (min_n1 + 0.5 * extent0) * n1_ +
911  (min_n2 + 0.5 * extent1) * n2_ +
912  (min_n3 + 0.5 * extent2) * n3_;
913 
914  min_volume = volume;
915  }
916  }
917  }
918  }
919  }
920  }
921  }
922  }
923 
924  // --------------------------------------------------------------------
925  // 5) Test all configurations where two edges are on opposing faces,
926  // ,and the third one is on a face adjacent to the two.
927  // --------------------------------------------------------------------
928 
929  {
930  std::vector<int> antipodal_edges;
931  antipodal_edges.reserve(128);
932  std::vector<Eigen::Vector3d> antipodal_edge_normals;
933  antipodal_edge_normals.reserve(128);
934 
935  // Iterate over each edge_i in spatial_edge_order.
936  for (int edge_i : spatial_edge_order) {
937  // Cache face indices and normals for edge_i.
938  auto [face_i_a, face_i_b] = faces_for_edge[edge_i];
939  const Eigen::Vector3d& f1a = face_normals[face_i_a];
940  const Eigen::Vector3d& f1b = face_normals[face_i_b];
941 
942  antipodal_edges.clear();
943  antipodal_edge_normals.clear();
944 
945  // Iterate over vertices antipodal to edge_i.
946  const auto& antipodals_for_i = antipodal_points_for_edge[edge_i];
947  for (int antipodal_vertex : antipodals_for_i) {
948  const auto& neighbors = adjacency_data[antipodal_vertex];
949  for (int v_adj : neighbors) {
950  if (v_adj < antipodal_vertex) continue;
951 
952  int edgeIndex = antipodal_vertex * num_vertices + v_adj;
953  int edge = vertex_pairs_to_edges[edgeIndex];
954 
955  if (edge_i > edge) continue;
956  if (isInternalEdge(edge)) continue;
957 
958  auto [faceJ_a, faceJ_b] = faces_for_edge[edge];
959  const Eigen::Vector3d& f2a = face_normals[faceJ_a];
960  const Eigen::Vector3d& f2b = face_normals[faceJ_b];
961 
962  Eigen::Vector3d n;
963 
964  bool areCompatibleOpposingEdges = false;
965  constexpr double tooCloseToFaceEpsilon = 1e-4;
966 
967  Eigen::Matrix3d A;
968  A.col(0) = f2b;
969  A.col(1) = f1a - f1b;
970  A.col(2) = f2a - f2b;
971  Eigen::ColPivHouseholderQR<Eigen::Matrix3d> solver(A);
972  Eigen::Vector3d x = solver.solve(-f1b);
973  double c = x(0);
974  double t = x(1);
975  double cu = x(2);
976 
977  if (c <= 0.0 || t < 0.0 || t > 1.0) {
978  areCompatibleOpposingEdges = false;
979  } else {
980  double u = cu / c;
981  if (t < tooCloseToFaceEpsilon ||
982  t > 1.0 - tooCloseToFaceEpsilon ||
983  u < tooCloseToFaceEpsilon ||
984  u > 1.0 - tooCloseToFaceEpsilon) {
985  areCompatibleOpposingEdges = false;
986  } else {
987  if (cu < 0.0 || cu > c) {
988  areCompatibleOpposingEdges = false;
989  } else {
990  n = f1b + (f1a - f1b) * t;
991  areCompatibleOpposingEdges = true;
992  }
993  }
994  }
995 
996  if (areCompatibleOpposingEdges) {
997  antipodal_edges.push_back(edge);
998  antipodal_edge_normals.push_back(n.normalized());
999  }
1000  }
1001  }
1002 
1003  auto moveSign = [](double& dst, double& src) {
1004  if (src < 0.0) {
1005  dst = -dst;
1006  src = -src;
1007  }
1008  };
1009 
1010  const auto& compatible_edges_i = compatible_edges[edge_i];
1011  for (int edge_j : compatible_edges_i) {
1012  for (size_t k = 0; k < antipodal_edges.size(); ++k) {
1013  int edgeK = antipodal_edges[k];
1014 
1015  const Eigen::Vector3d& n1 = antipodal_edge_normals[k];
1016  double min_n1 = n1.dot(hull_v[edges[edgeK].first]);
1017  double max_n1 = n1.dot(hull_v[edges[edge_i].first]);
1018 
1019  // Test all mutual compatible edges.
1020  auto [faceK_a, faceK_b] = faces_for_edge[edge_j];
1021  const Eigen::Vector3d& f3a = face_normals[faceK_a];
1022  const Eigen::Vector3d& f3b = face_normals[faceK_b];
1023 
1024  double num = n1.dot(f3b);
1025  double den = n1.dot(f3b - f3a);
1026  moveSign(num, den);
1027 
1028  constexpr double epsilon = 1e-4;
1029  if (den < epsilon) {
1030  num = (std::abs(num) < 1e-4) ? 0.0 : -1.0;
1031  den = 1.0;
1032  }
1033 
1034  if (num >= den * -epsilon && num <= den * (1.0 + epsilon)) {
1035  double v = num / den;
1036  Eigen::Vector3d n3 =
1037  (f3b + (f3a - f3b) * v).normalized();
1038  Eigen::Vector3d n2 = n3.cross(n1).normalized();
1039 
1040  double max_n2, min_n2;
1041  clearGraphSearch();
1042  int hint = extremeVertexConvex(
1043  extremeVertexConvex, n2, flood_fill_visited,
1044  flood_fill_visit_color, max_n2,
1045  (k == 0) ? v_hint1 : v_hint1_b);
1046  if (k == 0) {
1047  v_hint1 = v_hint1_b = hint;
1048  } else {
1049  v_hint1_b = hint;
1050  }
1051 
1052  clearGraphSearch();
1053  hint = extremeVertexConvex(
1054  extremeVertexConvex, -n2, flood_fill_visited,
1055  flood_fill_visit_color, min_n2,
1056  (k == 0) ? v_hint2 : v_hint2_b);
1057  if (k == 0) {
1058  v_hint2 = v_hint2_b = hint;
1059  } else {
1060  v_hint2_b = hint;
1061  }
1062 
1063  min_n2 = -min_n2;
1064 
1065  double max_n3 = n3.dot(hull_v[edges[edge_j].first]);
1066  double min_n3 = std::numeric_limits<double>::infinity();
1067 
1068  // If there are very few antipodal vertices, do a
1069  // very tight loop and just iterate over each.
1070  const auto& antipodals_edge =
1071  antipodal_points_for_edge[edge_j];
1072  if (antipodals_edge.size() < 20) {
1073  for (int v_idx : antipodals_edge) {
1074  min_n3 =
1075  std::min(min_n3, n3.dot(hull_v[v_idx]));
1076  }
1077  } else {
1078  // Otherwise perform a spatial locality
1079  // exploiting graph search.
1080  clearGraphSearch();
1081  hint = extremeVertexConvex(
1082  extremeVertexConvex, -n3,
1083  flood_fill_visited, flood_fill_visit_color,
1084  min_n3, (k == 0) ? v_hint3 : v_hint3_b);
1085 
1086  if (k == 0) {
1087  v_hint3 = v_hint3_b = hint;
1088  } else {
1089  v_hint3_b = hint;
1090  }
1091 
1092  min_n3 = -min_n3;
1093  }
1094 
1095  double volume = (max_n1 - min_n1) * (max_n2 - min_n2) *
1096  (max_n3 - min_n3);
1097  if (volume < min_volume) {
1098  min_obb.R_.col(0) = n1;
1099  min_obb.R_.col(1) = n2;
1100  min_obb.R_.col(2) = n3;
1101  min_obb.extent_(0) = (max_n1 - min_n1);
1102  min_obb.extent_(1) = (max_n2 - min_n2);
1103  min_obb.extent_(2) = (max_n3 - min_n3);
1104  min_obb.center_ = 0.5 * ((min_n1 + max_n1) * n1 +
1105  (min_n2 + max_n2) * n2 +
1106  (min_n3 + max_n3) * n3);
1107  min_volume = volume;
1108  }
1109  }
1110  }
1111  }
1112  }
1113  }
1114 
1115  // --------------------------------------------------------------------
1116  // 6) Test all configurations where two edges are on the same face (OBB
1117  // aligns with a face of the convex hull)
1118  // --------------------------------------------------------------------
1119  {
1120  // Preallocate vectors to avoid frequent reallocations.
1121  std::vector<int> antipodal_edges;
1122  antipodal_edges.reserve(128);
1123  std::vector<Eigen::Vector3d> antipodal_edge_normals;
1124  antipodal_edge_normals.reserve(128);
1125 
1126  for (int face_idx : spatial_face_order) {
1127  const Eigen::Vector3d& n1 = face_normals[face_idx];
1128 
1129  // Find two edges on the face. Since we have flexibility to
1130  // choose from multiple edges of the same face, choose two that
1131  // are possibly most opposing to each other, in the hope that
1132  // their sets of sidepodal edges are most mutually exclusive as
1133  // possible, speeding up the search below.
1134  int e1 = -1;
1135  const auto& tri = hull_t[face_idx];
1136  int v0 = tri(2);
1137  for (int j = 0; j < 3; ++j) {
1138  int v1 = tri(j);
1139  int e = vertex_pairs_to_edges[v0 * num_vertices + v1];
1140  if (!isInternalEdge(e)) {
1141  e1 = e;
1142  break;
1143  }
1144  v0 = v1;
1145  }
1146 
1147  if (e1 == -1) continue;
1148 
1149  // Compute min_n1 either by scanning antipodal points or using
1150  // ExtremeVertexConvex.
1151  double max_n1 = n1.dot(hull_v[edges[e1].first]);
1152  double min_n1 = std::numeric_limits<double>::infinity();
1153  const auto& antipodals = antipodal_points_for_edge[e1];
1154  if (antipodals.size() < 20) {
1156  for (int v_idx : antipodals) {
1157  min_n1 = std::min(min_n1, n1.dot(hull_v[v_idx]));
1158  }
1159  } else {
1160  clearGraphSearch();
1161  v_hint4 = extremeVertexConvex(
1162  extremeVertexConvex, -n1, flood_fill_visited,
1163  flood_fill_visit_color, min_n1, v_hint4);
1164  min_n1 = -min_n1;
1165  }
1166 
1167  // Check edges compatible with e1.
1168  const auto& compatible_edges_i = compatible_edges[e1];
1169  for (int edge_k : compatible_edges_i) {
1170  auto [face_k_a, face_k_b] = faces_for_edge[edge_k];
1171  const Eigen::Vector3d& f3a = face_normals[face_k_a];
1172  const Eigen::Vector3d& f3b = face_normals[face_k_b];
1173 
1174  // Is edge3 compatible with direction n?
1175  double num = n1.dot(f3b);
1176  double den = n1.dot(f3b - f3a);
1177  double v;
1178  constexpr double epsilon = 1e-4;
1179  if (std::abs(den) >= epsilon) {
1180  v = num / den;
1181  } else {
1182  v = (std::abs(num) < epsilon) ? 0.0 : -1.0;
1183  }
1184 
1185  if (v >= -epsilon && v <= 1.0 + epsilon) {
1186  Eigen::Vector3d n3 = (f3b + (f3a - f3b) * v).normalized();
1187  Eigen::Vector3d n2 = n3.cross(n1).normalized();
1188 
1189  double max_n2, min_n2;
1190  clearGraphSearch();
1191  v_hint1 = extremeVertexConvex(
1192  extremeVertexConvex, n2, flood_fill_visited,
1193  flood_fill_visit_color, max_n2, v_hint1);
1194  clearGraphSearch();
1195  v_hint2 = extremeVertexConvex(
1196  extremeVertexConvex, -n2, flood_fill_visited,
1197  flood_fill_visit_color, min_n2, v_hint2);
1198  min_n2 = -min_n2;
1199 
1200  double max_n3 = n3.dot(hull_v[edges[edge_k].first]);
1201  double min_n3 = std::numeric_limits<double>::infinity();
1202 
1203  // If there are very few antipodal vertices, do a very
1204  // tight loop and just iterate over each.
1205  const auto& antipodals_edge =
1206  antipodal_points_for_edge[edge_k];
1207  if (antipodals_edge.size() < 20) {
1208  for (int v_idx : antipodals_edge) {
1209  min_n3 = std::min(min_n3, n3.dot(hull_v[v_idx]));
1210  }
1211  } else {
1212  clearGraphSearch();
1213  v_hint3 = extremeVertexConvex(
1214  extremeVertexConvex, -n3, flood_fill_visited,
1215  flood_fill_visit_color, min_n3, v_hint3);
1216  min_n3 = -min_n3;
1217  }
1218 
1219  double volume = (max_n1 - min_n1) * (max_n2 - min_n2) *
1220  (max_n3 - min_n3);
1221  if (volume < min_volume) {
1222  min_obb.R_.col(0) = n1;
1223  min_obb.R_.col(1) = n2;
1224  min_obb.R_.col(2) = n3;
1225  min_obb.extent_(0) = (max_n1 - min_n1);
1226  min_obb.extent_(1) = (max_n2 - min_n2);
1227  min_obb.extent_(2) = (max_n3 - min_n3);
1228  min_obb.center_ = 0.5 * ((min_n1 + max_n1) * n1 +
1229  (min_n2 + max_n2) * n2 +
1230  (min_n3 + max_n3) * n3);
1231  assert(volume > 0.0);
1232  min_volume = volume;
1233  }
1234  }
1235  }
1236  }
1237  }
1238 
1239  // Final check to ensure right-handed coordinate frame
1240  if (min_obb.R_.col(0).cross(min_obb.R_.col(1)).dot(min_obb.R_.col(2)) <
1241  0.0) {
1242  min_obb.R_.col(2) = -min_obb.R_.col(2);
1243  }
1244  mapOBBToClosestIdentity(min_obb);
1245  return static_cast<OrientedBoundingBox>(min_obb).To(points_.GetDevice());
1246 }
1247 
1249  bool robust) {
1251  if (points.GetShape(0) == 0) {
1252  utility::LogError("Input point set is empty.");
1253  return OrientedBoundingBox();
1254  }
1255  if (points.GetShape(0) < 4) {
1256  utility::LogError("Input point set has less than 4 points.");
1257  return OrientedBoundingBox();
1258  }
1259 
1260  // copy to cpu here
1261  PointCloud pcd(points.To(core::Device()));
1262  auto hull_mesh = pcd.ComputeConvexHull(robust);
1263  if (hull_mesh.GetVertexPositions().NumElements() == 0) {
1264  utility::LogError("Failed to compute convex hull.");
1265  return OrientedBoundingBox();
1266  }
1267 
1268  // Get convex hull vertices and triangles
1269  const std::vector<Eigen::Vector3d>& hull_v =
1271  hull_mesh.GetVertexPositions());
1272  const std::vector<Eigen::Vector3i>& hull_t =
1274  hull_mesh.GetTriangleIndices());
1275 
1276  OrientedBoundingBox min_box(
1278  hull_mesh.GetVertexPositions().To(core::Float32))
1280  double min_vol = min_box.Volume();
1281 
1282  PointCloud hull_pcd(hull_mesh.GetVertexPositions().Clone());
1283  for (auto& tri : hull_t) {
1284  hull_pcd.GetPointPositions().CopyFrom(hull_mesh.GetVertexPositions());
1285  Eigen::Vector3d a = hull_v[tri(0)];
1286  Eigen::Vector3d b = hull_v[tri(1)];
1287  Eigen::Vector3d c = hull_v[tri(2)];
1288  Eigen::Vector3d u = b - a;
1289  Eigen::Vector3d v = c - a;
1290  Eigen::Vector3d w = u.cross(v);
1291  v = w.cross(u);
1292  u = u / u.norm();
1293  v = v / v.norm();
1294  w = w / w.norm();
1295  Eigen::Matrix3d m_rot;
1296  m_rot << u[0], v[0], w[0], u[1], v[1], w[1], u[2], v[2], w[2];
1297  auto rot_inv =
1299  auto center =
1301  hull_pcd.Rotate(rot_inv, center);
1302 
1303  const auto aabox = hull_pcd.GetAxisAlignedBoundingBox();
1304  double volume = aabox.Volume();
1305  if (volume < min_vol) {
1306  min_vol = volume;
1307  min_box = aabox.GetOrientedBoundingBox();
1309  min_box.Rotate(rot, center);
1310  }
1311  }
1312  auto device = points.GetDevice();
1313  auto dtype = points.GetDtype();
1314  return OrientedBoundingBox(min_box.GetCenter().To(device, dtype),
1315  min_box.GetRotation().To(device, dtype),
1316  min_box.GetExtent().To(device, dtype));
1317 }
1318 
1319 } // namespace minimum_obb
1320 } // namespace kernel
1321 } // namespace geometry
1322 } // namespace t
1323 } // namespace cloudViewer
int points
Eigen::Matrix3d R_
Definition: MinimumOBB.cpp:49
Eigen::Vector3d center_
Definition: MinimumOBB.cpp:51
Eigen::Vector3d extent_
Definition: MinimumOBB.cpp:50
#define AssertTensorShape(tensor,...)
Definition: TensorCheck.h:61
void CopyFrom(const Tensor &other)
Copy Tensor values to current tensor from the source tensor.
Definition: Tensor.cpp:770
Device GetDevice() const override
Definition: Tensor.cpp:1435
Tensor Reshape(const SizeVector &dst_shape) const
Definition: Tensor.cpp:671
SizeVector GetShape() const
Definition: Tensor.h:1127
Tensor To(Dtype dtype, bool copy=false) const
Definition: Tensor.cpp:739
static AxisAlignedBoundingBox CreateFromPoints(const core::Tensor &points)
OrientedBoundingBox GetOrientedBoundingBox() const
Convert to an oriented box.
double Volume() const
Returns the volume of the bounding box.
A bounding box oriented along an arbitrary frame of reference.
OrientedBoundingBox & Rotate(const core::Tensor &rotation, const utility::optional< core::Tensor > &center=utility::nullopt)
Rotate the oriented box by the given rotation matrix. If the rotation matrix is not orthogonal,...
void SetExtent(const core::Tensor &extent)
Set the extent of the box. If the data type of the given tensor differs from the data type of the box...
void SetCenter(const core::Tensor &center)
Set the center of the box. If the data type of the given tensor differs from the data type of the box...
void SetRotation(const core::Tensor &rotation)
Set the rotation matrix of the box. If the data type of the given tensor differs from the data type o...
double Volume() const
Returns the volume of the bounding box.
A point cloud contains a list of 3D points.
Definition: PointCloud.h:82
PointCloud & Rotate(const core::Tensor &R, const core::Tensor &center)
Rotates the PointPositions and PointNormals (if exists).
Definition: PointCloud.cpp:206
TriangleMesh ComputeConvexHull(bool joggle_inputs=false) const
core::Tensor & GetPointPositions()
Get the value of the "positions" attribute. Convenience function.
Definition: PointCloud.h:124
AxisAlignedBoundingBox GetAxisAlignedBoundingBox() const
Create an axis-aligned bounding box from attribute "positions".
#define LogError(...)
Definition: Logging.h:60
__device__ __forceinline__ float infinity()
Definition: result_set.h:36
__host__ __device__ float3 cross(float3 a, float3 b)
Definition: cutil_math.h:1295
__host__ __device__ float dot(float2 a, float2 b)
Definition: cutil_math.h:1119
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
core::Tensor EigenMatrixToTensor(const Eigen::MatrixBase< Derived > &matrix)
Converts a eigen matrix of shape (M, N) with alignment A and type T to a tensor.
std::vector< Eigen::Vector3d > TensorToEigenVector3dVector(const core::Tensor &tensor)
Converts a tensor of shape (N, 3) to std::vector<Eigen::Vector3d>. An exception will be thrown if the...
std::vector< Eigen::Vector3i > TensorToEigenVector3iVector(const core::Tensor &tensor)
Converts a tensor of shape (N, 3) to std::vector<Eigen::Vector3i>. An exception will be thrown if the...
Eigen::Matrix< double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor > TensorToEigenMatrixXd(const core::Tensor &tensor)
Converts a 2D tensor to Eigen::MatrixXd of same shape. Regardless of the tensor dtype,...
CLOUDVIEWER_HOST_DEVICE Pair< First, Second > make_pair(const First &_first, const Second &_second)
Definition: SlabTraits.h:49
const Dtype Float32
Definition: Dtype.cpp:42
::ecvOrientedBBox OrientedBoundingBox
void To(const core::Tensor &src, core::Tensor &dst, double scale, double offset)
Definition: Image.cpp:17
OrientedBoundingBox ComputeMinimumOBBApprox(const core::Tensor &points, bool robust)
OrientedBoundingBox ComputeMinimumOBBJylanki(const core::Tensor &points_, bool robust)
Definition: MinimumOBB.cpp:55
constexpr nullopt_t nullopt
Definition: Optional.h:136
Generic file read and write utility for python interface.
Eigen::Matrix< Index, 3, 1 > Vector3i
Definition: knncpp.h:30
void swap(cloudViewer::core::SmallVectorImpl< T > &LHS, cloudViewer::core::SmallVectorImpl< T > &RHS)
Implement std::swap in terms of SmallVector swap.
Definition: SmallVector.h:1370