diff --git a/CHANGELOG.md b/CHANGELOG.md index ce980c4b417..8405fcb2696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## Main +- Exposed advanced parameters (`full_depth`, `samples_per_node`, `point_weight`) for Poisson surface reconstruction in `TriangleMesh.create_from_point_cloud_poisson` (PR #7430) (issue #7248) - Upgrade stdgpu third-party library to commit d7c07d0. - Fix performance for non-contiguous NumPy array conversion in pybind vector converters. This change removes restrictive `py::array::c_style` flags and adds a runtime contiguity check, improving Pandas-to-Open3D conversion speed by up to ~50×. (issue #5250)(PR #7343). - Corrected documentation for Link Open3D in C++ projects (broken links). diff --git a/cpp/open3d/geometry/SurfaceReconstructionPoisson.cpp b/cpp/open3d/geometry/SurfaceReconstructionPoisson.cpp index 7195166701f..1462ddb6093 100644 --- a/cpp/open3d/geometry/SurfaceReconstructionPoisson.cpp +++ b/cpp/open3d/geometry/SurfaceReconstructionPoisson.cpp @@ -43,6 +43,9 @@ namespace { // The order of the B-Spline used to splat in data for color interpolation static const int DATA_DEGREE = 0; +// Default pull factor for color/auxiliary data interpolation (PoissonRecon +// --data default) +static const float DEFAULT_DATAX = 32.f; // The order of the B-Spline used to splat in the weights for density estimation static const int WEIGHT_DEGREE = 2; // The order of the B-Spline used to splat in the normals for constructing the @@ -400,6 +403,9 @@ void Execute(const open3d::geometry::PointCloud& pcd, float width, float scale, bool linear_fit, + int full_depth, + float samples_per_node, + float point_weight, UIntPack) { static const int Dim = sizeof...(FEMSigs); typedef UIntPack Sigs; @@ -417,17 +423,16 @@ void Execute(const open3d::geometry::PointCloud& pcd, XForm xForm, iXForm; xForm = XForm::Identity(); - float datax = 32.f; + // Other internal parameters remain hardcoded int base_depth = 0; int base_v_cycles = 1; - float confidence = 0.f; - float point_weight = 2.f * DEFAULT_FEM_DEGREE; float confidence_bias = 0.f; - float samples_per_node = 1.5f; float cg_solver_accuracy = 1e-3f; - int full_depth = 5; int iters = 8; - bool exact_interpolation = false; + float datax = DEFAULT_DATAX; + + // Parameters are now passed as function arguments: + // full_depth, samples_per_node, point_weight double startTime = Time(); Real isoValue = 0; @@ -463,31 +468,16 @@ void Execute(const open3d::geometry::PointCloud& pcd, pointStream.xform_ = &xForm; { - auto ProcessDataWithConfidence = [&](const Point& p, - Open3DData& d) { - Real l = (Real)d.normal_.norm(); - if (!l || l != l) return (Real)-1.; - return (Real)pow(l, confidence); - }; auto ProcessData = [](const Point& p, Open3DData& d) { Real l = (Real)d.normal_.norm(); if (!l || l != l) return (Real)-1.; d.normal_ /= l; return (Real)1.; }; - if (confidence > 0) { - pointCount = FEMTreeInitializer::template Initialize< - Open3DData>(tree.spaceRoot(), pointStream, depth, - samples, sampleData, true, - tree.nodeAllocators[0], tree.initializer(), - ProcessDataWithConfidence); - } else { - pointCount = FEMTreeInitializer::template Initialize< - Open3DData>(tree.spaceRoot(), pointStream, depth, - samples, sampleData, true, - tree.nodeAllocators[0], tree.initializer(), - ProcessData); - } + pointCount = FEMTreeInitializer::template Initialize< + Open3DData>(tree.spaceRoot(), pointStream, depth, samples, + sampleData, true, tree.nodeAllocators[0], + tree.initializer(), ProcessData); } iXForm = xForm.inverse(); @@ -614,28 +604,17 @@ void Execute(const open3d::geometry::PointCloud& pcd, // Add the interpolation constraints if (point_weight > 0) { profiler.start(); - if (exact_interpolation) { - iInfo = FEMTree:: - template InitializeExactPointInterpolationInfo( - tree, samples, - ConstraintDual( - targetValue, - (Real)point_weight * pointWeightSum), - SystemDual((Real)point_weight * - pointWeightSum), - true, false); - } else { - iInfo = FEMTree:: - template InitializeApproximatePointInterpolationInfo< - Real, 0>( - tree, samples, - ConstraintDual( - targetValue, - (Real)point_weight * pointWeightSum), - SystemDual((Real)point_weight * - pointWeightSum), - true, 1); - } + // Use approximate interpolation (always) + iInfo = FEMTree:: + template InitializeApproximatePointInterpolationInfo( + tree, samples, + ConstraintDual( + targetValue, + (Real)point_weight * pointWeightSum), + SystemDual((Real)point_weight * + pointWeightSum), + true, 1); tree.addInterpolationConstraints(constraints, solveDepth, *iInfo); profiler.dumpOutput("#Set point constraints:"); } @@ -721,7 +700,10 @@ TriangleMesh::CreateFromPointCloudPoisson(const PointCloud& pcd, float width, float scale, bool linear_fit, - int n_threads) { + int n_threads, + int full_depth, + float samples_per_node, + float point_weight) { static const BoundaryType BType = DEFAULT_FEM_BOUNDARY; typedef IsotropicUIntPack< DIMENSION, FEMDegreeAndBType::Signature> @@ -746,7 +728,8 @@ TriangleMesh::CreateFromPointCloudPoisson(const PointCloud& pcd, auto mesh = std::make_shared(); std::vector densities; Execute(pcd, mesh, densities, static_cast(depth), width, scale, - linear_fit, FEMSigs()); + linear_fit, full_depth, samples_per_node, point_weight, + FEMSigs()); ThreadPool::Terminate(); diff --git a/cpp/open3d/geometry/TriangleMesh.h b/cpp/open3d/geometry/TriangleMesh.h index 7f0b05c6ca5..cf3a970eebf 100644 --- a/cpp/open3d/geometry/TriangleMesh.h +++ b/cpp/open3d/geometry/TriangleMesh.h @@ -547,15 +547,34 @@ class TriangleMesh : public MeshBase { /// estimate the positions of iso-vertices. /// \param n_threads Number of threads used for reconstruction. Set to -1 to /// automatically determine it. + /// \param full_depth Minimum depth for density estimation (default: 5). + /// Below this depth, the octree is complete (fully subdivided). + /// Higher values provide more stability in sparse regions but consume more + /// memory. + /// Recommended range: 3-7. Use higher values (6-7) if your point cloud has + /// sparse regions. + /// \param samples_per_node Minimum number of sample points per octree node + /// (default: 1.5). Controls adaptive octree refinement based on local point + /// density. Lower values (e.g., 1.0) allow finer subdivision and capture + /// more detail but may increase noise. Higher values (e.g., 3.0) suppress + /// noise but may lose fine details. Recommended range: 1.0-3.0. + /// \param point_weight Importance of point interpolation constraints + /// (default: 4.0). Controls the trade-off between data fidelity and surface + /// smoothness. Higher values (e.g., 10.0) prioritize fitting input points + /// exactly, resulting in surfaces closer to the data. Lower values produce + /// smoother surfaces. Recommended range: 2.0-10.0. /// \return The estimated TriangleMesh, and per vertex density values that - /// can be used to to trim the mesh. + /// can be used to trim the mesh. static std::tuple, std::vector> CreateFromPointCloudPoisson(const PointCloud &pcd, size_t depth = 8, float width = 0.0f, float scale = 1.1f, bool linear_fit = false, - int n_threads = -1); + int n_threads = -1, + int full_depth = 5, + float samples_per_node = 1.5f, + float point_weight = 4.0f); /// Factory function to create a tetrahedron mesh (trianglemeshfactory.cpp). /// the mesh centroid will be at (0,0,0) and \p radius defines the diff --git a/cpp/open3d/visualization/gui/PickPointsInteractor.cpp b/cpp/open3d/visualization/gui/PickPointsInteractor.cpp index 0633e28ac67..0871ed11a5c 100644 --- a/cpp/open3d/visualization/gui/PickPointsInteractor.cpp +++ b/cpp/open3d/visualization/gui/PickPointsInteractor.cpp @@ -74,8 +74,7 @@ class SelectionIndexLookup { std::string name; size_t start_index; - Obj(const std::string &n, size_t start) - : name(n), start_index(start) {}; + Obj(const std::string &n, size_t start) : name(n), start_index(start){}; bool IsValid() const { return !name.empty(); } }; diff --git a/cpp/pybind/geometry/trianglemesh.cpp b/cpp/pybind/geometry/trianglemesh.cpp index 818630e9542..8172ef7c206 100644 --- a/cpp/pybind/geometry/trianglemesh.cpp +++ b/cpp/pybind/geometry/trianglemesh.cpp @@ -350,7 +350,9 @@ void pybind_trianglemesh_definitions(py::module &m) { "This function uses the original implementation by " "Kazhdan. See https://github.com/mkazhdan/PoissonRecon", "pcd"_a, "depth"_a = 8, "width"_a = 0, "scale"_a = 1.1, - "linear_fit"_a = false, "n_threads"_a = -1) + "linear_fit"_a = false, "n_threads"_a = -1, + "full_depth"_a = 5, "samples_per_node"_a = 1.5f, + "point_weight"_a = 4.0f) .def_static( "create_from_oriented_bounding_box", &TriangleMesh::CreateFromOrientedBoundingBox, diff --git a/python/test/geometry/test_poisson_parameters.py b/python/test/geometry/test_poisson_parameters.py new file mode 100644 index 00000000000..6ea87e07aac --- /dev/null +++ b/python/test/geometry/test_poisson_parameters.py @@ -0,0 +1,91 @@ +# ---------------------------------------------------------------------------- +# - Open3D: www.open3d.org - +# ---------------------------------------------------------------------------- +# Copyright (c) 2018-2024 www.open3d.org +# SPDX-License-Identifier: MIT +# ---------------------------------------------------------------------------- + +import open3d as o3d +import numpy as np +import pytest + + +def _create_point_cloud(num_points=100): + """Helper to create a point cloud with normals.""" + np.random.seed(42) # Fixed seed for reproducible tests + pcd = o3d.geometry.PointCloud() + pcd.points = o3d.utility.Vector3dVector(np.random.rand(num_points, 3) - 0.5) + pcd.normals = o3d.utility.Vector3dVector( + np.random.rand(num_points, 3) - 0.5) + pcd.normalize_normals() + return pcd + + +def _assert_valid_mesh(mesh, densities): + """Helper to validate mesh and densities output.""" + assert mesh is not None + assert len(mesh.vertices) > 0 + assert len(mesh.triangles) > 0 + assert len(densities) == len(mesh.vertices) + + +@pytest.fixture +def sample_point_cloud(): + """Fixture that returns a simple point cloud for testing.""" + return _create_point_cloud() + + +def test_poisson_default_parameters(sample_point_cloud): + """Test Poisson reconstruction with default parameters.""" + mesh, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson( + sample_point_cloud, depth=6) + _assert_valid_mesh(mesh, densities) + + +@pytest.mark.parametrize("params", [ + { + "depth": 6, + "full_depth": 4, + "samples_per_node": 2.0, + "point_weight": 5.0 + }, + { + "depth": 6, + "full_depth": 3 + }, + { + "depth": 6, + "full_depth": 5 + }, + { + "depth": 5, + "samples_per_node": 1.0 + }, + { + "depth": 5, + "samples_per_node": 3.0 + }, + { + "depth": 5, + "point_weight": 4.0 + }, + { + "depth": 5, + "point_weight": 10.0 + }, +]) +def test_poisson_with_various_parameters(sample_point_cloud, params): + """Test Poisson reconstruction with various parameter combinations.""" + mesh, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson( + sample_point_cloud, **params) + _assert_valid_mesh(mesh, densities) + + +def test_poisson_backward_compatibility(): + """Test that old API calls still work (backward compatibility).""" + pcd = _create_point_cloud(num_points=50) + + # Old-style call without new parameters + mesh, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson( + pcd, depth=5, scale=1.1, linear_fit=False) + _assert_valid_mesh(mesh, densities)