From 7c0acac0a50293c52d2adb70967e729f98fa5018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dmitri=20=C5=A0astin?= <54913410+dmitrishastin@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:41:33 +0000 Subject: [PATCH 1/5] List intersections (#6442) Add "ListIntersections" (C++) / "list_intersections" (python) function that returns a dictionary for all ray intersections present in a raycasting scene. --- cpp/open3d/t/geometry/RaycastingScene.cpp | 206 +++++++++++++++++- cpp/open3d/t/geometry/RaycastingScene.h | 27 +++ cpp/pybind/t/geometry/raycasting_scene.cpp | 98 ++++++++- .../test/t/geometry/test_raycasting_scene.py | 51 ++++- 4 files changed, 379 insertions(+), 3 deletions(-) diff --git a/cpp/open3d/t/geometry/RaycastingScene.cpp b/cpp/open3d/t/geometry/RaycastingScene.cpp index 412d6130862..af7dd3f1480 100644 --- a/cpp/open3d/t/geometry/RaycastingScene.cpp +++ b/cpp/open3d/t/geometry/RaycastingScene.cpp @@ -110,6 +110,75 @@ void CountIntersectionsFunc(const RTCFilterFunctionNArguments* args) { } } +struct ListIntersectionsContext { + RTCIntersectContext context; + std::vector>* + previous_geom_prim_ID_tfar; + unsigned int* ray_ids; + unsigned int* geometry_ids; + unsigned int* primitive_ids; + float* primitive_uvs; + float* t_hit; + Eigen::VectorXi cumsum; + unsigned int* track_intersections; +}; + +void ListIntersectionsFunc(const RTCFilterFunctionNArguments* args) { + int* valid = args->valid; + const ListIntersectionsContext* context = + reinterpret_cast(args->context); + struct RTCRayN* rayN = args->ray; + struct RTCHitN* hitN = args->hit; + const unsigned int N = args->N; + + // Avoid crashing when debug visualizations are used. + if (context == nullptr) return; + + std::vector>* + previous_geom_prim_ID_tfar = context->previous_geom_prim_ID_tfar; + unsigned int* ray_ids = context->ray_ids; + unsigned int* geometry_ids = context->geometry_ids; + unsigned int* primitive_ids = context->primitive_ids; + float* primitive_uvs = context->primitive_uvs; + float* t_hit = context->t_hit; + Eigen::VectorXi cumsum = context->cumsum; + unsigned int* track_intersections = context->track_intersections; + + // Iterate over all rays in ray packet. + for (unsigned int ui = 0; ui < N; ui += 1) { + // Calculate loop and execution mask + unsigned int vi = ui + 0; + if (vi >= N) continue; + + // Ignore inactive rays. + if (valid[vi] != -1) continue; + + // Read ray/hit from ray structure. + RTCRay ray = rtcGetRayFromRayN(rayN, N, ui); + RTCHit hit = rtcGetHitFromHitN(hitN, N, ui); + + unsigned int ray_id = ray.id; + std::tuple gpID(hit.geomID, hit.primID, + ray.tfar); + auto& prev_gpIDtfar = previous_geom_prim_ID_tfar->operator[](ray_id); + if (std::get<0>(prev_gpIDtfar) != hit.geomID || + (std::get<1>(prev_gpIDtfar) != hit.primID && + std::get<2>(prev_gpIDtfar) != ray.tfar)) { + size_t idx = cumsum[ray_id] + track_intersections[ray_id]; + ray_ids[idx] = ray_id; + geometry_ids[idx] = hit.geomID; + primitive_ids[idx] = hit.primID; + primitive_uvs[idx * 2 + 0] = hit.u; + primitive_uvs[idx * 2 + 1] = hit.v; + t_hit[idx] = ray.tfar; + previous_geom_prim_ID_tfar->operator[](ray_id) = gpID; + ++(track_intersections[ray_id]); + } + // Always ignore hit + valid[ui] = 0; + } +} + // Adapted from common/math/closest_point.h inline Vec3fa closestPointTriangle(Vec3fa const& p, Vec3fa const& a, @@ -482,6 +551,83 @@ struct RaycastingScene::Impl { } } + void ListIntersections(const float* const rays, + const size_t num_rays, + const size_t num_intersections, + const Eigen::VectorXi& cumsum, + unsigned int* track_intersections, + unsigned int* ray_ids, + unsigned int* geometry_ids, + unsigned int* primitive_ids, + float* primitive_uvs, + float* t_hit, + const int nthreads) { + CommitScene(); + + memset(track_intersections, 0, sizeof(uint32_t) * num_rays); + memset(ray_ids, 0, sizeof(uint32_t) * num_intersections); + memset(geometry_ids, 0, sizeof(uint32_t) * num_intersections); + memset(primitive_ids, 0, sizeof(uint32_t) * num_intersections); + memset(primitive_uvs, 0, sizeof(float) * num_intersections * 2); + memset(t_hit, 0, sizeof(float) * num_intersections); + + std::vector> + previous_geom_prim_ID_tfar( + num_rays, + std::make_tuple(uint32_t(RTC_INVALID_GEOMETRY_ID), + uint32_t(RTC_INVALID_GEOMETRY_ID), + 0.f)); + + ListIntersectionsContext context; + rtcInitIntersectContext(&context.context); + context.context.filter = ListIntersectionsFunc; + context.previous_geom_prim_ID_tfar = &previous_geom_prim_ID_tfar; + context.ray_ids = ray_ids; + context.geometry_ids = geometry_ids; + context.primitive_ids = primitive_ids; + context.primitive_uvs = primitive_uvs; + context.t_hit = t_hit; + context.cumsum = cumsum; + context.track_intersections = track_intersections; + + auto LoopFn = [&](const tbb::blocked_range& range) { + std::vector rayhits(range.size()); + + for (size_t i = range.begin(); i < range.end(); ++i) { + RTCRayHit* rh = &rayhits[i - range.begin()]; + const float* r = &rays[i * 6]; + rh->ray.org_x = r[0]; + rh->ray.org_y = r[1]; + rh->ray.org_z = r[2]; + rh->ray.dir_x = r[3]; + rh->ray.dir_y = r[4]; + rh->ray.dir_z = r[5]; + rh->ray.tnear = 0; + rh->ray.tfar = std::numeric_limits::infinity(); + rh->ray.mask = 0; + rh->ray.flags = 0; + rh->ray.id = i; + rh->hit.geomID = RTC_INVALID_GEOMETRY_ID; + rh->hit.instID[0] = RTC_INVALID_GEOMETRY_ID; + } + rtcIntersect1M(scene_, &context.context, &rayhits[0], range.size(), + sizeof(RTCRayHit)); + }; + + if (nthreads > 0) { + tbb::task_arena arena(nthreads); + arena.execute([&]() { + tbb::parallel_for( + tbb::blocked_range(0, num_rays, BATCH_SIZE), + LoopFn); + }); + } else { + tbb::parallel_for( + tbb::blocked_range(0, num_rays, BATCH_SIZE), + LoopFn); + } + } + void ComputeClosestPoints(const float* const query_points, const size_t num_query_points, float* closest_points, @@ -691,6 +837,64 @@ core::Tensor RaycastingScene::CountIntersections(const core::Tensor& rays, return intersections; } +std::unordered_map +RaycastingScene::ListIntersections(const core::Tensor& rays, + const int nthreads) { + AssertTensorDtypeLastDimDeviceMinNDim(rays, "rays", 6, + impl_->tensor_device_); + + auto shape = rays.GetShape(); + shape.pop_back(); // Remove last dim, we want to use this shape for the + // results. + size_t num_rays = shape.NumElements(); + + // determine total number of intersections + core::Tensor intersections(shape, core::Dtype::FromType()); + core::Tensor track_intersections(shape, core::Dtype::FromType()); + auto data = rays.Contiguous(); + impl_->CountIntersections(data.GetDataPtr(), num_rays, + intersections.GetDataPtr(), nthreads); + + // prepare shape with that number of elements + Eigen::Map intersections_vector( + intersections.GetDataPtr(), num_rays); + size_t num_intersections = intersections_vector.sum(); + + // prepare ray allocations (cumsum) + Eigen::VectorXi cumsum = Eigen::MatrixXi::Zero(num_rays, 1); + std::partial_sum(intersections_vector.begin(), + intersections_vector.end() - 1, cumsum.begin() + 1, + std::plus()); + + // generate results structure + std::unordered_map result; + shape.clear(); + shape.push_back(num_rays + 1); + result["ray_splits"] = core::Tensor(shape, core::UInt32); + uint32_t* ptr = result["ray_splits"].GetDataPtr(); + for (int i = 0; i < cumsum.size(); ++i) { + ptr[i] = cumsum[i]; + } + ptr[num_rays] = num_intersections; + shape[0] = intersections_vector.sum(); + result["ray_ids"] = core::Tensor(shape, core::UInt32); + result["geometry_ids"] = core::Tensor(shape, core::UInt32); + result["primitive_ids"] = core::Tensor(shape, core::UInt32); + result["t_hit"] = core::Tensor(shape, core::Float32); + shape.push_back(2); + result["primitive_uvs"] = core::Tensor(shape, core::Float32); + + impl_->ListIntersections(data.GetDataPtr(), num_rays, + num_intersections, cumsum, + track_intersections.GetDataPtr(), + result["ray_ids"].GetDataPtr(), + result["geometry_ids"].GetDataPtr(), + result["primitive_ids"].GetDataPtr(), + result["primitive_uvs"].GetDataPtr(), + result["t_hit"].GetDataPtr(), nthreads); + return result; +} + std::unordered_map RaycastingScene::ComputeClosestPoints(const core::Tensor& query_points, const int nthreads) { @@ -961,4 +1165,4 @@ uint32_t RaycastingScene::INVALID_ID() { return RTC_INVALID_GEOMETRY_ID; } } // namespace geometry } // namespace t -} // namespace open3d +} // namespace open3d \ No newline at end of file diff --git a/cpp/open3d/t/geometry/RaycastingScene.h b/cpp/open3d/t/geometry/RaycastingScene.h index b22639148f5..f25d994b0b5 100644 --- a/cpp/open3d/t/geometry/RaycastingScene.h +++ b/cpp/open3d/t/geometry/RaycastingScene.h @@ -104,6 +104,33 @@ class RaycastingScene { core::Tensor CountIntersections(const core::Tensor &rays, const int nthreads = 0); + /// \brief Lists the intersections of the rays with the scene + /// \param rays A tensor with >=2 dims, shape {.., 6}, and Dtype Float32 + /// describing the rays; {..} can be any number of dimensions. + /// The last dimension must be 6 and has the format [ox, oy, oz, dx, dy, dz] + /// with [ox,oy,oz] as the origin and [dx,dy,dz] as the direction. It is not + /// necessary to normalize the direction although it should be normalised if + /// t_hit is to be calculated in coordinate units. + /// \param nthreads The number of threads to use. Set to 0 for automatic. + /// \return The returned dictionary contains: /// + /// - \b ray_splits A tensor with ray intersection splits. Can be + /// used to iterate over all intersections for each ray. The shape + /// is {num_rays + 1}. + /// - \b ray_ids A tensor with ray IDs. The shape is + /// {num_intersections}. + /// - \b t_hit A tensor with the distance to the hit. The shape is + /// {num_intersections}. + /// - \b geometry_ids A tensor with the geometry IDs. The shape is + /// {num_intersections}. + /// - \b primitive_ids A tensor with the primitive IDs, which + /// corresponds to the triangle index. The shape is + /// {num_intersections}. + /// - \b primitive_uvs A tensor with the barycentric coordinates of + /// the intersection points within the triangles. The shape is + /// {num_intersections, 2}. + std::unordered_map ListIntersections( + const core::Tensor &rays, const int nthreads = 0); + /// \brief Computes the closest points on the surfaces of the scene. /// \param query_points A tensor with >=2 dims, shape {.., 3} and Dtype /// Float32 describing the query points. {..} can be any number of diff --git a/cpp/pybind/t/geometry/raycasting_scene.cpp b/cpp/pybind/t/geometry/raycasting_scene.cpp index bdab66e02e1..e7f6dcecbd5 100644 --- a/cpp/pybind/t/geometry/raycasting_scene.cpp +++ b/cpp/pybind/t/geometry/raycasting_scene.cpp @@ -177,6 +177,102 @@ Computes the number of intersection of the rays with the scene. Returns: A tensor with the number of intersections. The shape is {..}. +)doc"); + + raycasting_scene.def("list_intersections", + &RaycastingScene::ListIntersections, "rays"_a, + "nthreads"_a = 0, R"doc( +Lists the intersections of the rays with the scene:: + + import open3d as o3d + import numpy as np + + # Create scene and add the monkey model. + scene = o3d.t.geometry.RaycastingScene() + d = o3d.data.MonkeyModel() + mesh = o3d.t.io.read_triangle_mesh(d.path) + mesh_id = scene.add_triangles(mesh) + + # Create a grid of rays covering the bounding box + bb_min = mesh.vertex['positions'].min(dim=0).numpy() + bb_max = mesh.vertex['positions'].max(dim=0).numpy() + x,y = np.linspace(bb_min, bb_max, num=10)[:,:2].T + xv, yv = np.meshgrid(x,y) + orig = np.stack([xv, yv, np.full_like(xv, bb_min[2]-1)], axis=-1).reshape(-1,3) + dest = orig + np.full(orig.shape, (0,0,2+bb_max[2]-bb_min[2]),dtype=np.float32) + rays = np.concatenate([orig, dest-orig], axis=-1).astype(np.float32) + + # Compute the ray intersections. + lx = scene.list_intersections(rays) + lx = {k:v.numpy() for k,v in lx.items()} + + # Calculate intersection coordinates using the primitive uvs and the mesh + v = mesh.vertex['positions'].numpy() + t = mesh.triangle['indices'].numpy() + tidx = lx['primitive_ids'] + uv = lx['primitive_uvs'] + w = 1 - np.sum(uv, axis=1) + c = \ + v[t[tidx, 1].flatten(), :] * uv[:, 0][:, None] + \ + v[t[tidx, 2].flatten(), :] * uv[:, 1][:, None] + \ + v[t[tidx, 0].flatten(), :] * w[:, None] + + # Calculate intersection coordinates using ray_ids + c = rays[lx['ray_ids']][:,:3] + rays[lx['ray_ids']][:,3:]*lx['t_hit'][...,None] + + # Visualize the rays and intersections. + lines = o3d.t.geometry.LineSet() + lines.point.positions = np.hstack([orig,dest]).reshape(-1,3) + lines.line.indices = np.arange(lines.point.positions.shape[0]).reshape(-1,2) + lines.line.colors = np.full((lines.line.indices.shape[0],3), (1,0,0)) + x = o3d.t.geometry.PointCloud(positions=c) + o3d.visualization.draw([mesh, lines, x], point_size=8) + + +Args: + rays (open3d.core.Tensor): A tensor with >=2 dims, shape {.., 6}, and Dtype + Float32 describing the rays; {..} can be any number of dimensions. + The last dimension must be 6 and has the format [ox, oy, oz, dx, dy, dz] + with [ox,oy,oz] as the origin and [dx,dy,dz] as the direction. It is not + necessary to normalize the direction although it should be normalised if + t_hit is to be calculated in coordinate units. + + nthreads (int): The number of threads to use. Set to 0 for automatic. + +Returns: + The returned dictionary contains + + ray_splits + A tensor with ray intersection splits. Can be used to iterate over all intersections for each ray. The shape is {num_rays + 1}. + + ray_ids + A tensor with ray IDs. The shape is {num_intersections}. + + t_hit + A tensor with the distance to the hit. The shape is {num_intersections}. + + geometry_ids + A tensor with the geometry IDs. The shape is {num_intersections}. + + primitive_ids + A tensor with the primitive IDs, which corresponds to the triangle + index. The shape is {num_intersections}. + + primitive_uvs + A tensor with the barycentric coordinates of the intersection points within + the triangles. The shape is {num_intersections, 2}. + + +An example of using ray_splits:: + + ray_splits: [0, 2, 3, 6, 6, 8] # note that the length of this is num_rays+1 + t_hit: [t1, t2, t3, t4, t5, t6, t7, t8] + + for ray_id, (start, end) in enumerate(zip(ray_splits[:-1], ray_splits[1:])): + for i,t in enumerate(t_hit[start:end]): + print(f'ray {ray_id}, intersection {i} at {t}') + + )doc"); raycasting_scene.def("compute_closest_points", @@ -350,4 +446,4 @@ The value for invalid IDs } } // namespace geometry } // namespace t -} // namespace open3d +} // namespace open3d \ No newline at end of file diff --git a/python/test/t/geometry/test_raycasting_scene.py b/python/test/t/geometry/test_raycasting_scene.py index f89bb0b6644..3ce024a2b29 100644 --- a/python/test/t/geometry/test_raycasting_scene.py +++ b/python/test/t/geometry/test_raycasting_scene.py @@ -137,6 +137,39 @@ def test_count_lots_of_intersections(): _ = scene.count_intersections(rays) +def test_list_intersections(): + cube = o3d.t.geometry.TriangleMesh.from_legacy( + o3d.geometry.TriangleMesh.create_box()) + + scene = o3d.t.geometry.RaycastingScene() + scene.add_triangles(cube) + + rays = o3d.core.Tensor([[0.5, 0.5, -1, 0, 0, 1], [0.5, 0.5, 0.5, 0, 0, 1], + [10, 10, 10, 1, 0, 0]], + dtype=o3d.core.float32) + ans = scene.list_intersections(rays) + + np.testing.assert_allclose(ans['t_hit'].numpy(), + np.array([1.0, 2.0, 0.5]), + rtol=1e-6, + atol=1e-6) + + +# list lots of random ray intersections to test the internal batching +# we expect no errors for this test +def test_list_lots_of_intersections(): + cube = o3d.t.geometry.TriangleMesh.from_legacy( + o3d.geometry.TriangleMesh.create_box()) + + scene = o3d.t.geometry.RaycastingScene() + scene.add_triangles(cube) + + rs = np.random.RandomState(123) + rays = o3d.core.Tensor.from_numpy(rs.rand(123456, 6).astype(np.float32)) + + _ = scene.list_intersections(rays) + + def test_compute_closest_points(): vertices = o3d.core.Tensor([[0, 0, 0], [1, 0, 0], [1, 1, 0]], dtype=o3d.core.float32) @@ -248,7 +281,9 @@ def test_output_shapes(shape): 'primitive_ids': [], 'primitive_uvs': [2], 'primitive_normals': [3], - 'points': [3] + 'points': [3], + 'ray_ids': [], + 'ray_splits': [] } ans = scene.cast_rays(rays) @@ -267,6 +302,20 @@ def test_output_shapes(shape): ) == expected_shape, 'shape mismatch: expected {} but got {} for {}'.format( expected_shape, list(v.shape), k) + ans = scene.list_intersections(rays) + nx = np.sum(scene.count_intersections(rays).numpy()).tolist() + for k, v in ans.items(): + if k == 'ray_splits': + alt_shape = [np.prod(rays.shape[:-1]) + 1] + else: + alt_shape = [nx] + #use np.append otherwise issues if alt_shape = [0] and last_dim[k] = [] + expected_shape = np.append(alt_shape, last_dim[k]).tolist() + assert list( + v.shape + ) == expected_shape, 'shape mismatch: expected {} but got {} for {}'.format( + expected_shape, list(v.shape), k) + def test_sphere_wrong_occupancy(): # This test checks a specific scenario where the old implementation From 1585e9686ed8e6aa9c423c9fbafb74628a7f0f63 Mon Sep 17 00:00:00 2001 From: nsaiapova Date: Tue, 14 Nov 2023 00:01:39 +0100 Subject: [PATCH 2/5] Implement open3d::t::geometry::TriangleMesh::SelectByIndex (#6415) * Define a helper DISPATCH_INT_DTYPE_PREFIX_TO_TEMPLATE This is a helper to call a templated function with an integer argument, based on Dtype. As a second argument, it takes a suffix, used to build a unique type name. This way, we can use it to call a function with more than one integer argument. Example: DISPATCH_INT_DTYPE_PREFIX_TO_TEMPLATE(core::Dtype::Int32, int32, [&]() { DISPATCH_INT_DTYPE_PREFIX_TO_TEMPLATE(core::Dtype::UInt64, uint64, [&]() { scalar_int32_t a; scalar_uint64_t b; // ... }); * Implement open3d::t::geometry::TriangleMesh::SelectByIndex The method takes a list of indices and returns a new mesh built with the selected vertices and triangles formed by these vertices. The indices type can be any integral type. The algorithm is implemented on CPU only. The implementation is inspired by open3d::geometry::TriangleMesh::SelectByIndex. and by open3d::t::geometry::TriangleMesh::SelectFacesByMask. We first compute a mask of vertices to be selected. If the input index exceeds the maximum number of vertices or is negative, we ignore the index and print a warning. If the mesh has triangles, we build tringle mask and select needed triangles. The next step is to update triangle indices to a new ones. It is similar to SelectFacesByMask, so I introduced a static helper to do that. Based on the vertex mask we build a mapping index vector using inclusive prefix sum algorithm and use it as a map between old and new indices. We select the vertices by mask and build the resulting mesh from the selected vertices and triangles. Copying the mesh attributes is again similar to SelectFacesByMask, so I put it to a separate static function. * Refactor t::geometry::TriangleMesh::SelectFacesByMask * Add error handling on empty mesh * Use DISPATCH_INT_DTYPE_PREFIX_TO_TEMPLATE instead of a conditional branch * Use UpdateTriangleIndicesByVertexMask helper to update triangle indices * Use CopyAttributesByMask helper to copy the mesh attributes * Add tests --- cpp/open3d/core/Dispatch.h | 31 +++ cpp/open3d/t/geometry/TriangleMesh.cpp | 235 ++++++++++++++---- cpp/open3d/t/geometry/TriangleMesh.h | 13 +- cpp/pybind/t/geometry/trianglemesh.cpp | 26 +- cpp/tests/t/geometry/TriangleMesh.cpp | 256 ++++++++++++++++++++ python/test/t/geometry/test_trianglemesh.py | 222 +++++++++++++++++ 6 files changed, 739 insertions(+), 44 deletions(-) diff --git a/cpp/open3d/core/Dispatch.h b/cpp/open3d/core/Dispatch.h index 466ccf86beb..fe658c8cf15 100644 --- a/cpp/open3d/core/Dispatch.h +++ b/cpp/open3d/core/Dispatch.h @@ -113,3 +113,34 @@ open3d::utility::LogError("Unsupported data type."); \ } \ }() + +#define DISPATCH_INT_DTYPE_PREFIX_TO_TEMPLATE(DTYPE, PREFIX, ...) \ + [&] { \ + if (DTYPE == open3d::core::Int8) { \ + using scalar_##PREFIX##_t = int8_t; \ + return __VA_ARGS__(); \ + } else if (DTYPE == open3d::core::Int16) { \ + using scalar_##PREFIX##_t = int16_t; \ + return __VA_ARGS__(); \ + } else if (DTYPE == open3d::core::Int32) { \ + using scalar_##PREFIX##_t = int32_t; \ + return __VA_ARGS__(); \ + } else if (DTYPE == open3d::core::Int64) { \ + using scalar_##PREFIX##_t = int64_t; \ + return __VA_ARGS__(); \ + } else if (DTYPE == open3d::core::UInt8) { \ + using scalar_##PREFIX##_t = uint8_t; \ + return __VA_ARGS__(); \ + } else if (DTYPE == open3d::core::UInt16) { \ + using scalar_##PREFIX##_t = uint16_t; \ + return __VA_ARGS__(); \ + } else if (DTYPE == open3d::core::UInt32) { \ + using scalar_##PREFIX##_t = uint32_t; \ + return __VA_ARGS__(); \ + } else if (DTYPE == open3d::core::UInt64) { \ + using scalar_##PREFIX##_t = uint64_t; \ + return __VA_ARGS__(); \ + } else { \ + open3d::utility::LogError("Unsupported data type."); \ + } \ + }() diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index a87abf04eb5..e4e878af93a 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -995,7 +995,66 @@ int TriangleMesh::PCAPartition(int max_faces) { return num_parititions; } +/// A helper to compute new vertex indices out of vertex mask. +/// \param tris_cpu tensor with triangle indices to update. +/// \param vertex_mask tensor with the mask for vertices. +template +static void UpdateTriangleIndicesByVertexMask(core::Tensor &tris_cpu, + const core::Tensor &vertex_mask) { + int64_t num_verts = vertex_mask.GetLength(); + int64_t num_tris = tris_cpu.GetLength(); + const T *vertex_mask_ptr = vertex_mask.GetDataPtr(); + std::vector prefix_sum(num_verts + 1, 0); + utility::InclusivePrefixSum(vertex_mask_ptr, vertex_mask_ptr + num_verts, + &prefix_sum[1]); + + // update triangle indices + T *vert_idx_ptr = tris_cpu.GetDataPtr(); + for (int64_t i = 0; i < num_tris * 3; ++i) { + vert_idx_ptr[i] = prefix_sum[vert_idx_ptr[i]]; + } +} + +/// A helper to copy mesh attributes. +/// \param dst destination mesh +/// \param src source mesh +/// \param vertex_mask vertex mask of the source mesh +/// \param tri_mask triangle mask of the source mesh +static void CopyAttributesByMasks(TriangleMesh &dst, + const TriangleMesh &src, + const core::Tensor &vertex_mask, + const core::Tensor &tri_mask) { + if (src.HasVertexPositions() && dst.HasVertexPositions()) { + for (auto item : src.GetVertexAttr()) { + if (!dst.HasVertexAttr(item.first)) { + dst.SetVertexAttr(item.first, + item.second.IndexGet({vertex_mask})); + } + } + } + + if (src.HasTriangleIndices() && dst.HasTriangleIndices()) { + for (auto item : src.GetTriangleAttr()) { + if (!dst.HasTriangleAttr(item.first)) { + dst.SetTriangleAttr(item.first, + item.second.IndexGet({tri_mask})); + } + } + } +} + TriangleMesh TriangleMesh::SelectFacesByMask(const core::Tensor &mask) const { + if (!HasVertexPositions()) { + utility::LogWarning( + "[SelectFacesByMask] mesh has no vertex positions."); + return {}; + } + if (!HasTriangleIndices()) { + utility::LogWarning( + "[SelectFacesByMask] mesh has no triangle indices."); + return {}; + } + core::AssertTensorShape(mask, {GetTriangleIndices().GetLength()}); core::AssertTensorDtype(mask, core::Bool); GetTriangleAttr().AssertSizeSynchronized(); @@ -1004,62 +1063,154 @@ TriangleMesh TriangleMesh::SelectFacesByMask(const core::Tensor &mask) const { // select triangles core::Tensor tris = GetTriangleIndices().IndexGet({mask}); core::Tensor tris_cpu = tris.To(core::Device()).Contiguous(); - const int64_t num_tris = tris_cpu.GetLength(); // create mask for vertices that are part of the selected faces const int64_t num_verts = GetVertexPositions().GetLength(); - core::Tensor vertex_mask = core::Tensor::Zeros({num_verts}, core::Int32); - std::vector prefix_sum(num_verts + 1, 0); - { - int32_t *vertex_mask_ptr = vertex_mask.GetDataPtr(); - if (tris_cpu.GetDtype() == core::Int32) { - int32_t *vert_idx_ptr = tris_cpu.GetDataPtr(); - for (int64_t i = 0; i < tris_cpu.GetLength() * 3; ++i) { - vertex_mask_ptr[vert_idx_ptr[i]] = 1; - } - } else { - int64_t *vert_idx_ptr = tris_cpu.GetDataPtr(); - for (int64_t i = 0; i < tris_cpu.GetLength() * 3; ++i) { - vertex_mask_ptr[vert_idx_ptr[i]] = 1; - } - } - utility::InclusivePrefixSum( - vertex_mask_ptr, vertex_mask_ptr + num_verts, &prefix_sum[1]); - } - - // update triangle indices - if (tris_cpu.GetDtype() == core::Int32) { - int32_t *vert_idx_ptr = tris_cpu.GetDataPtr(); + // empty tensor to further construct the vertex mask + core::Tensor vertex_mask; + + DISPATCH_INT_DTYPE_PREFIX_TO_TEMPLATE(tris_cpu.GetDtype(), tris, [&]() { + vertex_mask = core::Tensor::Zeros( + {num_verts}, core::Dtype::FromType()); + const int64_t num_tris = tris_cpu.GetLength(); + scalar_tris_t *vertex_mask_ptr = + vertex_mask.GetDataPtr(); + scalar_tris_t *vert_idx_ptr = tris_cpu.GetDataPtr(); + // mask for the vertices, which are used in the triangles for (int64_t i = 0; i < num_tris * 3; ++i) { - int64_t new_idx = prefix_sum[vert_idx_ptr[i]]; - vert_idx_ptr[i] = int32_t(new_idx); + vertex_mask_ptr[vert_idx_ptr[i]] = 1; } - } else { - int64_t *vert_idx_ptr = tris_cpu.GetDataPtr(); - for (int64_t i = 0; i < num_tris * 3; ++i) { - int64_t new_idx = prefix_sum[vert_idx_ptr[i]]; - vert_idx_ptr[i] = new_idx; - } - } + UpdateTriangleIndicesByVertexMask(tris_cpu, vertex_mask); + }); tris = tris_cpu.To(GetDevice()); vertex_mask = vertex_mask.To(GetDevice(), core::Bool); core::Tensor verts = GetVertexPositions().IndexGet({vertex_mask}); TriangleMesh result(verts, tris); - // copy attributes - for (auto item : GetVertexAttr()) { - if (!result.HasVertexAttr(item.first)) { - result.SetVertexAttr(item.first, - item.second.IndexGet({vertex_mask})); - } + CopyAttributesByMasks(result, *this, vertex_mask, mask); + + return result; +} + +/// brief Static negative checker for signed integer types +template ::value && + !std::is_same::value && + std::is_signed::value, + T>::type * = nullptr> +static bool IsNegative(T val) { + return val < 0; +} + +/// brief Overloaded static negative checker for unsigned integer types. +/// It unconditionally returns false, but we need it for template functions. +template ::value && + !std::is_same::value && + !std::is_signed::value, + T>::type * = nullptr> +static bool IsNegative(T val) { + return false; +} + +TriangleMesh TriangleMesh::SelectByIndex(const core::Tensor &indices) const { + TriangleMesh result; + core::AssertTensorShape(indices, {indices.GetLength()}); + if (!HasVertexPositions()) { + utility::LogWarning("[SelectByIndex] TriangleMesh has no vertices."); + return result; } - for (auto item : GetTriangleAttr()) { - if (!result.HasTriangleAttr(item.first)) { - result.SetTriangleAttr(item.first, item.second.IndexGet({mask})); - } + GetVertexAttr().AssertSizeSynchronized(); + + // we allow indices of an integral type only + core::Dtype::DtypeCode indices_dtype_code = + indices.GetDtype().GetDtypeCode(); + if (indices_dtype_code != core::Dtype::DtypeCode::Int && + indices_dtype_code != core::Dtype::DtypeCode::UInt) { + utility::LogError( + "[SelectByIndex] indices are not of integral type {}.", + indices.GetDtype().ToString()); + } + core::Tensor indices_cpu = indices.To(core::Device()).Contiguous(); + core::Tensor tris_cpu, tri_mask; + core::Dtype tri_dtype; + if (HasTriangleIndices()) { + GetTriangleAttr().AssertSizeSynchronized(); + tris_cpu = GetTriangleIndices().To(core::Device()).Contiguous(); + // bool mask for triangles. + tri_mask = core::Tensor::Zeros({tris_cpu.GetLength()}, core::Bool); + tri_dtype = tris_cpu.GetDtype(); + } else { + utility::LogWarning("TriangleMesh has no triangle indices."); + tri_dtype = core::Int64; + } + + // int mask to select vertices for the new mesh. We need it as int as we + // will use its values to sum up and get the map of new indices + core::Tensor vertex_mask = + core::Tensor::Zeros({GetVertexPositions().GetLength()}, tri_dtype); + + DISPATCH_INT_DTYPE_PREFIX_TO_TEMPLATE(tri_dtype, tris, [&]() { + DISPATCH_INT_DTYPE_PREFIX_TO_TEMPLATE( + indices_cpu.GetDtype(), indices, [&]() { + const int64_t num_tris = tris_cpu.GetLength(); + const int64_t num_verts = vertex_mask.GetLength(); + + // compute the vertices mask + scalar_tris_t *vertex_mask_ptr = + vertex_mask.GetDataPtr(); + const scalar_indices_t *indices_ptr = + indices.GetDataPtr(); + for (int64_t i = 0; i < indices.GetLength(); ++i) { + if (IsNegative(indices_ptr[i]) || + indices_ptr[i] >= + static_cast(num_verts)) { + utility::LogWarning( + "[SelectByIndex] indices contains index {} " + "out of range. " + "It is ignored.", + indices_ptr[i]); + } + vertex_mask_ptr[indices_ptr[i]] = 1; + } + + if (tri_mask.GetDtype() == core::Undefined) { + // we don't need to compute triangles, if there are none + return; + } + + // Build the triangle mask + scalar_tris_t *tris_cpu_ptr = + tris_cpu.GetDataPtr(); + bool *tri_mask_ptr = tri_mask.GetDataPtr(); + for (int64_t i = 0; i < num_tris; ++i) { + if (vertex_mask_ptr[tris_cpu_ptr[3 * i]] == 1 && + vertex_mask_ptr[tris_cpu_ptr[3 * i + 1]] == 1 && + vertex_mask_ptr[tris_cpu_ptr[3 * i + 2]] == 1) { + tri_mask_ptr[i] = true; + } + } + // select only needed triangles + tris_cpu = tris_cpu.IndexGet({tri_mask}); + // update the triangle indices + UpdateTriangleIndicesByVertexMask( + tris_cpu, vertex_mask); + }); + }); + + // send the vertex mask to original device and apply to vertices + vertex_mask = vertex_mask.To(GetDevice(), core::Bool); + core::Tensor new_vertices = GetVertexPositions().IndexGet({vertex_mask}); + result.SetVertexPositions(new_vertices); + + if (HasTriangleIndices()) { + // select triangles and send the selected ones to the original device + result.SetTriangleIndices(tris_cpu.To(GetDevice())); } + CopyAttributesByMasks(result, *this, vertex_mask, tri_mask); + return result; } diff --git a/cpp/open3d/t/geometry/TriangleMesh.h b/cpp/open3d/t/geometry/TriangleMesh.h index 7828ac16b02..8f7373afd9d 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.h +++ b/cpp/open3d/t/geometry/TriangleMesh.h @@ -927,9 +927,20 @@ class TriangleMesh : public Geometry, public DrawableGeometry { /// Returns a new mesh with the faces selected by a boolean mask. /// \param mask A boolean mask with the shape (N) with N as the number of /// faces in the mesh. - /// \return A new mesh with the selected faces. + /// \return A new mesh with the selected faces. If the original mesh is + /// empty, return an empty mesh. TriangleMesh SelectFacesByMask(const core::Tensor &mask) const; + /// Returns a new mesh with the vertices selected by a vector of indices. + /// If an item from the indices list exceeds the max vertex number of + /// the mesh or has a negative value, it is ignored. + /// \param indices An integer list of indices. Duplicates are + /// allowed, but ignored. Signed and unsigned integral types are allowed. + /// \return A new mesh with the selected vertices and faces built + /// from the selected vertices. If the original mesh is empty, return + /// an empty mesh. + TriangleMesh SelectByIndex(const core::Tensor &indices) const; + protected: core::Device device_ = core::Device("CPU:0"); TensorMap vertex_attr_; diff --git a/cpp/pybind/t/geometry/trianglemesh.cpp b/cpp/pybind/t/geometry/trianglemesh.cpp index 5979238e1b2..06cacf404a5 100644 --- a/cpp/pybind/t/geometry/trianglemesh.cpp +++ b/cpp/pybind/t/geometry/trianglemesh.cpp @@ -901,7 +901,7 @@ the partition id for each face. number of faces in the mesh. Returns: - A new mesh with the selected faces. + A new mesh with the selected faces. If the original mesh is empty, return an empty mesh. Example: @@ -923,6 +923,30 @@ the partition id for each face. o3d.visualization.draw(parts) +)"); + + triangle_mesh.def( + "select_by_index", &TriangleMesh::SelectByIndex, "indices"_a, + R"(Returns a new mesh with the vertices selected according to the indices list. +If an item from the indices list exceeds the max vertex number of the mesh +or has a negative value, it is ignored. + +Args: + indices (open3d.core.Tensor): An integer list of indices. Duplicates are + allowed, but ignored. Signed and unsigned integral types are accepted. + +Returns: + A new mesh with the selected vertices and faces built from these vertices. + If the original mesh is empty, return an empty mesh. + +Example: + + This code selects the top face of a box, which has indices [2, 3, 6, 7]:: + + import open3d as o3d + import numpy as np + box = o3d.t.geometry.TriangleMesh.create_box() + top_face = box.select_by_index([2, 3, 6, 7]) )"); } diff --git a/cpp/tests/t/geometry/TriangleMesh.cpp b/cpp/tests/t/geometry/TriangleMesh.cpp index 520848a5032..8cb6ba0ddac 100644 --- a/cpp/tests/t/geometry/TriangleMesh.cpp +++ b/cpp/tests/t/geometry/TriangleMesh.cpp @@ -946,5 +946,261 @@ TEST_P(TriangleMeshPermuteDevices, CreateMobius) { triangle_indices_custom)); } +TEST_P(TriangleMeshPermuteDevices, SelectFacesByMask) { + // check that an exception is thrown if the mesh is empty + t::geometry::TriangleMesh mesh_empty; + core::Tensor mask_empty = + core::Tensor::Zeros({12}, core::Bool, mesh_empty.GetDevice()); + core::Tensor mask_full = + core::Tensor::Ones({12}, core::Bool, mesh_empty.GetDevice()); + + // check completely empty mesh + EXPECT_TRUE(mesh_empty.SelectFacesByMask(mask_empty).IsEmpty()); + EXPECT_TRUE(mesh_empty.SelectFacesByMask(mask_full).IsEmpty()); + + // check mesh w/o triangles + core::Tensor cpu_vertices = + core::Tensor::Ones({2, 3}, core::Float32, mesh_empty.GetDevice()); + mesh_empty.SetVertexPositions(cpu_vertices); + EXPECT_TRUE(mesh_empty.SelectFacesByMask(mask_empty).IsEmpty()); + EXPECT_TRUE(mesh_empty.SelectFacesByMask(mask_full).IsEmpty()); + + // create box with normals, colors and labels defined. + t::geometry::TriangleMesh box = t::geometry::TriangleMesh::CreateBox(); + core::Tensor vertex_colors = core::Tensor::Init({{0.0, 0.0, 0.0}, + {1.0, 1.0, 1.0}, + {2.0, 2.0, 2.0}, + {3.0, 3.0, 3.0}, + {4.0, 4.0, 4.0}, + {5.0, 5.0, 5.0}, + {6.0, 6.0, 6.0}, + {7.0, 7.0, 7.0}}); + ; + core::Tensor vertex_labels = core::Tensor::Init({{0.0, 0.0, 0.0}, + {1.0, 1.0, 1.0}, + {2.0, 2.0, 2.0}, + {3.0, 3.0, 3.0}, + {4.0, 4.0, 4.0}, + {5.0, 5.0, 5.0}, + {6.0, 6.0, 6.0}, + {7.0, 7.0, 7.0}}) * + 10; + ; + core::Tensor triangle_labels = + core::Tensor::Init({{0.0, 0.0, 0.0}, + {1.0, 1.0, 1.0}, + {2.0, 2.0, 2.0}, + {3.0, 3.0, 3.0}, + {4.0, 4.0, 4.0}, + {5.0, 5.0, 5.0}, + {6.0, 6.0, 6.0}, + {7.0, 7.0, 7.0}, + {8.0, 8.0, 8.0}, + {9.0, 9.0, 9.0}, + {10.0, 10.0, 10.0}, + {11.0, 11.0, 11.0}}) * + 100; + box.SetVertexColors(vertex_colors); + box.SetVertexAttr("labels", vertex_labels); + box.ComputeTriangleNormals(); + box.SetTriangleAttr("labels", triangle_labels); + + // empty index list + EXPECT_TRUE(box.SelectFacesByMask(mask_empty).IsEmpty()); + + // set the expected value + core::Tensor expected_verts = core::Tensor::Init({{0.0, 0.0, 1.0}, + {1.0, 0.0, 1.0}, + {0.0, 1.0, 1.0}, + {1.0, 1.0, 1.0}}); + core::Tensor expected_vert_colors = + core::Tensor::Init({{2.0, 2.0, 2.0}, + {3.0, 3.0, 3.0}, + {6.0, 6.0, 6.0}, + {7.0, 7.0, 7.0}}); + core::Tensor expected_vert_labels = + core::Tensor::Init({{20.0, 20.0, 20.0}, + {30.0, 30.0, 30.0}, + {60.0, 60.0, 60.0}, + {70.0, 70.0, 70.0}}); + core::Tensor expected_tris = + core::Tensor::Init({{0, 1, 3}, {0, 3, 2}}); + core::Tensor tris_mask = + core::Tensor::Init({0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0}); + core::Tensor expected_tri_normals = + box.GetTriangleNormals().IndexGet({tris_mask}); + core::Tensor expected_tri_labels = core::Tensor::Init( + {{800.0, 800.0, 800.0}, {900.0, 900.0, 900.0}}); + + // check basic case + t::geometry::TriangleMesh selected = box.SelectFacesByMask(tris_mask); + + EXPECT_TRUE(selected.GetVertexPositions().AllClose(expected_verts)); + EXPECT_TRUE(selected.GetVertexColors().AllClose(expected_vert_colors)); + EXPECT_TRUE( + selected.GetVertexAttr("labels").AllClose(expected_vert_labels)); + EXPECT_TRUE(selected.GetTriangleIndices().AllClose(expected_tris)); + EXPECT_TRUE(selected.GetTriangleNormals().AllClose(expected_tri_normals)); + EXPECT_TRUE( + selected.GetTriangleAttr("labels").AllClose(expected_tri_labels)); + + // Check that initial mesh is unchanged. + t::geometry::TriangleMesh box_untouched = + t::geometry::TriangleMesh::CreateBox(); + EXPECT_TRUE(box.GetVertexPositions().AllClose( + box_untouched.GetVertexPositions())); + EXPECT_TRUE(box.GetTriangleIndices().AllClose( + box_untouched.GetTriangleIndices())); +} + +TEST_P(TriangleMeshPermuteDevices, SelectByIndex) { + // check that an exception is thrown if the mesh is empty + t::geometry::TriangleMesh mesh_empty; + core::Tensor indices_empty = core::Tensor::Init({}); + + // check completely empty mesh + EXPECT_TRUE(mesh_empty.SelectByIndex(indices_empty).IsEmpty()); + EXPECT_TRUE(mesh_empty.SelectByIndex(core::Tensor::Init({0})) + .IsEmpty()); + + // check mesh w/o triangles + core::Tensor vertices_no_tris_orig = + core::Tensor::Ones({2, 3}, core::Float32, mesh_empty.GetDevice()); + core::Tensor expected_vertices_no_tris_orig = + core::Tensor::Ones({1, 3}, core::Float32, mesh_empty.GetDevice()); + mesh_empty.SetVertexPositions(vertices_no_tris_orig); + t::geometry::TriangleMesh selected_no_tris_orig = + mesh_empty.SelectByIndex(core::Tensor::Init({0})); + EXPECT_TRUE(selected_no_tris_orig.GetVertexPositions().AllClose( + expected_vertices_no_tris_orig)); + + // create box with normals, colors and labels defined. + t::geometry::TriangleMesh box = t::geometry::TriangleMesh::CreateBox(); + core::Tensor vertex_colors = core::Tensor::Init({{0.0, 0.0, 0.0}, + {1.0, 1.0, 1.0}, + {2.0, 2.0, 2.0}, + {3.0, 3.0, 3.0}, + {4.0, 4.0, 4.0}, + {5.0, 5.0, 5.0}, + {6.0, 6.0, 6.0}, + {7.0, 7.0, 7.0}}); + ; + core::Tensor vertex_labels = core::Tensor::Init({{0.0, 0.0, 0.0}, + {1.0, 1.0, 1.0}, + {2.0, 2.0, 2.0}, + {3.0, 3.0, 3.0}, + {4.0, 4.0, 4.0}, + {5.0, 5.0, 5.0}, + {6.0, 6.0, 6.0}, + {7.0, 7.0, 7.0}}) * + 10; + ; + core::Tensor triangle_labels = + core::Tensor::Init({{0.0, 0.0, 0.0}, + {1.0, 1.0, 1.0}, + {2.0, 2.0, 2.0}, + {3.0, 3.0, 3.0}, + {4.0, 4.0, 4.0}, + {5.0, 5.0, 5.0}, + {6.0, 6.0, 6.0}, + {7.0, 7.0, 7.0}, + {8.0, 8.0, 8.0}, + {9.0, 9.0, 9.0}, + {10.0, 10.0, 10.0}, + {11.0, 11.0, 11.0}}) * + 100; + box.SetVertexColors(vertex_colors); + box.SetVertexAttr("labels", vertex_labels); + box.ComputeTriangleNormals(); + box.SetTriangleAttr("labels", triangle_labels); + + // empty index list + EXPECT_TRUE(box.SelectByIndex(indices_empty).IsEmpty()); + + // set the expected value + core::Tensor expected_verts = core::Tensor::Init({{0.0, 0.0, 1.0}, + {1.0, 0.0, 1.0}, + {0.0, 1.0, 1.0}, + {1.0, 1.0, 1.0}}); + core::Tensor expected_vert_colors = + core::Tensor::Init({{2.0, 2.0, 2.0}, + {3.0, 3.0, 3.0}, + {6.0, 6.0, 6.0}, + {7.0, 7.0, 7.0}}); + core::Tensor expected_vert_labels = + core::Tensor::Init({{20.0, 20.0, 20.0}, + {30.0, 30.0, 30.0}, + {60.0, 60.0, 60.0}, + {70.0, 70.0, 70.0}}); + core::Tensor expected_tris = + core::Tensor::Init({{0, 1, 3}, {0, 3, 2}}); + core::Tensor tris_mask = + core::Tensor::Init({0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0}); + core::Tensor expected_tri_normals = + box.GetTriangleNormals().IndexGet({tris_mask}); + core::Tensor expected_tri_labels = core::Tensor::Init( + {{800.0, 800.0, 800.0}, {900.0, 900.0, 900.0}}); + + // check basic case + core::Tensor indices = core::Tensor::Init({2, 3, 6, 7}); + t::geometry::TriangleMesh selected = box.SelectByIndex(indices); + + EXPECT_TRUE(selected.GetVertexPositions().AllClose(expected_verts)); + EXPECT_TRUE(selected.GetVertexColors().AllClose(expected_vert_colors)); + EXPECT_TRUE( + selected.GetVertexAttr("labels").AllClose(expected_vert_labels)); + EXPECT_TRUE(selected.GetTriangleIndices().AllClose(expected_tris)); + EXPECT_TRUE(selected.GetTriangleNormals().AllClose(expected_tri_normals)); + EXPECT_TRUE( + selected.GetTriangleAttr("labels").AllClose(expected_tri_labels)); + + // check duplicated indices case + core::Tensor indices_duplicate = + core::Tensor::Init({2, 2, 3, 3, 6, 7, 7}); + t::geometry::TriangleMesh selected_duplicate = + box.SelectByIndex(indices_duplicate); + EXPECT_TRUE( + selected_duplicate.GetVertexPositions().AllClose(expected_verts)); + EXPECT_TRUE(selected_duplicate.GetVertexColors().AllClose( + expected_vert_colors)); + EXPECT_TRUE(selected_duplicate.GetVertexAttr("labels").AllClose( + expected_vert_labels)); + EXPECT_TRUE( + selected_duplicate.GetTriangleIndices().AllClose(expected_tris)); + EXPECT_TRUE(selected_duplicate.GetTriangleNormals().AllClose( + expected_tri_normals)); + EXPECT_TRUE(selected_duplicate.GetTriangleAttr("labels").AllClose( + expected_tri_labels)); + + // select with empty triangles as result + // set the expected value + core::Tensor expected_verts_no_tris = core::Tensor::Init( + {{0.0, 0.0, 0.0}, {1.0, 0.0, 1.0}, {0.0, 1.0, 0.0}}); + core::Tensor expected_vert_colors_no_tris = core::Tensor::Init( + {{0.0, 0.0, 0.0}, {3.0, 3.0, 3.0}, {4.0, 4.0, 4.0}}); + core::Tensor expected_vert_labels_no_tris = core::Tensor::Init( + {{0.0, 0.0, 0.0}, {30.0, 30.0, 30.0}, {40.0, 40.0, 40.0}}); + + core::Tensor indices_no_tris = core::Tensor::Init({0, 3, 4}); + t::geometry::TriangleMesh selected_no_tris = + box.SelectByIndex(indices_no_tris); + + EXPECT_TRUE(selected_no_tris.GetVertexPositions().AllClose( + expected_verts_no_tris)); + EXPECT_TRUE(selected_no_tris.GetVertexColors().AllClose( + expected_vert_colors_no_tris)); + EXPECT_TRUE(selected_no_tris.GetVertexAttr("labels").AllClose( + expected_vert_labels_no_tris)); + EXPECT_FALSE(selected_no_tris.HasTriangleIndices()); + + // check that initial mesh is unchanged + t::geometry::TriangleMesh box_untouched = + t::geometry::TriangleMesh::CreateBox(); + EXPECT_TRUE(box.GetVertexPositions().AllClose( + box_untouched.GetVertexPositions())); + EXPECT_TRUE(box.GetTriangleIndices().AllClose( + box_untouched.GetTriangleIndices())); +} + } // namespace tests } // namespace open3d diff --git a/python/test/t/geometry/test_trianglemesh.py b/python/test/t/geometry/test_trianglemesh.py index 843184dd3e6..2a108adff56 100644 --- a/python/test/t/geometry/test_trianglemesh.py +++ b/python/test/t/geometry/test_trianglemesh.py @@ -417,3 +417,225 @@ def test_pickle(device): mesh.vertex.positions.cpu().numpy()) np.testing.assert_equal(mesh_load.triangle.indices.cpu().numpy(), mesh.triangle.indices.cpu().numpy()) + + +@pytest.mark.parametrize("device", list_devices()) +def test_select_faces_by_mask_32(device): + sphere_custom = o3d.t.geometry.TriangleMesh.create_sphere( + 1, 3, o3c.float64, o3c.int32, device) + + expected_verts = o3c.Tensor( + [[0.0, 0.0, 1.0], [0.866025, 0, 0.5], [0.433013, 0.75, 0.5], + [-0.866025, 0.0, 0.5], [-0.433013, -0.75, 0.5], [0.433013, -0.75, 0.5] + ], o3c.float64, device) + + expected_tris = o3c.Tensor([[0, 1, 2], [0, 3, 4], [0, 4, 5], [0, 5, 1]], + o3c.int32, device) + + # check indices shape mismatch + mask_2d = o3c.Tensor([[False, False], [False, False], [False, False]], + o3c.bool, device) + with pytest.raises(RuntimeError): + selected = sphere_custom.select_faces_by_mask(mask_2d) + + # check indices type mismatch + mask_float = o3c.Tensor([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], o3c.float32, device) + with pytest.raises(RuntimeError): + selected = sphere_custom.select_faces_by_mask(mask_float) + + # check the basic case + mask = o3c.Tensor([ + True, False, False, False, False, False, True, False, True, False, True, + False, False, False, False, False, False, False, False, False, False, + False, False, False + ], o3c.bool, device) + selected = sphere_custom.select_faces_by_mask(mask) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check that the original mesh is unmodified + untouched_sphere = o3d.t.geometry.TriangleMesh.create_sphere( + 1, 3, o3c.float64, o3c.int32, device) + assert sphere_custom.vertex.positions.allclose( + untouched_sphere.vertex.positions) + assert sphere_custom.triangle.indices.allclose( + untouched_sphere.triangle.indices) + + +@pytest.mark.parametrize("device", list_devices()) +def test_select_faces_by_mask_64(device): + sphere_custom = o3d.t.geometry.TriangleMesh.create_sphere( + 1, 3, o3c.float64, o3c.int64, device) + + # check indices shape mismatch + mask_2d = o3c.Tensor([[False, False], [False, False], [False, False]], + o3c.bool, device) + with pytest.raises(RuntimeError): + selected = sphere_custom.select_faces_by_mask(mask_2d) + + # check indices type mismatch + mask_float = o3c.Tensor([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], o3c.float32, device) + with pytest.raises(RuntimeError): + selected = sphere_custom.select_faces_by_mask(mask_float) + + expected_verts = o3c.Tensor( + [[0.0, 0.0, 1.0], [0.866025, 0, 0.5], [0.433013, 0.75, 0.5], + [-0.866025, 0.0, 0.5], [-0.433013, -0.75, 0.5], [0.433013, -0.75, 0.5] + ], o3c.float64, device) + + expected_tris = o3c.Tensor([[0, 1, 2], [0, 3, 4], [0, 4, 5], [0, 5, 1]], + o3c.int64, device) + # check the basic case + mask = o3c.Tensor([ + True, False, False, False, False, False, True, False, True, False, True, + False, False, False, False, False, False, False, False, False, False, + False, False, False + ], o3c.bool, device) + + selected = sphere_custom.select_faces_by_mask(mask) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check that the original mesh is unmodified + untouched_sphere = o3d.t.geometry.TriangleMesh.create_sphere( + 1, 3, o3c.float64, o3c.int64, device) + assert sphere_custom.vertex.positions.allclose( + untouched_sphere.vertex.positions) + assert sphere_custom.triangle.indices.allclose( + untouched_sphere.triangle.indices) + + +@pytest.mark.parametrize("device", list_devices()) +def test_select_by_index_32(device): + sphere_custom = o3d.t.geometry.TriangleMesh.create_sphere( + 1, 3, o3c.float64, o3c.int32, device) + + expected_verts = o3c.Tensor( + [[0.0, 0.0, 1.0], [0.866025, 0, 0.5], [0.433013, 0.75, 0.5], + [-0.866025, 0.0, 0.5], [-0.433013, -0.75, 0.5], [0.433013, -0.75, 0.5] + ], o3c.float64, device) + + expected_tris = o3c.Tensor([[0, 1, 2], [0, 3, 4], [0, 4, 5], [0, 5, 1]], + o3c.int32, device) + + # check indices shape mismatch + indices_2d = o3c.Tensor([[0, 2], [3, 5], [6, 7]], o3c.int32, device) + with pytest.raises(RuntimeError): + selected = sphere_custom.select_by_index(indices_2d) + + # check indices type mismatch + indices_float = o3c.Tensor([2.0, 4.0], o3c.float32, device) + with pytest.raises(RuntimeError): + selected = sphere_custom.select_by_index(indices_float) + + # check the expected mesh with int8 input + indices_8 = o3c.Tensor([0, 2, 3, 5, 6, 7], o3c.int8, device) + selected = sphere_custom.select_by_index(indices_8) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check the expected mesh with int16 input + indices_16 = o3c.Tensor([2, 0, 5, 3, 7, 6], o3c.int16, device) + selected = sphere_custom.select_by_index(indices_16) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check the expected mesh with uint32 input + indices_u32 = o3c.Tensor([7, 6, 5, 3, 2, 0], o3c.uint32, device) + selected = sphere_custom.select_by_index(indices_u32) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check the expected mesh with uint64 input and unsorted indices + indices_u64 = o3c.Tensor([7, 6, 3, 5, 0, 2], o3c.uint64, device) + selected = sphere_custom.select_by_index(indices_u64) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check that an index exceeding the max vertex index of the mesh is ignored + selected = sphere_custom.select_by_index([0, 2, 3, 5, 6, 99, 7]) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check that a negative index is ignored + selected = sphere_custom.select_by_index([0, 2, 3, 5, -10, 6, 7]) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check that the original mesh is unmodified + untouched_sphere = o3d.t.geometry.TriangleMesh.create_sphere( + 1, 3, o3c.float64, o3c.int32, device) + assert sphere_custom.vertex.positions.allclose( + untouched_sphere.vertex.positions) + assert sphere_custom.triangle.indices.allclose( + untouched_sphere.triangle.indices) + + +@pytest.mark.parametrize("device", list_devices()) +def test_select_by_index_64(device): + sphere_custom = o3d.t.geometry.TriangleMesh.create_sphere( + 1, 3, o3c.float64, o3c.int64, device) + + # check indices shape mismatch + with pytest.raises(RuntimeError): + indices_2d = o3c.Tensor([[0, 2], [3, 5], [6, 7]], o3c.int64, device) + selected = sphere_custom.select_by_index(indices_2d) + + # check indices type mismatch + with pytest.raises(RuntimeError): + indices_float = o3c.Tensor([2.0, 4.0], o3c.float64, device) + selected = sphere_custom.select_by_index(indices_float) + + expected_verts = o3c.Tensor( + [[0.0, 0.0, 1.0], [0.866025, 0, 0.5], [0.433013, 0.75, 0.5], + [-0.866025, 0.0, 0.5], [-0.433013, -0.75, 0.5], [0.433013, -0.75, 0.5] + ], o3c.float64, device) + + expected_tris = o3c.Tensor([[0, 1, 2], [0, 3, 4], [0, 4, 5], [0, 5, 1]], + o3c.int64, device) + + # check the expected mesh with int8 input + indices_u8 = o3c.Tensor([0, 2, 3, 5, 6, 7], o3c.uint8, device) + selected = sphere_custom.select_by_index(indices_u8) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check the expected mesh with int16 input + indices_u16 = o3c.Tensor([2, 0, 5, 3, 7, 6], o3c.uint16, device) + selected = sphere_custom.select_by_index(indices_u16) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check the expected mesh with int32 input + indices_32 = o3c.Tensor([7, 6, 5, 3, 2, 0], o3c.int32, device) + selected = sphere_custom.select_by_index(indices_32) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check the expected mesh with int64 input and unsorted indices + indices_64 = o3c.Tensor([7, 6, 3, 5, 0, 2], o3c.int64, device) + selected = sphere_custom.select_by_index(indices_64) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check that an index exceeding the max vertex index of the mesh is ignored + selected = sphere_custom.select_by_index([0, 2, 3, 5, 6, 99, 7]) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check that a negative index is ignored + selected = sphere_custom.select_by_index([0, 2, 3, 5, -10, 6, 7]) + assert selected.vertex.positions.allclose(expected_verts) + assert selected.triangle.indices.allclose(expected_tris) + + # check that the original mesh is unmodified + untouched_sphere = o3d.t.geometry.TriangleMesh.create_sphere( + 1, 3, o3c.float64, o3c.int64, device) + assert sphere_custom.vertex.positions.allclose( + untouched_sphere.vertex.positions) + assert sphere_custom.triangle.indices.allclose( + untouched_sphere.triangle.indices) From 9cf5df2399bbfd6ff5dea4143581b92a54261ade Mon Sep 17 00:00:00 2001 From: Sameer Sheorey <41028320+ssheorey@users.noreply.github.com> Date: Tue, 14 Nov 2023 11:41:26 +0530 Subject: [PATCH 3/5] Add Open3D architecture diagram (#6488) --- README.md | 5 +++++ cpp/open3d/t/geometry/TriangleMesh.h | 2 +- docs/getting_started.in.rst | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ea43c38ffca..a3eedc52343 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,11 @@ the open-source community. - GPU acceleration for core 3D operations - Available in C++ and Python +Here's a brief overview of the different components of Open3D and how they fit +together to enable full end to end pipelines: + +![Open3D_layers](https://github.com/isl-org/Open3D/assets/41028320/e9b8645a-a823-4d78-8310-e85207bbc3e4) + For more, please visit the [Open3D documentation](http://www.open3d.org/docs). ## Python quick start diff --git a/cpp/open3d/t/geometry/TriangleMesh.h b/cpp/open3d/t/geometry/TriangleMesh.h index 8f7373afd9d..99417f8433c 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.h +++ b/cpp/open3d/t/geometry/TriangleMesh.h @@ -91,7 +91,7 @@ class LineSet; /// default and common attributes. class TriangleMesh : public Geometry, public DrawableGeometry { public: - /// Construct an empty pointcloud on the provided device. + /// Construct an empty trianglemesh on the provided device. /// \param device The device on which to initialize the trianglemesh /// (default: 'CPU:0'). TriangleMesh(const core::Device &device = core::Device("CPU:0")); diff --git a/docs/getting_started.in.rst b/docs/getting_started.in.rst index d3ffdc21956..0708e0a9ea8 100644 --- a/docs/getting_started.in.rst +++ b/docs/getting_started.in.rst @@ -13,10 +13,10 @@ Open3D Python packages are distributed via Supported Python versions: -* 3.7 * 3.8 * 3.9 * 3.10 +* 3.11 Supported operating systems: @@ -80,9 +80,9 @@ version (``HEAD`` of ``master`` branch): - `Python 3.11 `__ * - MacOS - - `Python 3.8 `__ - - `Python 3.9 `__ - - `Python 3.10 `__ + - `Python 3.8 `__ + - `Python 3.9 `__ + - `Python 3.10 `__ - `Python 3.11 `__ * - Windows From 9b45f01cb7762eb8d20154628dccc1b0a134ce1e Mon Sep 17 00:00:00 2001 From: nsaiapova Date: Tue, 14 Nov 2023 18:24:29 +0100 Subject: [PATCH 4/5] Fix seg fault in t::geometry::TriangleMesh::SelectByIndex for negative index (#6489) And add a C++ test. --- cpp/open3d/t/geometry/TriangleMesh.cpp | 1 + cpp/tests/t/geometry/TriangleMesh.cpp | 24 +++++++++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/cpp/open3d/t/geometry/TriangleMesh.cpp b/cpp/open3d/t/geometry/TriangleMesh.cpp index e4e878af93a..d96e9a55b75 100644 --- a/cpp/open3d/t/geometry/TriangleMesh.cpp +++ b/cpp/open3d/t/geometry/TriangleMesh.cpp @@ -1171,6 +1171,7 @@ TriangleMesh TriangleMesh::SelectByIndex(const core::Tensor &indices) const { "out of range. " "It is ignored.", indices_ptr[i]); + continue; } vertex_mask_ptr[indices_ptr[i]] = 1; } diff --git a/cpp/tests/t/geometry/TriangleMesh.cpp b/cpp/tests/t/geometry/TriangleMesh.cpp index 8cb6ba0ddac..d1e2d819eac 100644 --- a/cpp/tests/t/geometry/TriangleMesh.cpp +++ b/cpp/tests/t/geometry/TriangleMesh.cpp @@ -1143,16 +1143,18 @@ TEST_P(TriangleMeshPermuteDevices, SelectByIndex) { // check basic case core::Tensor indices = core::Tensor::Init({2, 3, 6, 7}); - t::geometry::TriangleMesh selected = box.SelectByIndex(indices); + t::geometry::TriangleMesh selected_basic = box.SelectByIndex(indices); - EXPECT_TRUE(selected.GetVertexPositions().AllClose(expected_verts)); - EXPECT_TRUE(selected.GetVertexColors().AllClose(expected_vert_colors)); + EXPECT_TRUE(selected_basic.GetVertexPositions().AllClose(expected_verts)); EXPECT_TRUE( - selected.GetVertexAttr("labels").AllClose(expected_vert_labels)); - EXPECT_TRUE(selected.GetTriangleIndices().AllClose(expected_tris)); - EXPECT_TRUE(selected.GetTriangleNormals().AllClose(expected_tri_normals)); + selected_basic.GetVertexColors().AllClose(expected_vert_colors)); + EXPECT_TRUE(selected_basic.GetVertexAttr("labels").AllClose( + expected_vert_labels)); + EXPECT_TRUE(selected_basic.GetTriangleIndices().AllClose(expected_tris)); EXPECT_TRUE( - selected.GetTriangleAttr("labels").AllClose(expected_tri_labels)); + selected_basic.GetTriangleNormals().AllClose(expected_tri_normals)); + EXPECT_TRUE(selected_basic.GetTriangleAttr("labels").AllClose( + expected_tri_labels)); // check duplicated indices case core::Tensor indices_duplicate = @@ -1172,6 +1174,14 @@ TEST_P(TriangleMeshPermuteDevices, SelectByIndex) { EXPECT_TRUE(selected_duplicate.GetTriangleAttr("labels").AllClose( expected_tri_labels)); + core::Tensor indices_negative = + core::Tensor::Init({2, -4, 3, 6, 7}); + t::geometry::TriangleMesh selected_negative = + box.SelectByIndex(indices_negative); + EXPECT_TRUE( + selected_negative.GetVertexPositions().AllClose(expected_verts)); + EXPECT_TRUE(selected_negative.GetTriangleIndices().AllClose(expected_tris)); + // select with empty triangles as result // set the expected value core::Tensor expected_verts_no_tris = core::Tensor::Init( From 392fcb900bf06c265ef4fb7784c46dcb8ab84e1e Mon Sep 17 00:00:00 2001 From: Saurabh Khanduja Date: Tue, 14 Nov 2023 22:04:23 +0100 Subject: [PATCH 5/5] Bugfixes and docs improvement of AABB tensor class (#6486) * Change invalid values of min_bound and max_bound as done in legacy, related pr #6444 * Correct points length check when creating AABB from points. * Print min and max bound of AABB when printing AABB * Add brief docs for methods and variables, fix warning messages. * Fix AABB ToString() test --------- Co-authored-by: Sameer Sheorey <41028320+ssheorey@users.noreply.github.com> --- CHANGELOG.md | 1 + cpp/open3d/geometry/BoundingVolume.cpp | 5 +++- cpp/open3d/geometry/BoundingVolume.h | 9 +++++- cpp/open3d/t/geometry/BoundingVolume.cpp | 29 ++++++++++++------- cpp/open3d/t/geometry/BoundingVolume.h | 29 ++++++++++++++----- .../visualization/visualizer/Visualizer.h | 2 ++ cpp/pybind/t/geometry/boundingvolume.cpp | 2 +- .../t/geometry/AxisAlignedBoundingBox.cpp | 16 ++++++---- docs/Doxyfile.in | 7 ----- 9 files changed, 66 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d3ba84ed84..df92535007d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * Fix printing of tensor in gpu and add validation check for bounds of axis-aligned bounding box (PR #6444) * Python 3.11 support. bump pybind11 v2.6.2 -> v2.11.1 * Check for support of CUDA Memory Pools at runtime (#4679) +* Fix `toString`, `CreateFromPoints` methods and improve docs in `AxisAlignedBoundingBox`. 🐛📝 ## 0.13 diff --git a/cpp/open3d/geometry/BoundingVolume.cpp b/cpp/open3d/geometry/BoundingVolume.cpp index ba2594b6ba3..89b0ca70e5d 100644 --- a/cpp/open3d/geometry/BoundingVolume.cpp +++ b/cpp/open3d/geometry/BoundingVolume.cpp @@ -303,7 +303,7 @@ AxisAlignedBoundingBox& AxisAlignedBoundingBox::Scale( AxisAlignedBoundingBox& AxisAlignedBoundingBox::Rotate( const Eigen::Matrix3d& rotation, const Eigen::Vector3d& center) { utility::LogError( - "A rotation of a AxisAlignedBoundingBox would not be axis aligned " + "A rotation of an AxisAlignedBoundingBox would not be axis-aligned " "anymore, convert it to an OrientedBoundingBox first"); return *this; } @@ -330,6 +330,9 @@ AxisAlignedBoundingBox AxisAlignedBoundingBox::CreateFromPoints( const std::vector& points) { AxisAlignedBoundingBox box; if (points.empty()) { + utility::LogWarning( + "The number of points is 0 when creating axis-aligned bounding " + "box."); box.min_bound_ = Eigen::Vector3d(0.0, 0.0, 0.0); box.max_bound_ = Eigen::Vector3d(0.0, 0.0, 0.0); } else { diff --git a/cpp/open3d/geometry/BoundingVolume.h b/cpp/open3d/geometry/BoundingVolume.h index 76cdca5977d..b4b310f3ca0 100644 --- a/cpp/open3d/geometry/BoundingVolume.h +++ b/cpp/open3d/geometry/BoundingVolume.h @@ -151,7 +151,8 @@ class OrientedBoundingBox : public Geometry3D { /// \class AxisAlignedBoundingBox /// -/// \brief A bounding box that is aligned along the coordinate axes. +/// \brief A bounding box that is aligned along the coordinate axes and defined +/// by the min_bound and max_bound. /// /// The AxisAlignedBoundingBox uses the coordinate axes for bounding box /// generation. This means that the bounding box is oriented along the @@ -227,14 +228,20 @@ class AxisAlignedBoundingBox : public Geometry3D { /// extents. double GetMaxExtent() const { return (max_bound_ - min_bound_).maxCoeff(); } + /// Calculates the percentage position of the given x-coordinate within + /// the x-axis range of this AxisAlignedBoundingBox. double GetXPercentage(double x) const { return (x - min_bound_(0)) / (max_bound_(0) - min_bound_(0)); } + /// Calculates the percentage position of the given y-coordinate within + /// the y-axis range of this AxisAlignedBoundingBox. double GetYPercentage(double y) const { return (y - min_bound_(1)) / (max_bound_(1) - min_bound_(1)); } + /// Calculates the percentage position of the given z-coordinate within + /// the z-axis range of this AxisAlignedBoundingBox. double GetZPercentage(double z) const { return (z - min_bound_(2)) / (max_bound_(2) - min_bound_(2)); } diff --git a/cpp/open3d/t/geometry/BoundingVolume.cpp b/cpp/open3d/t/geometry/BoundingVolume.cpp index 0e529f27d1d..6a221502efe 100644 --- a/cpp/open3d/t/geometry/BoundingVolume.cpp +++ b/cpp/open3d/t/geometry/BoundingVolume.cpp @@ -42,9 +42,12 @@ AxisAlignedBoundingBox::AxisAlignedBoundingBox(const core::Tensor &min_bound, // Check if the bounding box is valid. if (Volume() < 0) { - utility::LogError( - "Invalid axis-aligned bounding box. Please make sure all " - "the elements in max bound are larger than min bound."); + utility::LogWarning( + "max_bound {} of bounding box is smaller than min_bound {} in " + "one or more axes. Fix input values to remove this warning.", + max_bound_.ToString(false), min_bound_.ToString(false)); + max_bound_ = open3d::core::Maximum(min_bound, max_bound); + min_bound_ = open3d::core::Minimum(min_bound, max_bound); } } @@ -80,7 +83,7 @@ void AxisAlignedBoundingBox::SetMinBound(const core::Tensor &min_bound) { if (Volume() < 0) { utility::LogWarning( "Invalid axis-aligned bounding box. Please make sure all " - "the elements in min bound are smaller than min bound."); + "the elements in min bound are smaller than max bound."); min_bound_ = tmp; } } @@ -110,8 +113,8 @@ void AxisAlignedBoundingBox::SetColor(const core::Tensor &color) { if (color.Max({0}).To(core::Float64).Item() > 1.0 || color.Min({0}).To(core::Float64).Item() < 0.0) { utility::LogError( - "The color must be in the range [0, 1], but for range [{}, " - "{}].", + "The color must be in the range [0, 1], but found in range " + "[{}, {}].", color.Min({0}).To(core::Float64).Item(), color.Max({0}).To(core::Float64).Item()); } @@ -220,7 +223,9 @@ core::Tensor AxisAlignedBoundingBox::GetPointIndicesWithinBoundingBox( } std::string AxisAlignedBoundingBox::ToString() const { - return fmt::format("AxisAlignedBoundingBox[{}, {}]", GetDtype().ToString(), + return fmt::format("AxisAlignedBoundingBox[{} - {}, {}, {}]", + GetMinBound().ToString(false), + GetMaxBound().ToString(false), GetDtype().ToString(), GetDevice().ToString()); } @@ -228,8 +233,10 @@ AxisAlignedBoundingBox AxisAlignedBoundingBox::CreateFromPoints( const core::Tensor &points) { core::AssertTensorShape(points, {utility::nullopt, 3}); core::AssertTensorDtypes(points, {core::Float32, core::Float64}); - if (points.GetLength() <= 3) { - utility::LogWarning("The points number is less than 3."); + if (points.GetLength() <= 0) { + utility::LogWarning( + "The number of points is 0 when creating axis-aligned bounding " + "box."); return AxisAlignedBoundingBox(points.GetDevice()); } else { const core::Tensor min_bound = points.Min({0}); @@ -385,8 +392,8 @@ void OrientedBoundingBox::SetColor(const core::Tensor &color) { if (color.Max({0}).To(core::Float64).Item() > 1.0 || color.Min({0}).To(core::Float64).Item() < 0.0) { utility::LogError( - "The color must be in the range [0, 1], but for range [{}, " - "{}].", + "The color must be in the range [0, 1], but found in range " + "[{}, {}].", color.Min({0}).To(core::Float64).Item(), color.Max({0}).To(core::Float64).Item()); } diff --git a/cpp/open3d/t/geometry/BoundingVolume.h b/cpp/open3d/t/geometry/BoundingVolume.h index 27fe7e4aaa6..b11cae01777 100644 --- a/cpp/open3d/t/geometry/BoundingVolume.h +++ b/cpp/open3d/t/geometry/BoundingVolume.h @@ -93,12 +93,12 @@ class AxisAlignedBoundingBox : public Geometry, public DrawableGeometry { /// \param min_bound Tensor with {3,} shape, and type float32 or float64. void SetMinBound(const core::Tensor &min_bound); - /// \brief Set the max boundof the box. + /// \brief Set the max bound of the box. /// If the data type of the given tensor differs from the data type of the /// box, an exception will be thrown. /// /// If the max bound makes the box invalid, it will not be set to the box. - /// \param min_bound Tensor with {3,} shape, and type float32 or float64. + /// \param max_bound Tensor with {3,} shape, and type float32 or float64. void SetMaxBound(const core::Tensor &max_bound); /// \brief Set the color of the box. @@ -156,16 +156,22 @@ class AxisAlignedBoundingBox : public Geometry, public DrawableGeometry { /// Returns the half extent of the bounding box. core::Tensor GetHalfExtent() const { return GetExtent() / 2; } - /// Returns the maximum extent, i.e. the maximum of X, Y and Z axis' + /// \brief Returns the maximum extent, i.e. the maximum of X, Y and Z axis' /// extents. double GetMaxExtent() const { return GetExtent().Max({0}).To(core::Float64).Item(); } + /// Calculates the percentage position of the given x-coordinate within + /// the x-axis range of this AxisAlignedBoundingBox. double GetXPercentage(double x) const; + /// Calculates the percentage position of the given y-coordinate within + /// the y-axis range of this AxisAlignedBoundingBox. double GetYPercentage(double y) const; + /// Calculates the percentage position of the given z-coordinate within + /// the z-axis range of this AxisAlignedBoundingBox. double GetZPercentage(double z) const; /// Returns the volume of the bounding box. @@ -173,8 +179,9 @@ class AxisAlignedBoundingBox : public Geometry, public DrawableGeometry { return GetExtent().Prod({0}).To(core::Float64).Item(); } - /// Returns the eight points that define the bounding box. The Return tensor - /// has shape {8, 3} and data type same as the box. + /// \brief Returns the eight points that define the bounding box. + /// + /// The Return tensor has shape {8, 3} and data type same as the box. core::Tensor GetBoxPoints() const; /// \brief Indices to points that are within the bounding box. @@ -206,16 +213,21 @@ class AxisAlignedBoundingBox : public Geometry, public DrawableGeometry { /// Creates the axis-aligned box that encloses the set of points. /// \param points A list of points with data type of float32 or float64 (N x - /// 3 tensor, where N must be larger than 3). + /// 3 tensor). /// \return AxisAlignedBoundingBox with same data type and device as input /// points. static AxisAlignedBoundingBox CreateFromPoints(const core::Tensor &points); protected: + /// The device to use for the bounding box. The default is CPU:0. core::Device device_ = core::Device("CPU:0"); + /// The data type of the bounding box. core::Dtype dtype_ = core::Float32; + /// The lower x, y, z bounds of the bounding box. core::Tensor min_bound_; + /// The upper x, y, z bounds of the bounding box. core::Tensor max_bound_; + /// The color of the bounding box in RGB. The default is white. core::Tensor color_; }; @@ -373,8 +385,9 @@ class OrientedBoundingBox : public Geometry, public DrawableGeometry { return GetExtent().Prod({0}).To(core::Float64).Item(); } - /// Returns the eight points that define the bounding box. The Return tensor - /// has shape {8, 3} and data type same as the box. + /// \brief Returns the eight points that define the bounding box. + /// + /// The Return tensor has shape {8, 3} and data type same as the box. /// /// \verbatim /// ------- x diff --git a/cpp/open3d/visualization/visualizer/Visualizer.h b/cpp/open3d/visualization/visualizer/Visualizer.h index 02126229bc3..247ee2c91fe 100644 --- a/cpp/open3d/visualization/visualizer/Visualizer.h +++ b/cpp/open3d/visualization/visualizer/Visualizer.h @@ -127,6 +127,7 @@ class Visualizer { /// Visualizer should be updated accordingly. /// /// \param geometry_ptr The Geometry object. + /// \param reset_bounding_box Reset viewpoint to view all geometries. virtual bool AddGeometry( std::shared_ptr geometry_ptr, bool reset_bounding_box = true); @@ -140,6 +141,7 @@ class Visualizer { /// added by AddGeometry /// /// \param geometry_ptr The Geometry object. + /// \param reset_bounding_box Reset viewpoint to view all geometries. virtual bool RemoveGeometry( std::shared_ptr geometry_ptr, bool reset_bounding_box = true); diff --git a/cpp/pybind/t/geometry/boundingvolume.cpp b/cpp/pybind/t/geometry/boundingvolume.cpp index 7505e02ae52..946fb9617df 100644 --- a/cpp/pybind/t/geometry/boundingvolume.cpp +++ b/cpp/pybind/t/geometry/boundingvolume.cpp @@ -197,7 +197,7 @@ The scaling center will be the box center if it is not specified.)", m, "AxisAlignedBoundingBox", "create_from_points", {{"points", "A list of points with data type of float32 or float64 (N x 3 " - "tensor, where N must be larger than 3)."}}); + "tensor)."}}); py::class_, std::shared_ptr, Geometry, DrawableGeometry> diff --git a/cpp/tests/t/geometry/AxisAlignedBoundingBox.cpp b/cpp/tests/t/geometry/AxisAlignedBoundingBox.cpp index 333895493ba..54405b414ff 100644 --- a/cpp/tests/t/geometry/AxisAlignedBoundingBox.cpp +++ b/cpp/tests/t/geometry/AxisAlignedBoundingBox.cpp @@ -32,6 +32,7 @@ INSTANTIATE_TEST_SUITE_P( AxisAlignedBoundingBoxPermuteDevicePairs::TestCases())); TEST_P(AxisAlignedBoundingBoxPermuteDevices, ConstructorNoArg) { + using ::testing::AnyOf; t::geometry::AxisAlignedBoundingBox aabb; // Inherited from Geometry3D. @@ -51,7 +52,11 @@ TEST_P(AxisAlignedBoundingBoxPermuteDevices, ConstructorNoArg) { EXPECT_EQ(aabb.GetDevice(), core::Device("CPU:0")); // Print Information. - EXPECT_EQ(aabb.ToString(), "AxisAlignedBoundingBox[Float32, CPU:0]"); + EXPECT_THAT( + aabb.ToString(), // Compiler dependent output + AnyOf("AxisAlignedBoundingBox[[0 0 0] - [0 0 0], Float32, CPU:0]", + "AxisAlignedBoundingBox[[0.0 0.0 0.0] - [0.0 0.0 0.0], " + "Float32, CPU:0]")); } TEST_P(AxisAlignedBoundingBoxPermuteDevices, Constructor) { @@ -60,10 +65,6 @@ TEST_P(AxisAlignedBoundingBoxPermuteDevices, Constructor) { core::Tensor min_bound = core::Tensor::Init({-1, -1, -1}, device); core::Tensor max_bound = core::Tensor::Init({1, 1, 1}, device); - // Attempt to construct with invalid min/max bound. - EXPECT_THROW(t::geometry::AxisAlignedBoundingBox(max_bound, min_bound), - std::runtime_error); - t::geometry::AxisAlignedBoundingBox aabb(min_bound, max_bound); // Public members. @@ -76,6 +77,11 @@ TEST_P(AxisAlignedBoundingBoxPermuteDevices, Constructor) { core::Tensor::Init({1, 1, 1}, device))); EXPECT_EQ(aabb.GetDevice(), device); + + // Attempt to construct with invalid min/max bound should create a valid + // bounding box with a warning. + t::geometry::AxisAlignedBoundingBox aabb_invalid(max_bound, min_bound); + EXPECT_TRUE(aabb_invalid.GetBoxPoints().AllClose(aabb.GetBoxPoints())); } TEST_P(AxisAlignedBoundingBoxPermuteDevicePairs, CopyDevice) { diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in index 9a514ce1535..0ed539ff60e 100644 --- a/docs/Doxyfile.in +++ b/docs/Doxyfile.in @@ -1146,13 +1146,6 @@ VERBATIM_HEADERS = YES ALPHABETICAL_INDEX = YES -# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in -# which the alphabetical index list will be split. -# Minimum value: 1, maximum value: 20, default value: 5. -# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. - -COLS_IN_ALPHA_INDEX = 5 - # In case all classes in a project start with a common prefix, all classes will # be put under the same header in the alphabetical index. The IGNORE_PREFIX tag # can be used to specify a prefix (or a list of prefixes) that should be ignored