• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pmp-library / pmp-library / 25581337924

08 May 2026 09:51PM UTC coverage: 89.809% (-1.2%) from 90.99%
25581337924

push

github

dsieger
Simplify decimation test, disable unsupported test

Closes #248

5173 of 5760 relevant lines covered (89.81%)

603378.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.66
/src/pmp/algorithms/decimation.cpp
1
// Copyright 2011-2020 the Polygon Mesh Processing Library developers.
2
// SPDX-License-Identifier: MIT
3

4
#include "pmp/algorithms/decimation.h"
5

6
#include <iterator>
7
#include <limits>
8
#include <numbers>
9

10
#include "pmp/algorithms/distance_point_triangle.h"
11
#include "pmp/algorithms/normals.h"
12

13
namespace pmp {
14
namespace {
15

16
template <class HeapEntry, class HeapInterface>
17
class Heap : private std::vector<HeapEntry>
18
{
19
public:
20
    using This = Heap<HeapEntry, HeapInterface>;
21

22
    // Constructor
23
    Heap() : HeapVector() {}
24

25
    // Construct with a given \p HeapInterface.
26
    Heap(const HeapInterface& i) : HeapVector(), interface_(i) {}
3✔
27

28
    // Destructor.
29
    ~Heap() = default;
3✔
30

31
    // clear the heap
32
    void clear() { HeapVector::clear(); }
33

34
    // is heap empty?
35
    bool empty() { return HeapVector::empty(); }
81✔
36

37
    // returns the size of heap
38
    unsigned int size() { return (unsigned int)HeapVector::size(); }
1,071✔
39

40
    // reserve space for N entries
41
    void reserve(unsigned int n) { HeapVector::reserve(n); }
3✔
42

43
    // reset heap position to -1 (not in heap)
44
    void reset_heap_position(HeapEntry h)
49✔
45
    {
46
        interface_.set_heap_position(h, -1);
49✔
47
    }
49✔
48

49
    // is an entry in the heap?
50
    bool is_stored(HeapEntry h)
164✔
51
    {
52
        return interface_.get_heap_position(h) != -1;
164✔
53
    }
54

55
    // insert the entry h
56
    void insert(HeapEntry h)
30✔
57
    {
58
        This::push_back(h);
30✔
59
        upheap(size() - 1);
30✔
60
    }
30✔
61

62
    // get the first entry
63
    HeapEntry front()
27✔
64
    {
65
        assert(!empty());
27✔
66
        return entry(0);
27✔
67
    }
68

69
    // delete the first entry
70
    void pop_front()
27✔
71
    {
72
        assert(!empty());
27✔
73
        interface_.set_heap_position(entry(0), -1);
27✔
74
        if (size() > 1)
27✔
75
        {
76
            entry(0, entry(size() - 1));
24✔
77
            HeapVector::resize(size() - 1);
24✔
78
            downheap(0);
24✔
79
        }
80
        else
81
            HeapVector::resize(size() - 1);
3✔
82
    }
27✔
83

84
    // remove an entry
85
    void remove(HeapEntry h)
3✔
86
    {
87
        const int pos = interface_.get_heap_position(h);
3✔
88
        interface_.set_heap_position(h, -1);
3✔
89

90
        assert(pos != -1);
3✔
91
        assert((unsigned int)pos < size());
3✔
92

93
        // last item ?
94
        if ((unsigned int)pos == size() - 1)
3✔
95
            HeapVector::resize(size() - 1);
1✔
96

97
        else
98
        {
99
            entry(pos, entry(size() - 1)); // move last elem to pos
2✔
100
            HeapVector::resize(size() - 1);
2✔
101
            downheap(pos);
2✔
102
            upheap(pos);
2✔
103
        }
104
    }
3✔
105

106
    // update an entry: change the key and update the position to
107
    // reestablish the heap property.
108
    void update(HeapEntry h)
42✔
109
    {
110
        const int pos = interface_.get_heap_position(h);
42✔
111
        assert(pos != -1);
42✔
112
        assert((unsigned int)pos < size());
42✔
113
        downheap(pos);
42✔
114
        upheap(pos);
42✔
115
    }
42✔
116

117
    // Check heap condition. true if heap condition is satisfied, false if not.
118
    bool check()
119
    {
120
        bool ok(true);
121
        unsigned int i, j;
122
        for (i = 0; i < size(); ++i)
123
        {
124
            if (((j = left(i)) < size()) &&
125
                interface_.greater(entry(i), entry(j)))
126
            {
127
                ok = false;
128
            }
129
            if (((j = right(i)) < size()) &&
130
                interface_.greater(entry(i), entry(j)))
131
            {
132
                ok = false;
133
            }
134
        }
135
        return ok;
136
    }
137

138
private:
139
    using HeapVector = std::vector<HeapEntry>;
140

141
    // Upheap. Establish heap property.
142
    void upheap(unsigned int idx)
74✔
143
    {
144
        HeapEntry h = entry(idx);
74✔
145
        unsigned int parent_idx;
146

147
        while ((idx > 0) && interface_.less(h, entry(parent_idx = parent(idx))))
76✔
148
        {
149
            entry(idx, entry(parent_idx));
2✔
150
            idx = parent_idx;
2✔
151
        }
152

153
        entry(idx, h);
74✔
154
    }
74✔
155

156
    // Downheap. Establish heap property.
157
    void downheap(unsigned int idx)
68✔
158
    {
159
        HeapEntry h = entry(idx);
68✔
160
        unsigned int child_idx;
161
        const unsigned int s = size();
68✔
162

163
        while (idx < s)
151✔
164
        {
165
            child_idx = left(idx);
151✔
166
            if (child_idx >= s)
151✔
167
                break;
67✔
168

169
            if ((child_idx + 1 < s) &&
152✔
170
                (interface_.less(entry(child_idx + 1), entry(child_idx))))
68✔
171
                ++child_idx;
×
172

173
            if (interface_.less(h, entry(child_idx)))
84✔
174
                break;
1✔
175

176
            entry(idx, entry(child_idx));
83✔
177
            idx = child_idx;
83✔
178
        }
179

180
        entry(idx, h);
68✔
181
    }
68✔
182

183
    // Get the entry at index idx
184
    inline HeapEntry entry(unsigned int idx)
589✔
185
    {
186
        assert(idx < size());
589✔
187
        return (This::operator[](idx));
589✔
188
    }
189

190
    // Set entry H to index idx and update H's heap position.
191
    inline void entry(unsigned int idx, HeapEntry h)
253✔
192
    {
193
        assert(idx < size());
253✔
194
        This::operator[](idx) = h;
253✔
195
        interface_.set_heap_position(h, idx);
253✔
196
    }
253✔
197

198
    // Get parent's index
199
    inline unsigned int parent(unsigned int i) { return (i - 1) >> 1; }
62✔
200

201
    // Get left child's index
202
    inline unsigned int left(unsigned int i) { return (i << 1) + 1; }
151✔
203

204
    // Get right child's index
205
    inline unsigned int right(unsigned int i) { return (i << 1) + 2; }
206

207
    // Instance of HeapInterface
208
    HeapInterface interface_;
209
};
210

211
// Store a quadric as a symmetric 4x4 matrix.
212
class Quadric
213
{
214
public: // clang-format off
215

216
    // construct quadric from upper triangle of symmetric 4x4 matrix
217
    Quadric(double a, double b, double c, double d,
218
            double e, double f, double g,
219
            double h, double i,
220
            double j)
221
        : a_(a), b_(b), c_(c), d_(d),
222
          e_(e), f_(f), g_(g),
223
          h_(h), i_(i),
224
          j_(j)
225
    {}
226

227
    // constructor quadric from given plane equation: ax+by+cz+d=0
228
    Quadric(double a=0.0, double b=0.0, double c=0.0, double d=0.0)
222✔
229
        :  a_(a*a), b_(a*b), c_(a*c),  d_(a*d),
222✔
230
           e_(b*b), f_(b*c), g_(b*d),
222✔
231
           h_(c*c), i_(c*d),
222✔
232
           j_(d*d)
222✔
233
    {}
222✔
234

235
    // construct from point and normal specifying a plane
236
    Quadric(const Normal& n, const Point& p)
219✔
237
    {
219✔
238
        *this = Quadric(n[0], n[1], n[2], -dot(n,p));
219✔
239
    }
219✔
240

241
    // set all matrix entries to zero
242
    void clear() { a_ = b_ = c_ = d_ = e_ = f_ = g_ = h_ = i_ = j_ = 0.0; }
49✔
243

244
    // add given quadric to this quadric
245
    Quadric& operator+=(const Quadric& q)
437✔
246
    {
247
        a_ += q.a_; b_ += q.b_; c_ += q.c_; d_ += q.d_;
437✔
248
        e_ += q.e_; f_ += q.f_; g_ += q.g_;
437✔
249
        h_ += q.h_; i_ += q.i_;
437✔
250
        j_ += q.j_;
437✔
251
        return *this;
437✔
252
    }
253

254
    // multiply quadric by a scalar
255
    Quadric& operator*=(double s)
256
    {
257
        a_ *= s; b_ *= s; c_ *= s;  d_ *= s;
258
        e_ *= s; f_ *= s; g_ *= s;
259
        h_ *= s; i_ *= s;
260
        j_ *= s;
261
        return *this;
262
    }
263

264
    // evaluate quadric Q at position p by computing (p^T * Q * p)
265
    double operator()(const Point& p) const
192✔
266
    {
267
        const double x(p[0]), y(p[1]), z(p[2]);
192✔
268
        return a_*x*x + 2.0*b_*x*y + 2.0*c_*x*z + 2.0*d_*x
192✔
269
            +  e_*y*y + 2.0*f_*y*z + 2.0*g_*y
192✔
270
            +  h_*z*z + 2.0*i_*z
192✔
271
            +  j_;
192✔
272
    }
273

274
private:
275

276
    double a_, b_, c_, d_,
277
        e_, f_, g_,
278
        h_, i_,
279
        j_;
280
}; // clang-format on
281

282
class NormalCone
283
{
284
public:
285
    NormalCone() = default;
286

287
    // Initialize cone with center (unit vector) and angle (radius in radians)
288
    NormalCone(const Normal& normal, Scalar angle = 0.0)
827✔
289
        : center_normal_(normal), angle_(angle)
827✔
290
    {
291
    }
827✔
292

293
    // returns center normal
294
    const Normal& center_normal() const { return center_normal_; }
295

296
    // returns size of cone (radius in radians)
297
    Scalar angle() const { return angle_; }
629✔
298

299
    // merge *this with n.
300
    NormalCone& merge(const Normal& n) { return merge(NormalCone(n)); }
760✔
301

302
    // merge *this with nc. *this will then enclose both cones.
303
    NormalCone& merge(const NormalCone& nc)
1,180✔
304
    {
305
        const Scalar dp = dot(center_normal_, nc.center_normal_);
1,180✔
306

307
        // axes point in same direction
308
        if (dp > 0.99999)
1,180✔
309
        {
310
            angle_ = std::max(angle_, nc.angle_);
1,027✔
311
        }
312

313
        // axes point in opposite directions
314
        else if (dp < -0.99999)
153✔
315
        {
316
            angle_ = Scalar(2 * std::numbers::pi);
×
317
        }
318

319
        else
320
        {
321
            // new angle
322
            const Scalar center_angle = std::acos(dp);
153✔
323
            const Scalar min_angle =
324
                std::min(-angle_, center_angle - nc.angle_);
153✔
325
            const Scalar max_angle = std::max(angle_, center_angle + nc.angle_);
153✔
326
            angle_ = Scalar(0.5) * (max_angle - min_angle);
153✔
327

328
            // axis by SLERP
329
            const Scalar axis_angle = Scalar(0.5) * (min_angle + max_angle);
153✔
330
            center_normal_ =
331
                ((center_normal_ * std::sin(center_angle - axis_angle) +
153✔
332
                  nc.center_normal_ * std::sin(axis_angle)) /
459✔
333
                 std::sin(center_angle));
306✔
334
        }
335

336
        return *this;
1,180✔
337
    }
338

339
private:
340
    Normal center_normal_;
341
    Scalar angle_;
342
};
343

344
class Decimation
345
{
346
public:
347
    Decimation(SurfaceMesh& mesh);
348
    ~Decimation();
349
    void initialize(Scalar aspect_ratio = 0.0, Scalar edge_length = 0.0,
350
                    unsigned int max_valence = 0, Scalar normal_deviation = 0.0,
351
                    Scalar hausdorff_error = 0.0, Scalar seam_threshold = 1e-2,
352
                    Scalar seam_angle_deviation = 1);
353
    void decimate(unsigned int n_vertices);
354

355
private:
356
    // Store data for an halfedge collapse
357
    struct CollapseData
358
    {
359
        CollapseData(SurfaceMesh& sm, Halfedge h);
360

361
        SurfaceMesh& mesh;
362

363
        /*        vl
364
         *        *
365
         *       / \
366
         *      /   \
367
         *     / fl  \
368
         * v0 *------>* v1
369
         *     \ fr  /
370
         *      \   /
371
         *       \ /
372
         *        *
373
         *        vr
374
         */
375
        Halfedge v0v1; // Halfedge to be collapsed
376
        Halfedge v1v0; // Reverse halfedge
377
        Vertex v0;     // Vertex to be removed
378
        Vertex v1;     // Remaining vertex
379
        Face fl;       // Left face
380
        Face fr;       // Right face
381
        Vertex vl;     // Left vertex
382
        Vertex vr;     // Right vertex
383
        Halfedge v1vl, vlv0, v0vr, vrv1;
384
    };
385

386
    // Heap interface
387
    class HeapInterface
388
    {
389
    public:
390
        HeapInterface(VertexProperty<float> prio, VertexProperty<int> pos)
3✔
391
            : prio_(prio), pos_(pos)
3✔
392
        {
393
        }
3✔
394

395
        bool less(Vertex v0, Vertex v1) { return prio_[v0] < prio_[v1]; }
214✔
396
        bool greater(Vertex v0, Vertex v1) { return prio_[v0] > prio_[v1]; }
397
        int get_heap_position(Vertex v) { return pos_[v]; }
209✔
398
        void set_heap_position(Vertex v, int pos) { pos_[v] = pos; }
332✔
399

400
    private:
401
        VertexProperty<float> prio_;
402
        VertexProperty<int> pos_;
403
    };
404

405
    using PriorityQueue = Heap<Vertex, HeapInterface>;
406

407
    using Points = std::vector<Point>;
408

409
    // put the vertex v in the priority queue
410
    void enqueue_vertex(PriorityQueue& queue, Vertex v);
411

412
    // is collapsing the halfedge h allowed?
413
    bool is_collapse_legal(const CollapseData& cd);
414

415
    // are texture seams preserved if h is collapsed?
416
    bool texcoord_check(Halfedge h);
417

418
    // what is the priority of collapsing the halfedge h
419
    float priority(const CollapseData& cd);
420

421
    // preprocess halfedge collapse
422
    void preprocess_collapse(const CollapseData& cd);
423

424
    // postprocess halfedge collapse
425
    void postprocess_collapse(const CollapseData& cd);
426

427
    // compute aspect ratio for face f
428
    Scalar aspect_ratio(Face f) const;
429

430
    // compute distance from point p to triangle f
431
    Scalar distance(Face f, const Point& p) const;
432

433
    SurfaceMesh& mesh_;
434

435
    bool initialized_{false};
436

437
    VertexProperty<float> vpriority_;
438
    VertexProperty<Halfedge> vtarget_;
439
    VertexProperty<int> heap_pos_;
440
    VertexProperty<Quadric> vquadric_;
441
    FaceProperty<NormalCone> normal_cone_;
442
    FaceProperty<Points> face_points_;
443

444
    VertexProperty<Point> vpoint_;
445
    FaceProperty<Point> fnormal_;
446
    VertexProperty<bool> vselected_;
447
    VertexProperty<bool> vfeature_;
448
    EdgeProperty<bool> efeature_;
449
    EdgeProperty<bool> texture_seams_;
450

451
    bool has_selection_{false};
452
    bool has_features_{false};
453
    Scalar normal_deviation_;
454
    Scalar hausdorff_error_;
455
    Scalar aspect_ratio_;
456
    Scalar edge_length_;
457
    Scalar seam_threshold_;
458
    Scalar seam_angle_deviation_;
459
    unsigned int max_valence_;
460
};
461

462
Decimation::Decimation(SurfaceMesh& mesh) : mesh_(mesh)
3✔
463
{
464
    if (!mesh_.is_triangle_mesh())
3✔
465
        throw InvalidInputException("Input is not a triangle mesh!");
×
466

467
    aspect_ratio_ = 0;
3✔
468
    edge_length_ = 0;
3✔
469
    max_valence_ = 0;
3✔
470
    normal_deviation_ = 0;
3✔
471
    hausdorff_error_ = 0;
3✔
472

473
    seam_threshold_ = 1e-2;
3✔
474
    seam_angle_deviation_ = 0.99;
3✔
475

476
    // add properties
477
    vquadric_ = mesh_.add_vertex_property<Quadric>("v:quadric");
6✔
478
    texture_seams_ = mesh_.edge_property<bool>("e:seam", false);
6✔
479

480
    // get properties
481
    vpoint_ = mesh_.vertex_property<Point>("v:point");
12✔
482

483
    // compute face normals
484
    face_normals(mesh_);
3✔
485
    fnormal_ = mesh_.face_property<Normal>("f:normal");
12✔
486
}
3✔
487

488
Decimation::~Decimation()
3✔
489
{
490
    // remove added properties
491
    mesh_.remove_vertex_property(vquadric_);
3✔
492
    mesh_.remove_face_property(normal_cone_);
3✔
493
    mesh_.remove_face_property(face_points_);
3✔
494
    mesh_.remove_edge_property(texture_seams_);
3✔
495
}
3✔
496

497
void Decimation::initialize(Scalar aspect_ratio, Scalar edge_length,
3✔
498
                            unsigned int max_valence, Scalar normal_deviation,
499
                            Scalar hausdorff_error, Scalar seam_threshold,
500
                            Scalar seam_angle_deviation)
501
{
502
    // store parameters
503
    aspect_ratio_ = aspect_ratio;
3✔
504
    max_valence_ = max_valence;
3✔
505
    edge_length_ = edge_length;
3✔
506
    normal_deviation_ = normal_deviation / 180.0 * std::numbers::pi;
3✔
507
    hausdorff_error_ = hausdorff_error;
3✔
508
    seam_threshold_ = seam_threshold;
3✔
509
    seam_angle_deviation_ = (180.0 - seam_angle_deviation) / 180.0;
3✔
510

511
    // properties
512
    if (normal_deviation_ > 0.0)
3✔
513
        normal_cone_ = mesh_.face_property<NormalCone>("f:normalCone");
10✔
514
    else
515
        mesh_.remove_face_property(normal_cone_);
1✔
516
    if (hausdorff_error > 0.0)
3✔
517
        face_points_ = mesh_.face_property<Points>("f:points");
×
518
    else
519
        mesh_.remove_face_property(face_points_);
3✔
520

521
    // vertex selection
522
    has_selection_ = false;
3✔
523
    vselected_ = mesh_.get_vertex_property<bool>("v:selected");
6✔
524
    if (vselected_)
3✔
525
    {
526
        for (auto v : mesh_.vertices())
×
527
        {
528
            if (vselected_[v])
×
529
            {
530
                has_selection_ = true;
×
531
                break;
×
532
            }
533
        }
534
    }
535

536
    // feature vertices/edges
537
    has_features_ = false;
3✔
538
    vfeature_ = mesh_.get_vertex_property<bool>("v:feature");
6✔
539
    efeature_ = mesh_.get_edge_property<bool>("e:feature");
6✔
540
    if (vfeature_ && efeature_)
3✔
541
    {
542
        for (auto v : mesh_.vertices())
1✔
543
        {
544
            if (vfeature_[v])
1✔
545
            {
546
                has_features_ = true;
1✔
547
                break;
1✔
548
            }
549
        }
550
    }
551

552
    // initialize quadrics
553
    for (auto v : mesh_.vertices())
52✔
554
    {
555
        vquadric_[v].clear();
49✔
556

557
        if (!mesh_.is_isolated(v))
49✔
558
        {
559
            for (auto f : mesh_.faces(v))
268✔
560
            {
561
                vquadric_[v] += Quadric(fnormal_[f], vpoint_[v]);
219✔
562
            }
563
        }
564
    }
565

566
    // initialize normal cones
567
    if (normal_deviation_)
3✔
568
    {
569
        for (auto f : mesh_.faces())
69✔
570
        {
571
            normal_cone_[f] = NormalCone(fnormal_[f]);
67✔
572
        }
573
    }
574

575
    // initialize faces' point list
576
    if (hausdorff_error_)
3✔
577
    {
578
        for (auto f : mesh_.faces())
×
579
        {
580
            Points().swap(face_points_[f]); // free mem
×
581
        }
582
    }
583

584
    // detect texture seams
585
    auto texcoords = mesh_.get_halfedge_property<TexCoord>("h:tex");
6✔
586
    if (texcoords)
3✔
587
    {
588
        for (auto e : mesh_.edges())
35✔
589
        {
590
            // texcoords are stored in halfedge pointing towards a vertex
591
            const Halfedge h0 = mesh_.halfedge(e, 0);
34✔
592
            const Halfedge h1 = mesh_.halfedge(e, 1);     //opposite halfedge
34✔
593
            const Halfedge h0p = mesh_.prev_halfedge(h0); // start point edge 0
34✔
594
            const Halfedge h1p = mesh_.prev_halfedge(h1); // start point edge 1
34✔
595

596
            // if start or end points differ more than seam_threshold
597
            // the corresponding edge is a texture seam
598
            if (norm(texcoords[h1] - texcoords[h0p]) > seam_threshold_ ||
53✔
599
                norm(texcoords[h0] - texcoords[h1p]) > seam_threshold_)
53✔
600
            {
601
                texture_seams_[e] = true;
15✔
602
            }
603
            else
604
            {
605
                texture_seams_[e] = false;
19✔
606
            }
607
        }
608
    }
609

610
    initialized_ = true;
3✔
611
}
3✔
612

613
void Decimation::decimate(unsigned int n_vertices)
3✔
614
{
615
    // make sure the decimater is initialized
616
    if (!initialized_)
3✔
617
        initialize();
×
618

619
    std::vector<Vertex> one_ring;
3✔
620

621
    // add properties for priority queue
622
    vpriority_ = mesh_.add_vertex_property<float>("v:prio");
6✔
623
    heap_pos_ = mesh_.add_vertex_property<int>("v:heap");
6✔
624
    vtarget_ = mesh_.add_vertex_property<Halfedge>("v:target");
6✔
625

626
    // build priority queue
627
    const HeapInterface hi(vpriority_, heap_pos_);
3✔
628
    PriorityQueue queue(hi);
3✔
629
    queue.reserve(mesh_.n_vertices());
3✔
630
    for (auto v : mesh_.vertices())
52✔
631
    {
632
        queue.reset_heap_position(v);
49✔
633
        enqueue_vertex(queue, v);
49✔
634
    }
635

636
    auto nv = mesh_.n_vertices();
3✔
637
    while (nv > n_vertices && !queue.empty())
30✔
638
    {
639
        // get 1st element
640
        auto v = queue.front();
27✔
641
        queue.pop_front();
27✔
642
        auto h = vtarget_[v];
27✔
643
        const CollapseData cd(mesh_, h);
27✔
644

645
        // check this (again)
646
        if (!mesh_.is_collapse_ok(h))
27✔
647
            continue;
1✔
648

649
        // are texture seams preserved?
650
        if (!texcoord_check(cd.v0v1))
26✔
651
            continue;
×
652

653
        // store one-ring
654
        one_ring.clear();
26✔
655
        for (auto vv : mesh_.vertices(cd.v0))
141✔
656
        {
657
            one_ring.push_back(vv);
115✔
658
        }
659

660
        // preprocessing -> adjust texcoords
661
        preprocess_collapse(cd);
26✔
662

663
        // perform collapse
664
        mesh_.collapse(h);
26✔
665
        --nv;
26✔
666

667
        // postprocessing, e.g., update quadrics
668
        postprocess_collapse(cd);
26✔
669

670
        // update queue
671
        for (auto vv : one_ring)
141✔
672
            enqueue_vertex(queue, vv);
115✔
673
    }
674

675
    // clean up
676
    mesh_.garbage_collection();
3✔
677
    mesh_.remove_vertex_property(vpriority_);
3✔
678
    mesh_.remove_vertex_property(heap_pos_);
3✔
679
    mesh_.remove_vertex_property(vtarget_);
3✔
680
}
3✔
681

682
void Decimation::enqueue_vertex(PriorityQueue& queue, Vertex v)
164✔
683
{
684
    float prio, min_prio(std::numeric_limits<float>::max());
164✔
685
    Halfedge min_h;
164✔
686

687
    for (auto h : mesh_.halfedges(v))
1,790✔
688
    {
689
        const CollapseData cd(mesh_, h);
813✔
690
        if (is_collapse_legal(cd))
813✔
691
        {
692
            prio = priority(cd);
192✔
693
            if (prio != -1.0 && prio < min_prio)
192✔
694
            {
695
                min_prio = prio;
76✔
696
                min_h = h;
76✔
697
            }
698
        }
699
    }
700

701
    // target found -> put vertex on heap
702
    if (min_h.is_valid())
164✔
703
    {
704
        vpriority_[v] = min_prio;
72✔
705
        vtarget_[v] = min_h;
72✔
706

707
        if (queue.is_stored(v))
72✔
708
            queue.update(v);
42✔
709
        else
710
            queue.insert(v);
30✔
711
    }
712

713
    // not valid -> remove from heap
714
    else
715
    {
716
        if (queue.is_stored(v))
92✔
717
            queue.remove(v);
3✔
718

719
        vpriority_[v] = -1;
92✔
720
        vtarget_[v] = min_h;
92✔
721
    }
722
}
164✔
723

724
bool Decimation::is_collapse_legal(const CollapseData& cd)
813✔
725
{
726
    // test selected vertices
727
    if (has_selection_)
813✔
728
    {
729
        if (!vselected_[cd.v0])
×
730
            return false;
×
731
    }
732

733
    // test features
734
    if (has_features_)
813✔
735
    {
736
        if (vfeature_[cd.v0] && !efeature_[mesh_.edge(cd.v0v1)])
603✔
737
            return false;
268✔
738

739
        if (cd.vl.is_valid() && efeature_[mesh_.edge(cd.vlv0)])
335✔
740
            return false;
75✔
741

742
        if (cd.vr.is_valid() && efeature_[mesh_.edge(cd.v0vr)])
260✔
743
            return false;
58✔
744
    }
745

746
    // do not collapse boundary vertices to interior vertices
747
    if (mesh_.is_boundary(cd.v0) && !mesh_.is_boundary(cd.v1))
412✔
748
        return false;
40✔
749

750
    // there should be at least 2 incident faces at v0
751
    if (mesh_.cw_rotated_halfedge(mesh_.cw_rotated_halfedge(cd.v0v1)) ==
372✔
752
        cd.v0v1)
753
        return false;
10✔
754

755
    // topological check
756
    if (!mesh_.is_collapse_ok(cd.v0v1))
362✔
757
        return false;
17✔
758

759
    // are texture seams preserved?
760
    if (!texcoord_check(cd.v0v1))
345✔
761
        return false;
86✔
762

763
    // check maximal valence
764
    if (max_valence_ > 0)
259✔
765
    {
766
        auto val0 = mesh_.valence(cd.v0);
×
767
        auto val1 = mesh_.valence(cd.v1);
×
768
        auto val = val0 + val1 - 1;
×
769
        if (cd.fl.is_valid())
×
770
            --val;
×
771
        if (cd.fr.is_valid())
×
772
            --val;
×
773
        if (val > max_valence_ && !(val < std::max(val0, val1)))
×
774
            return false;
×
775
    }
776

777
    // remember the positions of the endpoints
778
    const Point p0 = vpoint_[cd.v0];
259✔
779
    const Point p1 = vpoint_[cd.v1];
259✔
780

781
    // check for maximum edge length
782
    if (edge_length_)
259✔
783
    {
784
        for (auto v : mesh_.vertices(cd.v0))
×
785
        {
786
            if (v != cd.v1 && v != cd.vl && v != cd.vr)
×
787
            {
788
                if (norm(vpoint_[v] - p1) > edge_length_)
×
789
                    return false;
×
790
            }
791
        }
792
    }
793

794
    // check for flipping normals
795
    if (normal_deviation_ == 0.0)
259✔
796
    {
797
        vpoint_[cd.v0] = p1;
38✔
798
        for (auto f : mesh_.faces(cd.v0))
284✔
799
        {
800
            if (f != cd.fl && f != cd.fr)
125✔
801
            {
802
                const Normal n0 = fnormal_[f];
70✔
803
                const Normal n1 = face_normal(mesh_, f);
70✔
804
                if (dot(n0, n1) < 0.0)
70✔
805
                {
806
                    vpoint_[cd.v0] = p0;
2✔
807
                    return false;
2✔
808
                }
809
            }
810
        }
811
        vpoint_[cd.v0] = p0;
36✔
812
    }
813

814
    // check normal cone
815
    else
816
    {
817
        vpoint_[cd.v0] = p1;
221✔
818

819
        Face fll, frr;
221✔
820
        if (cd.vl.is_valid())
221✔
821
            fll = mesh_.face(
221✔
822
                mesh_.opposite_halfedge(mesh_.prev_halfedge(cd.v0v1)));
221✔
823
        if (cd.vr.is_valid())
221✔
824
            frr = mesh_.face(
221✔
825
                mesh_.opposite_halfedge(mesh_.next_halfedge(cd.v1v0)));
221✔
826

827
        for (auto f : mesh_.faces(cd.v0))
2,071✔
828
        {
829
            if (f != cd.fl && f != cd.fr)
990✔
830
            {
831
                NormalCone nc = normal_cone_[f];
629✔
832
                nc.merge(face_normal(mesh_, f));
629✔
833

834
                if (f == fll)
629✔
835
                    nc.merge(normal_cone_[cd.fl]);
201✔
836
                if (f == frr)
629✔
837
                    nc.merge(normal_cone_[cd.fr]);
175✔
838

839
                if (nc.angle() > 0.5 * normal_deviation_)
629✔
840
                {
841
                    vpoint_[cd.v0] = p0;
65✔
842
                    return false;
65✔
843
                }
844
            }
845
        }
846

847
        vpoint_[cd.v0] = p0;
156✔
848
    }
849

850
    // check aspect ratio
851
    if (aspect_ratio_)
192✔
852
    {
853
        Scalar ar0(0), ar1(0);
19✔
854

855
        for (auto f : mesh_.faces(cd.v0))
197✔
856
        {
857
            if (f != cd.fl && f != cd.fr)
89✔
858
            {
859
                // worst aspect ratio after collapse
860
                vpoint_[cd.v0] = p1;
51✔
861
                ar1 = std::max(ar1, aspect_ratio(f));
51✔
862
                // worst aspect ratio before collapse
863
                vpoint_[cd.v0] = p0;
51✔
864
                ar0 = std::max(ar0, aspect_ratio(f));
51✔
865
            }
866
        }
867

868
        // aspect ratio is too bad, and it does also not improve
869
        if (ar1 > aspect_ratio_ && ar1 > ar0)
19✔
870
            return false;
×
871
    }
872

873
    // check Hausdorff error
874
    if (hausdorff_error_)
192✔
875
    {
876
        Points points;
×
877
        bool ok;
878

879
        // collect points to be tested
880
        for (auto f : mesh_.faces(cd.v0))
×
881
        {
882
            std::copy(face_points_[f].begin(), face_points_[f].end(),
×
883
                      std::back_inserter(points));
884
        }
885
        points.push_back(vpoint_[cd.v0]);
×
886

887
        // test points against all faces
888
        vpoint_[cd.v0] = p1;
×
889
        for (auto point : points)
×
890
        {
891
            ok = false;
×
892

893
            for (auto f : mesh_.faces(cd.v0))
×
894
            {
895
                if (f != cd.fl && f != cd.fr)
×
896
                {
897
                    if (distance(f, point) < hausdorff_error_)
×
898
                    {
899
                        ok = true;
×
900
                        break;
×
901
                    }
902
                }
903
            }
904

905
            if (!ok)
×
906
            {
907
                vpoint_[cd.v0] = p0;
×
908
                return false;
×
909
            }
910
        }
911
        vpoint_[cd.v0] = p0;
×
912
    }
×
913

914
    // collapse passed all tests -> ok
915
    return true;
192✔
916
}
917

918
bool Decimation::texcoord_check(Halfedge h)
371✔
919
{
920
    auto texcoords = mesh_.get_halfedge_property<TexCoord>("h:tex");
742✔
921
    if (!texcoords)
371✔
922
    {
923
        // no texture coordinates -> skip texture seam tests
924
        return true;
262✔
925
    }
926

927
    auto texture_seams = mesh_.edge_property<bool>("e:seam");
218✔
928
    if (!texture_seams)
109✔
929
    {
930
        // no seams found -> skip seam tests
931
        return true;
×
932
    }
933

934
    const Halfedge o(mesh_.opposite_halfedge(h));
109✔
935
    const Vertex v0(mesh_.to_vertex(o));
109✔
936

937
    if (!texture_seams[mesh_.edge(h)])
109✔
938
    {
939
        // v0v1 is not a texture seam
940
        for (auto he : mesh_.halfedges(v0))
213✔
941
        {
942
            if (he == h)
200✔
943
                continue;
28✔
944
            // Check if v0 is part of a texture seam
945
            // If yes, v0 must not be moved
946
            if (texture_seams[mesh_.edge(he)])
172✔
947
            {
948
                return false;
38✔
949
            }
950
        }
951

952
        return true;
13✔
953
    }
954

955
    // count number of adjacent texture seam edges
956
    int nr_seam_edges = 0;
58✔
957
    for (auto he : mesh_.halfedges(v0))
326✔
958
    {
959
        if (texture_seams[mesh_.edge(he)])
268✔
960
        {
961
            nr_seam_edges++;
124✔
962
        }
963
    }
964

965
    // if there are more than 2 seam edges at point v0
966
    // -> v0 must not be moved
967
    if (nr_seam_edges > 2)
58✔
968
    {
969
        return false;
8✔
970
    }
971

972
    const Halfedge seam1 = h;
50✔
973
    Halfedge seam2 = mesh_.prev_halfedge(h);
50✔
974
    while (seam2.idx() != o.idx())
135✔
975
    {
976
        if (texture_seams[mesh_.edge(seam2)])
125✔
977
        {
978
            auto s1 = normalize(texcoords[seam1] -
50✔
979
                                texcoords[mesh_.prev_halfedge(seam1)]);
50✔
980
            auto s2 = normalize(texcoords[seam2] -
50✔
981
                                texcoords[mesh_.prev_halfedge(seam2)]);
50✔
982

983
            // opposite uvs
984
            const Halfedge o_seam1 = mesh_.opposite_halfedge(seam1);
50✔
985
            const Halfedge o_seam2 = mesh_.opposite_halfedge(seam2);
50✔
986
            auto o1 = normalize(texcoords[o_seam1] -
50✔
987
                                texcoords[mesh_.prev_halfedge(o_seam1)]);
50✔
988
            auto o2 = normalize(texcoords[o_seam2] -
50✔
989
                                texcoords[mesh_.prev_halfedge(o_seam2)]);
50✔
990

991
            // check if the angle between the seam edge to be collapsed and the
992
            // seam edge prolonged is smaller than the allowed deviation
993
            if (dot(s1, s2) < seam_angle_deviation_ ||
70✔
994
                dot(o1, o2) < seam_angle_deviation_)
20✔
995
            {
996
                // angle is too large -> don't collapse this edge
997
                return false;
40✔
998
            }
999
        }
1000
        seam2 = mesh_.prev_halfedge(mesh_.opposite_halfedge(seam2));
85✔
1001
    }
1002

1003
    // passed all tests
1004
    return true;
10✔
1005
}
1006

1007
float Decimation::priority(const CollapseData& cd)
192✔
1008
{
1009
    // computer quadric error metric
1010
    Quadric Q = vquadric_[cd.v0];
192✔
1011
    Q += vquadric_[cd.v1];
192✔
1012
    return Q(vpoint_[cd.v1]);
384✔
1013
}
1014

1015
void Decimation::preprocess_collapse(const CollapseData& cd)
26✔
1016
{
1017
    const Halfedge h = cd.v0v1;
26✔
1018
    const Halfedge o = mesh_.opposite_halfedge(h);
26✔
1019
    Halfedge v1v2, v2v1, v0v2;
26✔
1020

1021
    // move texcoords in correct halfedge before collapsing an edge
1022
    auto texcoords = mesh_.get_halfedge_property<TexCoord>("h:tex");
52✔
1023
    if (texcoords)
26✔
1024
    {
1025
        auto texture_seams = mesh_.edge_property<bool>("e:seam", false);
8✔
1026
        Halfedge hit = h;
4✔
1027
        bool is_first_side = true;
4✔
1028

1029
        // which texcoord must be saved depends
1030
        // on the side of the texture seam
1031
        for (size_t i = 0; i < mesh_.valence(mesh_.to_vertex(o)) - 1; ++i)
17✔
1032
        {
1033
            hit = mesh_.prev_halfedge(hit);
13✔
1034
            if (is_first_side)
13✔
1035
                texcoords[hit] = texcoords[h];
12✔
1036
            else if (!is_first_side)
1✔
1037
                texcoords[hit] = texcoords[mesh_.prev_halfedge(o)];
1✔
1038
            if (texture_seams[mesh_.edge(hit)])
13✔
1039
            {
1040
                is_first_side = false;
2✔
1041

1042
                // loop case 1
1043
                if (mesh_.to_vertex(mesh_.next_halfedge(h)) ==
2✔
1044
                    mesh_.from_vertex(hit))
4✔
1045
                {
1046
                    v1v2 = mesh_.next_halfedge(h);
1✔
1047
                    texcoords[mesh_.opposite_halfedge(v1v2)] = texcoords[hit];
1✔
1048
                    texcoords[v1v2] = texcoords[mesh_.opposite_halfedge(hit)];
1✔
1049
                    texture_seams[mesh_.edge(v1v2)] = true;
1✔
1050
                }
1051

1052
                // loop case 2
1053
                if (mesh_.to_vertex(mesh_.next_halfedge(o)) ==
2✔
1054
                    mesh_.from_vertex(hit))
4✔
1055
                {
1056
                    v2v1 = mesh_.prev_halfedge(o);
1✔
1057
                    v0v2 = mesh_.opposite_halfedge(hit);
1✔
1058
                    texcoords[mesh_.opposite_halfedge(v2v1)] = texcoords[v0v2];
1✔
1059
                    texcoords[v2v1] = texcoords[hit];
1✔
1060
                    texture_seams[mesh_.edge(v2v1)] = true;
1✔
1061
                }
1062
            }
1063
            hit = mesh_.opposite_halfedge(hit);
13✔
1064
        }
1065
    }
1066
}
26✔
1067

1068
void Decimation::postprocess_collapse(const CollapseData& cd)
26✔
1069
{
1070
    // update error quadrics
1071
    vquadric_[cd.v1] += vquadric_[cd.v0];
26✔
1072

1073
    // update normal cones
1074
    if (normal_deviation_)
26✔
1075
    {
1076
        for (auto f : mesh_.faces(cd.v1))
153✔
1077
        {
1078
            normal_cone_[f].merge(face_normal(mesh_, f));
131✔
1079
        }
1080

1081
        if (cd.vl.is_valid())
22✔
1082
        {
1083
            const Face f = mesh_.face(cd.v1vl);
22✔
1084
            if (f.is_valid())
22✔
1085
                normal_cone_[f].merge(normal_cone_[cd.fl]);
22✔
1086
        }
1087

1088
        if (cd.vr.is_valid())
22✔
1089
        {
1090
            const Face f = mesh_.face(cd.vrv1);
22✔
1091
            if (f.is_valid())
22✔
1092
                normal_cone_[f].merge(normal_cone_[cd.fr]);
22✔
1093
        }
1094
    }
1095

1096
    // update Hausdorff error
1097
    if (hausdorff_error_)
26✔
1098
    {
1099
        Points points;
×
1100

1101
        // collect points to be distributed
1102

1103
        // points of v1's one-ring
1104
        for (auto f : mesh_.faces(cd.v1))
×
1105
        {
1106
            std::copy(face_points_[f].begin(), face_points_[f].end(),
×
1107
                      std::back_inserter(points));
1108
            face_points_[f].clear();
×
1109
        }
1110

1111
        // points of the 2 removed triangles
1112
        if (cd.fl.is_valid())
×
1113
        {
1114
            std::copy(face_points_[cd.fl].begin(), face_points_[cd.fl].end(),
×
1115
                      std::back_inserter(points));
1116
            Points().swap(face_points_[cd.fl]); // free mem
×
1117
        }
1118
        if (cd.fr.is_valid())
×
1119
        {
1120
            std::copy(face_points_[cd.fr].begin(), face_points_[cd.fr].end(),
×
1121
                      std::back_inserter(points));
1122
            Points().swap(face_points_[cd.fr]); // free mem
×
1123
        }
1124

1125
        // the removed vertex
1126
        points.push_back(vpoint_[cd.v0]);
×
1127

1128
        // test points against all faces
1129
        Scalar d, dd;
1130
        Face ff;
×
1131

1132
        for (auto point : points)
×
1133
        {
1134
            dd = std::numeric_limits<Scalar>::max();
×
1135

1136
            for (auto f : mesh_.faces(cd.v1))
×
1137
            {
1138
                d = distance(f, point);
×
1139
                if (d < dd)
×
1140
                {
1141
                    ff = f;
×
1142
                    dd = d;
×
1143
                }
1144
            }
1145

1146
            face_points_[ff].push_back(point);
×
1147
        }
1148
    }
×
1149
}
26✔
1150

1151
Scalar Decimation::aspect_ratio(Face f) const
102✔
1152
{
1153
    // min height is area/maxLength
1154
    // aspect ratio = length / height
1155
    //              = length * length / area
1156

1157
    auto fvit = mesh_.vertices(f);
102✔
1158

1159
    const Point p0 = vpoint_[*fvit];
102✔
1160
    const Point p1 = vpoint_[*(++fvit)];
102✔
1161
    const Point p2 = vpoint_[*(++fvit)];
102✔
1162

1163
    const Point d0 = p0 - p1;
102✔
1164
    const Point d1 = p1 - p2;
102✔
1165
    const Point d2 = p2 - p0;
102✔
1166

1167
    const Scalar l0 = sqrnorm(d0);
102✔
1168
    const Scalar l1 = sqrnorm(d1);
102✔
1169
    const Scalar l2 = sqrnorm(d2);
102✔
1170

1171
    // max squared edge length
1172
    const Scalar l = std::max({l0, l1, l2});
102✔
1173

1174
    // triangle area
1175
    const Scalar a = norm(cross(d0, d1));
102✔
1176

1177
    return l / a;
102✔
1178
}
1179

1180
Scalar Decimation::distance(Face f, const Point& p) const
×
1181
{
1182
    auto fvit = mesh_.vertices(f);
×
1183

1184
    const Point p0 = vpoint_[*fvit];
×
1185
    const Point p1 = vpoint_[*(++fvit)];
×
1186
    const Point p2 = vpoint_[*(++fvit)];
×
1187

1188
    Point n;
1189

1190
    return dist_point_triangle(p, p0, p1, p2, n);
×
1191
}
1192

1193
Decimation::CollapseData::CollapseData(SurfaceMesh& sm, Halfedge h) : mesh(sm)
840✔
1194
{
1195
    v0v1 = h;
840✔
1196
    v1v0 = mesh.opposite_halfedge(v0v1);
840✔
1197
    v0 = mesh.to_vertex(v1v0);
840✔
1198
    v1 = mesh.to_vertex(v0v1);
840✔
1199
    fl = mesh.face(v0v1);
840✔
1200
    fr = mesh.face(v1v0);
840✔
1201

1202
    // get vl
1203
    if (fl.is_valid())
840✔
1204
    {
1205
        v1vl = mesh.next_halfedge(v0v1);
799✔
1206
        vlv0 = mesh.next_halfedge(v1vl);
799✔
1207
        vl = mesh.to_vertex(v1vl);
799✔
1208
    }
1209

1210
    // get vr
1211
    if (fr.is_valid())
840✔
1212
    {
1213
        v0vr = mesh.next_halfedge(v1v0);
803✔
1214
        vrv1 = mesh.prev_halfedge(v0vr);
803✔
1215
        vr = mesh.from_vertex(vrv1);
803✔
1216
    }
1217
}
840✔
1218
} // namespace
1219

1220
void decimate(SurfaceMesh& mesh, unsigned int n_vertices, Scalar aspect_ratio,
3✔
1221
              Scalar edge_length, unsigned int max_valence,
1222
              Scalar normal_deviation, Scalar hausdorff_error,
1223
              Scalar seam_threshold, Scalar seam_angle_deviation)
1224
{
1225
    Decimation decimator(mesh);
3✔
1226
    decimator.initialize(aspect_ratio, edge_length, max_valence,
3✔
1227
                         normal_deviation, hausdorff_error, seam_threshold,
1228
                         seam_angle_deviation);
1229
    decimator.decimate(n_vertices);
3✔
1230
}
3✔
1231

1232
} // namespace pmp
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc