CGAL/cgal

Mesh Simplification: Garland-Heckbert policy is very slow on large, flat regions

Closed this issue · 3 comments

Issue Details

I'm using the mesh simplification feature with the GarlandHeckbert_plane_policies (the "garland" policy), performance is extremely slow on meshes that contain large, flat (planar) regions. The simplification rate drops to only a few dozen faces per second on a modern CPU (i7-10700f), making it impractical for large-scale simplification tasks on such geometries.

Note: When simplifying meshes with primarily curved or irregular surfaces, the simplification performance is normal and meets expectations.

Source Code

Here is a minimal, self-contained example that reproduces the issue. It creates a large planar mesh and then attempts to simplify it using the Garland-Heckbert policy. The slow performance can be observed by running this code.

#include <iostream>
#include <chrono>
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/Surface_mesh_simplification/edge_collapse.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/Face_count_stop_predicate.h>
#include <CGAL/Surface_mesh_simplification/Policies/Edge_collapse/GarlandHeckbert_plane_policies.h>
#include <CGAL/Surface_mesh_simplification/Edge_collapse_visitor_base.h>
#include <iomanip>

typedef CGAL::Simple_cartesian<double> Kernel;
typedef Kernel::Point_3 Point_3;
typedef CGAL::Surface_mesh<Point_3> Mesh;

namespace SMS = CGAL::Surface_mesh_simplification;

// Stats structure to track simplification progress
struct Stats
{
    std::size_t collected = 0;
    std::size_t processed = 0;
    std::size_t collapsed = 0;
    std::size_t non_collapsable = 0;
    std::size_t cost_uncomputable = 0;
    std::size_t placement_uncomputable = 0;
};

// Visitor class to track and display simplification progress
struct My_visitor : SMS::Edge_collapse_visitor_base<Mesh>
{
    My_visitor(Stats* s, size_t initial_face_count, size_t target_face_count, unsigned int update_frequency = 1000) :
        stats(s),
        initial_face_count(initial_face_count),
        target_face_count(target_face_count),
        total_face_count_to_process(initial_face_count - target_face_count),
        update_frequency(update_frequency),
        collect_counter(0),
        process_counter(0),
        last_update_time(std::chrono::steady_clock::now()) {}

    void OnCollected(const SMS::Edge_profile<Mesh>&, const std::optional<double>&)
    {
        ++(stats->collected);
        ++collect_counter;

        auto now = std::chrono::steady_clock::now();
        if (collect_counter >= update_frequency ||
            std::chrono::duration_cast<std::chrono::milliseconds>(now - last_update_time).count() > 250) {
            std::cout << "\rEdges collected: " << stats->collected << std::flush;
            collect_counter = 0;
            last_update_time = now;
        }
    }

    void OnSelected(const SMS::Edge_profile<Mesh>& mesh,
                    std::optional<double> cost,
                    std::size_t,
                    std::size_t)
    {
        if (stats->processed == 0) {
            std::cout << "\n"
                      << std::flush;
        }
        ++(stats->processed);
        ++process_counter;
        if (!cost)
            ++(stats->cost_uncomputable);
        auto now = std::chrono::steady_clock::now();
        if (process_counter >= update_frequency ||
            std::chrono::duration_cast<std::chrono::milliseconds>(now - last_update_time).count() > 250) {
            size_t current_face_count = mesh.surface_mesh().number_of_faces();
            double processed_ratio = static_cast<double>(initial_face_count - current_face_count) / total_face_count_to_process;
            std::cout << "\rFaces left: " << current_face_count << " ("
                      << std::fixed << std::setprecision(2) << processed_ratio * 100.0 << "%)"
                      << std::flush;
            process_counter = 0;
            last_update_time = now;
        }
    }

    void OnCollapsing(const SMS::Edge_profile<Mesh>&,
                      std::optional<Point_3> placement)
    {
        if (!placement)
            ++(stats->placement_uncomputable);
    }

    void OnNonCollapsable(const SMS::Edge_profile<Mesh>&)
    {
        ++(stats->non_collapsable);
    }

    void OnCollapsed(const SMS::Edge_profile<Mesh>&, Mesh::Vertex_index)
    {
        ++(stats->collapsed);
    }

    Stats* stats;
    size_t initial_face_count;
    size_t target_face_count;
    size_t total_face_count_to_process;
    unsigned int update_frequency;
    unsigned int collect_counter;
    unsigned int process_counter;
    std::chrono::steady_clock::time_point last_update_time;
};

// Function to create a large planar mesh
Mesh create_planar_mesh(int rows, int cols)
{
    Mesh mesh;
    std::vector<Mesh::Vertex_index> vertices;
    for (int i = 0; i <= rows; ++i) {
        for (int j = 0; j <= cols; ++j) {
            vertices.push_back(mesh.add_vertex(Point_3(j, i, 0)));
        }
    }

    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            Mesh::Vertex_index v1 = vertices[i * (cols + 1) + j];
            Mesh::Vertex_index v2 = vertices[i * (cols + 1) + j + 1];
            Mesh::Vertex_index v3 = vertices[(i + 1) * (cols + 1) + j];
            Mesh::Vertex_index v4 = vertices[(i + 1) * (cols + 1) + j + 1];
            mesh.add_face(v1, v2, v4);
            mesh.add_face(v1, v4, v3);
        }
    }
    return mesh;
}

int main()
{
    int rows = 1500;
    int cols = 1500;
    Mesh mesh = create_planar_mesh(rows, cols);
    std::cout << "Initial mesh: " << mesh.number_of_faces() << " faces." << std::endl;

    size_t target_face_count = 100000;
    SMS::Face_count_stop_predicate<Mesh> stop(target_face_count);

    // Apply simplification using Garland-Heckbert policies
    std::cout << "Starting simplification with Garland-Heckbert policy..." << std::endl;
    auto t_start = std::chrono::steady_clock::now();

    Stats stats;
    unsigned int update_frequency = std::max(1000u, static_cast<unsigned int>(mesh.number_of_edges() / 20));
    size_t initial_face_count = mesh.number_of_faces();
    My_visitor visitor(&stats, initial_face_count, target_face_count, update_frequency);

    typedef SMS::GarlandHeckbert_plane_policies<Mesh, Kernel> GHPolicies;
    GHPolicies gh_policies(mesh);
    auto gh_cost = gh_policies.get_cost();
    auto gh_placement = gh_policies.get_placement();

    int r = SMS::edge_collapse(mesh,
                               stop,
                               CGAL::parameters::get_cost(gh_cost)
                                   .get_placement(gh_placement)
                                   .visitor(visitor));

    auto t_end = std::chrono::steady_clock::now();
    double elapsed_sec = std::chrono::duration_cast<std::chrono::duration<double>>(t_end - t_start).count();

    std::cout << "\nSimplification finished." << std::endl;
    std::cout << "Edges removed: " << r << std::endl;
    std::cout << "Final mesh: " << mesh.number_of_faces() << " faces." << std::endl;
    std::cout << "Execution time: " << elapsed_sec << " seconds." << std::endl;

    // Display statistics
    std::cout << "\nEdges collected: " << stats.collected
              << "\nEdges processed: " << stats.processed
              << "\nEdges collapsed: " << stats.collapsed
              << std::endl
              << "\nEdges not collapsed due to topological constraints: " << stats.non_collapsable
              << "\nEdge not collapsed due to cost computation constraints: " << stats.cost_uncomputable
              << "\nEdge not collapsed due to placement computation constraints: " << stats.placement_uncomputable
              << std::endl;

    return 0;
}

Environment

  • Operating system: Linux (Ubuntu 24.04.2 LTS), 64 bits
  • Compiler: g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
  • Release or debug mode: Release
  • Specific flags used (if any): -O3 -DNDEBUG
  • CGAL version: v6.0.1
  • Boost version: 1.83.0
  • Other libraries versions if used (Eigen, TBB, etc.): Eigen 3.4.0, TBB 2021.11.0

Thank you for pointing out. We first thought that switching to other polices such as GarlandHeckbert_triangle_policies might solve the problem you encounter, but it produces the same result which is a bug. We will work on it and report back.

It is slow because the GarlandHeckbert_plane_policy performs poorly on flat surfaces. The neighborhood size around edges increases significantly, which in turn increases the running time.

Image

You can use GarlandHeckbert_probabilistic_plane_policies to achieve a more uniform decimation, which in turn reduces the running time.

Image

Fixed by #9048