From 594f8202161dc8cb1becd7c072b54502b2b60994 Mon Sep 17 00:00:00 2001 From: Bharath Kumar Date: Mon, 2 Sep 2024 08:56:45 -0700 Subject: [PATCH 1/4] [Mesh] TriangleMesh's "+=" operator appends UVs regardless of the presence of existing features (#6728) * [Mesh] TriangleMesh's "+=" operator applies regardless of the presence of existing features. * added change log * fixed changelog * add special case for += operator with empty mesh * remove blank line in changelog * adding unit test to test += operator for triangle mesh --------- Co-authored-by: Benjamin Ummenhofer --- CHANGELOG.md | 1 + cpp/open3d/geometry/TriangleMesh.cpp | 18 ++-- cpp/tests/geometry/TriangleMesh.cpp | 123 ++++++++++++++++++++++++++- 3 files changed, 133 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d032cdedbf4..022c41d17fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ - Fix KDTreeFlann possibly using a dangling pointer instead of internal storage and simplified its members (PR #6734) - Fix RANSAC early stop if no inliers in a specific iteration (PR #6789) - Fix segmentation fault (infinite recursion) of DetectPlanarPatches if multiple points have same coordinates (PR #6794) +- `TriangleMesh`'s `+=` operator appends UVs regardless of the presence of existing features (PR #6728) - Fix build with fmt v10.2.0 (#6783) - Fix segmentation fault (lambda reference capture) of VisualizerWithCustomAnimation::Play (PR #6804) - Add O3DVisualizer API to enable collapse control of verts in the side panel (PR #6865) diff --git a/cpp/open3d/geometry/TriangleMesh.cpp b/cpp/open3d/geometry/TriangleMesh.cpp index c73fc3490e8..9e909858736 100644 --- a/cpp/open3d/geometry/TriangleMesh.cpp +++ b/cpp/open3d/geometry/TriangleMesh.cpp @@ -52,9 +52,12 @@ TriangleMesh &TriangleMesh::Rotate(const Eigen::Matrix3d &R, TriangleMesh &TriangleMesh::operator+=(const TriangleMesh &mesh) { if (mesh.IsEmpty()) return (*this); - bool add_textures = HasTriangleUvs() && HasTextures() && - HasTriangleMaterialIds() && mesh.HasTriangleUvs() && - mesh.HasTextures() && mesh.HasTriangleMaterialIds(); + bool is_empty = IsEmpty(); + bool add_triangle_uvs = + mesh.HasTriangleUvs() && (HasTriangleUvs() || is_empty); + bool add_textures_and_material_ids = + mesh.HasTextures() && mesh.HasTriangleMaterialIds() && + ((HasTextures() && HasTriangleMaterialIds()) || is_empty); size_t old_vert_num = vertices_.size(); MeshBase::operator+=(mesh); size_t old_tri_num = triangles_.size(); @@ -77,19 +80,21 @@ TriangleMesh &TriangleMesh::operator+=(const TriangleMesh &mesh) { if (HasAdjacencyList()) { ComputeAdjacencyList(); } - if (add_textures) { + if (add_triangle_uvs) { size_t old_tri_uv_num = triangle_uvs_.size(); triangle_uvs_.resize(old_tri_uv_num + mesh.triangle_uvs_.size()); for (size_t i = 0; i < mesh.triangle_uvs_.size(); i++) { triangle_uvs_[old_tri_uv_num + i] = mesh.triangle_uvs_[i]; } - + } else { + triangle_uvs_.clear(); + } + if (add_textures_and_material_ids) { size_t old_tex_num = textures_.size(); textures_.resize(old_tex_num + mesh.textures_.size()); for (size_t i = 0; i < mesh.textures_.size(); i++) { textures_[old_tex_num + i] = mesh.textures_[i]; } - size_t old_mat_id_num = triangle_material_ids_.size(); triangle_material_ids_.resize(old_mat_id_num + mesh.triangle_material_ids_.size()); @@ -98,7 +103,6 @@ TriangleMesh &TriangleMesh::operator+=(const TriangleMesh &mesh) { mesh.triangle_material_ids_[i] + (int)old_tex_num; } } else { - triangle_uvs_.clear(); textures_.clear(); triangle_material_ids_.clear(); } diff --git a/cpp/tests/geometry/TriangleMesh.cpp b/cpp/tests/geometry/TriangleMesh.cpp index a562059cbc2..c00084d7ba4 100644 --- a/cpp/tests/geometry/TriangleMesh.cpp +++ b/cpp/tests/geometry/TriangleMesh.cpp @@ -22,8 +22,8 @@ namespace tests { /// \param eq_triangle_vertex_order If true then triangles are only equal if the /// order of the vertices is the same. If false any permutation of the triangle /// indices is allowed. -void ExpectMeshEQ(const open3d::geometry::TriangleMesh& mesh0, - const open3d::geometry::TriangleMesh& mesh1, +void ExpectMeshEQ(const open3d::geometry::TriangleMesh &mesh0, + const open3d::geometry::TriangleMesh &mesh1, double threshold = 1e-6, bool eq_triangle_vertex_order = true) { ExpectEQ(mesh0.vertices_, mesh1.vertices_, threshold); @@ -599,6 +599,125 @@ TEST(TriangleMesh, OperatorADD) { } } +TEST(TriangleMesh, OperatorAdditionAssignment) { + const size_t size = 100; + const size_t texture_size = 10; + + // Define the minimum and maximum bounds for random data generation + Eigen::Vector3d dmin(0.0, 0.0, 0.0); + Eigen::Vector3d dmax(1000.0, 1000.0, 1000.0); + Eigen::Vector3i imin(0, 0, 0); + Eigen::Vector3i imax(size - 1, size - 1, size - 1); + Eigen::Vector2d uvmin(0.0, 0.0); + Eigen::Vector2d uvmax(1.0, 1.0); + + // Mesh 0 contains only UVs but no textures and material ids + geometry::TriangleMesh tm0; + tm0.vertices_.resize(size); + tm0.triangles_.resize(size); + tm0.triangle_uvs_.resize(3 * size); + + // Mesh 1 contains all of UVS, textures and material ids + geometry::TriangleMesh tm1; + tm1.vertices_.resize(size); + tm1.triangles_.resize(size); + tm1.triangle_uvs_.resize(3 * size); + tm1.triangle_material_ids_.resize(size); + tm1.textures_.resize(texture_size); + + // Mesh 2 does not contains any of UVs, textures or material ids + geometry::TriangleMesh tm2; + tm2.vertices_.resize(size); + tm2.triangles_.resize(size); + + // Randomly generate data for tm0, tm1 and tm2 + Rand(tm0.vertices_, dmin, dmax, 0); + Rand(tm0.triangles_, imin, imax, 0); + std::vector + tm0_triangle_uvs(3 * size); + Rand(tm0_triangle_uvs, uvmin, uvmax, 0); + tm0.triangle_uvs_ = std::vector(tm0_triangle_uvs.begin(), + tm0_triangle_uvs.end()); + + Rand(tm1.vertices_, dmin, dmax, 0); + Rand(tm1.triangles_, imin, imax, 0); + std::vector + tm1_triangle_uvs(3 * size); + Rand(tm1_triangle_uvs, uvmin, uvmax, 0); + tm1.triangle_uvs_ = std::vector(tm1_triangle_uvs.begin(), + tm1_triangle_uvs.end()); + for (size_t i = 0; i < texture_size; i++) { + geometry::Image texture_image; + texture_image.Prepare(100, 100, 3, 1); + Rand(texture_image.data_, 0, 255, 0); + tm1.textures_[i] = texture_image; + } + Rand(tm1.triangle_material_ids_, 0, texture_size, 0); + + Rand(tm2.vertices_, dmin, dmax, 0); + Rand(tm2.triangles_, imin, imax, 0); + + geometry::TriangleMesh temp_tm0 = tm0; + geometry::TriangleMesh temp_tm1 = tm1; + geometry::TriangleMesh temp_tm2 = tm2; + // Function to reset values of temp_tm0, temp_tm1 and temp_tm2 + auto reset_values = [&tm0, &tm1, &tm2](geometry::TriangleMesh &temp_tm0, + geometry::TriangleMesh &temp_tm1, + geometry::TriangleMesh &temp_tm2) { + temp_tm0 = tm0; + temp_tm1 = tm1; + temp_tm2 = tm2; + }; + + // Test addition of tm1 with itself + temp_tm1 += tm1; + EXPECT_EQ(temp_tm1.triangles_.size(), 2 * size); + EXPECT_EQ(temp_tm1.triangle_uvs_.size(), 6 * size); + EXPECT_EQ(temp_tm1.vertices_.size(), 2 * size); + EXPECT_EQ(temp_tm1.textures_.size(), 2 * texture_size); + EXPECT_EQ(temp_tm1.triangle_material_ids_.size(), 2 * size); + for (size_t i = 0; i < size; i++) { + ExpectEQ(temp_tm1.vertices_[i], temp_tm1.vertices_[i + size]); + ExpectEQ(temp_tm1.triangles_[i], + (Eigen::Vector3i)(temp_tm1.triangles_[i + size] - + Eigen::Vector3i(size, size, size))); + EXPECT_EQ(temp_tm1.triangle_material_ids_[i], + temp_tm1.triangle_material_ids_[i + size] - texture_size); + } + for (size_t i = 0; i < 3 * size; i++) { + ExpectEQ(temp_tm1.triangle_uvs_[i], + temp_tm1.triangle_uvs_[i + 3 * size]); + } + for (size_t i = 0; i < texture_size; i++) { + ExpectEQ(temp_tm1.textures_[i].data_, + temp_tm1.textures_[i + texture_size].data_); + } + + // Test addition of tm0 and tm1 + reset_values(temp_tm0, temp_tm1, temp_tm2); + temp_tm0 += tm1; + temp_tm1 += tm0; + ExpectMeshEQ(temp_tm0, temp_tm1); + EXPECT_EQ(temp_tm0.textures_.size(), 0); + EXPECT_EQ(temp_tm0.triangle_material_ids_.size(), 0); + + // Test addition of tm1 and tm2 + reset_values(temp_tm0, temp_tm1, temp_tm2); + temp_tm1 += tm2; + temp_tm2 += tm1; + ExpectMeshEQ(temp_tm1, temp_tm2); + EXPECT_EQ(temp_tm0.textures_.size(), 0); + EXPECT_EQ(temp_tm0.triangle_material_ids_.size(), 0); + + // Test addition of tm2 and tm0 + reset_values(temp_tm0, temp_tm1, temp_tm2); + temp_tm2 += tm0; + temp_tm0 += tm2; + ExpectMeshEQ(temp_tm2, temp_tm0); + EXPECT_EQ(temp_tm0.textures_.size(), 0); + EXPECT_EQ(temp_tm0.triangle_material_ids_.size(), 0); +} + TEST(TriangleMesh, ComputeTriangleNormals) { std::vector ref = {{-0.119231, 0.738792, 0.663303}, {-0.115181, 0.730934, 0.672658}, From d18bb95c69f13333e3c54fce4eda9ac06d91b855 Mon Sep 17 00:00:00 2001 From: Nicola Loi Date: Wed, 4 Sep 2024 07:28:35 +0200 Subject: [PATCH 2/4] Support lowercase types (i, u, f) when reading PCD files (#6930) --- CHANGELOG.md | 1 + cpp/open3d/io/file_format/FilePCD.cpp | 21 ++++++++++++--------- cpp/open3d/t/io/file_format/FilePCD.cpp | 25 +++++++++++++++---------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 022c41d17fb..c58729b74da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ - Split pybind declarations/definitions to avoid C++ types in Python docs (PR #6869) - Fix minimal oriented bounding box of MeshBase derived classes and add new unit tests (PR #6898) - Fix projection of point cloud to Depth/RGBD image if no position attribute is provided (PR #6880) +- Support lowercase types when reading PCD files (PR #6930) ## 0.13 diff --git a/cpp/open3d/io/file_format/FilePCD.cpp b/cpp/open3d/io/file_format/FilePCD.cpp index ce04f2e2d5b..17aa080cd29 100644 --- a/cpp/open3d/io/file_format/FilePCD.cpp +++ b/cpp/open3d/io/file_format/FilePCD.cpp @@ -220,7 +220,8 @@ bool ReadPCDHeader(FILE *file, PCDHeader &header) { double UnpackBinaryPCDElement(const char *data_ptr, const char type, const int size) { - if (type == 'I') { + const char type_uppercase = std::toupper(type, std::locale()); + if (type_uppercase == 'I') { if (size == 1) { std::int8_t data; memcpy(&data, data_ptr, sizeof(data)); @@ -236,7 +237,7 @@ double UnpackBinaryPCDElement(const char *data_ptr, } else { return 0.0; } - } else if (type == 'U') { + } else if (type_uppercase == 'U') { if (size == 1) { std::uint8_t data; memcpy(&data, data_ptr, sizeof(data)); @@ -252,7 +253,7 @@ double UnpackBinaryPCDElement(const char *data_ptr, } else { return 0.0; } - } else if (type == 'F') { + } else if (type_uppercase == 'F') { if (size == 4) { float data; memcpy(&data, data_ptr, sizeof(data)); @@ -281,11 +282,12 @@ double UnpackASCIIPCDElement(const char *data_ptr, const char type, const int size) { char *end; - if (type == 'I') { + const char type_uppercase = std::toupper(type, std::locale()); + if (type_uppercase == 'I') { return (double)std::strtol(data_ptr, &end, 0); - } else if (type == 'U') { + } else if (type_uppercase == 'U') { return (double)std::strtoul(data_ptr, &end, 0); - } else if (type == 'F') { + } else if (type_uppercase == 'F') { return std::strtod(data_ptr, &end); } return 0.0; @@ -297,13 +299,14 @@ Eigen::Vector3d UnpackASCIIPCDColor(const char *data_ptr, if (size == 4) { std::uint8_t data[4] = {0, 0, 0, 0}; char *end; - if (type == 'I') { + const char type_uppercase = std::toupper(type, std::locale()); + if (type_uppercase == 'I') { std::int32_t value = std::strtol(data_ptr, &end, 0); memcpy(data, &value, 4); - } else if (type == 'U') { + } else if (type_uppercase == 'U') { std::uint32_t value = std::strtoul(data_ptr, &end, 0); memcpy(data, &value, 4); - } else if (type == 'F') { + } else if (type_uppercase == 'F') { float value = std::strtof(data_ptr, &end); memcpy(data, &value, 4); } diff --git a/cpp/open3d/t/io/file_format/FilePCD.cpp b/cpp/open3d/t/io/file_format/FilePCD.cpp index 4aa229c0d55..2670e133d5a 100644 --- a/cpp/open3d/t/io/file_format/FilePCD.cpp +++ b/cpp/open3d/t/io/file_format/FilePCD.cpp @@ -89,30 +89,34 @@ struct WriteAttributePtr { }; static core::Dtype GetDtypeFromPCDHeaderField(char type, int size) { - if (type == 'I') { + char type_uppercase = std::toupper(type, std::locale()); + if (type_uppercase == 'I') { if (size == 1) return core::Dtype::Int8; if (size == 2) return core::Dtype::Int16; if (size == 4) return core::Dtype::Int32; if (size == 8) return core::Dtype::Int64; else - utility::LogError("Unsupported data type."); - } else if (type == 'U') { + utility::LogError("Unsupported size {} for data type {}.", size, + type); + } else if (type_uppercase == 'U') { if (size == 1) return core::Dtype::UInt8; if (size == 2) return core::Dtype::UInt16; if (size == 4) return core::Dtype::UInt32; if (size == 8) return core::Dtype::UInt64; else - utility::LogError("Unsupported data type."); - } else if (type == 'F') { + utility::LogError("Unsupported size {} for data type {}.", size, + type); + } else if (type_uppercase == 'F') { if (size == 4) return core::Dtype::Float32; if (size == 8) return core::Dtype::Float64; else - utility::LogError("Unsupported data type."); + utility::LogError("Unsupported size {} for data type {}.", size, + type); } else { - utility::LogError("Unsupported data type."); + utility::LogError("Unsupported data type {}.", type); } } @@ -305,13 +309,14 @@ static void ReadASCIIPCDColorsFromField(ReadAttributePtr &attr, if (field.size == 4) { std::uint8_t data[4] = {0}; char *end; - if (field.type == 'I') { + char type_uppercase = std::toupper(field.type, std::locale()); + if (type_uppercase == 'I') { std::int32_t value = std::strtol(data_ptr, &end, 0); std::memcpy(data, &value, sizeof(std::int32_t)); - } else if (field.type == 'U') { + } else if (type_uppercase == 'U') { std::uint32_t value = std::strtoul(data_ptr, &end, 0); std::memcpy(data, &value, sizeof(std::uint32_t)); - } else if (field.type == 'F') { + } else if (type_uppercase == 'F') { float value = std::strtof(data_ptr, &end); std::memcpy(data, &value, sizeof(float)); } From b2d1f78b971030f460a0bf9b7ec33587fba1d5b0 Mon Sep 17 00:00:00 2001 From: Tim Ohliger Date: Wed, 4 Sep 2024 07:29:32 +0200 Subject: [PATCH 3/4] Updated delocate-fuse to delocate-merge (got removed in delocate>=0.12.0) (#6940) --- .github/workflows/macos.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 8d43e10d608..2aea46eb596 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -345,11 +345,8 @@ jobs: PYTAG="-cp$(echo ${{ env.python_version }} | tr -d '.')" mkdir universal_wheels pip install delocate - delocate-fuse -v x64_wheels/open3d-*${PYTAG}*.whl arm64_wheels/open3d-*${PYTAG}*.whl - # Normalize file name as delocate-fuse doesn't update it - OLD_WHL_NAME=$(basename x64_wheels/open3d-*${PYTAG}*.whl) - NEW_WHL_NAME=${OLD_WHL_NAME/x86_64/universal2} - mv x64_wheels/${OLD_WHL_NAME} universal_wheels/${NEW_WHL_NAME} + delocate-merge -v -w universal_wheels x64_wheels/open3d-*${PYTAG}*.whl arm64_wheels/open3d-*${PYTAG}*.whl + NEW_WHL_NAME=$(basename universal_wheels/open3d-*${PYTAG}*.whl) echo "PIP_PKG_NAME=$NEW_WHL_NAME" >> $GITHUB_ENV - name: Upload merged wheels From 553ca86cc11d57324b84eb96ae7cda5c90011091 Mon Sep 17 00:00:00 2001 From: Sharon Berezalsky Date: Thu, 5 Sep 2024 01:04:30 +0300 Subject: [PATCH 4/4] Expose minimal oriented bounding box to python api (#6946) --------- Co-authored-by: Sharon Berezalsky --- cpp/pybind/geometry/boundingvolume.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/cpp/pybind/geometry/boundingvolume.cpp b/cpp/pybind/geometry/boundingvolume.cpp index e178adfe19b..4ffcbf8a52d 100644 --- a/cpp/pybind/geometry/boundingvolume.cpp +++ b/cpp/pybind/geometry/boundingvolume.cpp @@ -83,6 +83,27 @@ The returned bounding box is an approximation to the minimal bounding box. open3d.geometry.OrientedBoundingBox: The oriented bounding box. The bounding box is oriented such that the axes are ordered with respect to the principal components. +)doc") + .def_static("create_from_points_minimal", + &OrientedBoundingBox::CreateFromPointsMinimal, + "points"_a, "robust"_a = false, + R"doc( +Creates the oriented bounding box with the smallest volume. + +The algorithm makes use of the fact that at least one edge of +the convex hull must be collinear with an edge of the minimum +bounding box: for each triangle in the convex hull, calculate +the minimal axis aligned box in the frame of that triangle. +at the end, return the box with the smallest volume + +Args: + points (open3d.utility.Vector3dVector): Input points. + robust (bool): If set to true uses a more robust method which works in + degenerate cases but introduces noise to the points coordinates. + +Returns: + open3d.geometry.OrientedBoundingBox: The oriented bounding box. The + bounding box is oriented such that its volume is minimized. )doc") .def("volume", &OrientedBoundingBox::Volume, "Returns the volume of the bounding box.")