From b87d1bbc3e0a6935010cafd576270342b882077a Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Thu, 1 Mar 2012 15:21:41 +0000 Subject: [PATCH 01/35] Very first version of the hierarchical clustering added (work in progress). It contains somes functions that may be more relevant in some other package : * 2 functions to compute the covariance matrix of a 3D point set * 2 functions to split a point set according to a plane Also the function of the actual hierachical clustering algorithm. --- .../include/CGAL/hierarchical_clustering.h | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 Point_set_processing_3/include/CGAL/hierarchical_clustering.h diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h new file mode 100644 index 00000000000..f869cfa099e --- /dev/null +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -0,0 +1,254 @@ +// Copyright (c) 2012 INRIA Sophia-Antipolis (France). +// All rights reserved. +// +// This file is part of CGAL (www.cgal.org). +// You can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, +// either version 3 of the License, or (at your option) any later version. +// +// Licensees holding a valid commercial license may use this file in +// accordance with the commercial license agreement provided with the software. +// +// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +// WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +// +// $URL: +// $Id: +// +// Author(s) : Simon Giraudot + + +#ifndef HIERARCHICAL_CLUSTERING_H +#define HIERARCHICAL_CLUSTERING_H + +#include +#include + +#include +#include +#include +#include +#include +#include + +#undef min +#undef max + + +/*****************************************************************************/ +template +typename K::FT* covariance_matrix_3 (InputIterator first, + InputIterator beyond, + typename K::Point_3 centroid) +/*****************************************************************************/ +{ + typedef typename K::FT FT; + FT *out = new FT[6]; + + for (int i = 0; i < 6; ++ i) + out[i] = (FT)(0.0); + + for (InputIterator it = first; it != beyond; ++ it) + { + const Point& p = *it; + Vector d = p - centroid; + out[0] += d.x () * d.x (); + out[1] += d.x () * d.y (); + out[2] += d.y () * d.y (); + out[3] += d.x () * d.z (); + out[4] += d.y () * d.z (); + out[5] += d.z () * d.z (); + } + + return out; +} + + + + +/*****************************************************************************/ +template +typename K::FT* covariance_matrix_3 (InputIterator first, + InputIterator beyond) +/*****************************************************************************/ +{ + return covariance_matrix_3 + (first, beyond, centroid (first, beyond)); +} + + + + +/*****************************************************************************/ +template +void point_set_split_plane_3 (InputIterator first, + InputIterator beyond, + typename K::Plane_3& plane, + OutputIterator1 points_on_positive_side, + OutputIterator2 points_on_negative_side, + OutputIterator3 points_on_plane) +/*****************************************************************************/ +{ + for (InputIterator it = first; it != beyond; ++ it) + if (plane.has_on_positive_side (*it)) + *points_on_positive_side++ = *it; + else if (plane.has_on_negative_side (*it)) + *points_on_negative_side++ = *it; + else + *points_on_plane++ = *it; +} + +/*****************************************************************************/ +template +void point_set_split_plane_3 (InputIterator first, + InputIterator beyond, + typename K::Plane_3& plane, + OutputIterator1 points_on_positive_side, + OutputIterator2 points_on_negative_side, + bool strictly_negative = true) +/*****************************************************************************/ +{ + for (InputIterator it = first; it != beyond; ++ it) + if (plane.has_on_positive_side (*it)) + *points_on_positive_side++ = *it; + else if (plane.has_on_negative_side (*it)) + *points_on_negative_side++ = *it; + else + { + if (strictly_negative) + *points_on_positive_side++ = *it; + else + *points_on_negative_side++ = *it; + } +} + + + +/*****************************************************************************/ +template +InputIterator hierarchical_clustering_3 (InputIterator first, + InputIterator beyond, + const unsigned int& size, + const typename K::FT& var_max) +/*****************************************************************************/ +{ + typedef typename K::FT FT; + typedef typename K::Plane_3 Plane; + typedef typename K::Point_3 Point; + typedef typename K::Vector_3 Vector; + + // We define a cluster as a set of points + its centroid (useful for + // faster computations of centroids - to be implemented) + typedef std::pair< std::list, Point > cluster; + + CGAL_precondition (first != beyond); + CGAL_point_set_processing_precondition (var_max >= 0.0 && var_max <= 1./3.); + + // The algorithm must return a set of points + std::list points_to_keep; + + // The first cluster is the whole set of input points + std::list first_cluster; + for (InputIterator it = first; it != beyond; ++ it) + first_cluster.push_back (*it); + + // Initialize the queue + std::queue clusters_queue; + clusters_queue.push (cluster (first_cluster, centroid (first, beyond))); + + while (!(clusters_queue.empty ())) + { + cluster current_cluster = clusters_queue.front (); + clusters_queue.pop (); + + // If the cluster only has 1 element, we add it to the list of + // output points + if (current_cluster.first.size () == 1) + { + points_to_keep.push_back (current_cluster.second); + continue; + } + + // Compute the covariance matrix of the set + FT* covariance + = covariance_matrix_3::iterator, K> + (current_cluster.first.begin (), // Beginning of the list + current_cluster.first.end(), // End of the list + current_cluster.second); // Centroid + + // Linear algebra = get eigenvalues and eigenvectors for + // PCA-like analysis + FT eigen_vectors[9]; + FT eigen_values[3]; + CGAL::internal::eigen_symmetric + (covariance, 3, eigen_vectors, eigen_values); + + + // Variation of the set = lambda_2 / (lambda_0 + lambda_1 + lambda_2) + FT var = (FT)(0.0); + for (int i = 0; i < 3; ++ i) + var += eigen_values[i]; + var = eigen_values[2] / var; + + // Split the set if size OR variance of the cluster is too big + if (current_cluster.first.size () > size || var > var_max) + { + std::list positive_side; + std::list negative_side; + + // Compute the plane which separates the set into 2: + // * Normal to the eigenvector with highest eigenvalue + // * Passes through the centroid of the set + Plane p (current_cluster.second, Vector (eigen_vectors[0], + eigen_vectors[1], + eigen_vectors[2])); + + // Split the point sets along this plane + typedef typename std::list::iterator Iterator; + point_set_split_plane_3 >, + std::back_insert_iterator >, + K> + (current_cluster.first.begin (), + current_cluster.first.end (), + p, + std::back_inserter (positive_side), + std::back_inserter (negative_side), true); + + // Compute the centroid (NOTE: this shall be improved -> the + // second centroid can be efficiently computed from the + // first and the previous ones). + Point centroid_positive = centroid (positive_side.begin (), + positive_side.end ()); + Point centroid_negative = centroid (negative_side.begin (), + negative_side.end ()); + + // If the sets are non-empty, add the clusters to the queue + if (positive_side.size () != 0) + clusters_queue.push (cluster (positive_side, centroid_positive)); + if (negative_side.size () != 0) + clusters_queue.push (cluster (negative_side, centroid_negative)); + } + // If the size/variance are small enough, add the centroid as + // and output point + else + points_to_keep.push_back (current_cluster.second); + + } + + + // The output of the function is to the first point to be removed + InputIterator first_point_to_remove = + std::copy (points_to_keep.begin (), points_to_keep.end (), first); + + return first_point_to_remove; +} + +#endif // HIERARCHICAL_CLUSTERING_H From 00fad8756a725a403f51c129e49402f139ee78c3 Mon Sep 17 00:00:00 2001 From: Pierre Alliez Date: Sat, 3 Mar 2012 20:53:01 +0000 Subject: [PATCH 02/35] Add comments in hierarchical clustering and fix indentations --- .../include/CGAL/hierarchical_clustering.h | 411 +++++++++--------- 1 file changed, 209 insertions(+), 202 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index f869cfa099e..989f341f4f4 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -15,7 +15,7 @@ // $URL: // $Id: // -// Author(s) : Simon Giraudot +// Author(s) : Simon Giraudot, Pierre Alliez #ifndef HIERARCHICAL_CLUSTERING_H @@ -34,221 +34,228 @@ #undef min #undef max - -/*****************************************************************************/ -template -typename K::FT* covariance_matrix_3 (InputIterator first, - InputIterator beyond, - typename K::Point_3 centroid) -/*****************************************************************************/ -{ - typedef typename K::FT FT; - FT *out = new FT[6]; - - for (int i = 0; i < 6; ++ i) - out[i] = (FT)(0.0); - - for (InputIterator it = first; it != beyond; ++ it) - { - const Point& p = *it; - Vector d = p - centroid; - out[0] += d.x () * d.x (); - out[1] += d.x () * d.y (); - out[2] += d.y () * d.y (); - out[3] += d.x () * d.z (); - out[4] += d.y () * d.z (); - out[5] += d.z () * d.z (); - } - - return out; -} +namespace CGAL { + // TODO: move to PCA -/*****************************************************************************/ -template -typename K::FT* covariance_matrix_3 (InputIterator first, - InputIterator beyond) -/*****************************************************************************/ -{ - return covariance_matrix_3 - (first, beyond, centroid (first, beyond)); -} - - - - -/*****************************************************************************/ -template -void point_set_split_plane_3 (InputIterator first, - InputIterator beyond, - typename K::Plane_3& plane, - OutputIterator1 points_on_positive_side, - OutputIterator2 points_on_negative_side, - OutputIterator3 points_on_plane) -/*****************************************************************************/ -{ - for (InputIterator it = first; it != beyond; ++ it) - if (plane.has_on_positive_side (*it)) - *points_on_positive_side++ = *it; - else if (plane.has_on_negative_side (*it)) - *points_on_negative_side++ = *it; - else - *points_on_plane++ = *it; -} - -/*****************************************************************************/ -template -void point_set_split_plane_3 (InputIterator first, - InputIterator beyond, - typename K::Plane_3& plane, - OutputIterator1 points_on_positive_side, - OutputIterator2 points_on_negative_side, - bool strictly_negative = true) -/*****************************************************************************/ -{ - for (InputIterator it = first; it != beyond; ++ it) - if (plane.has_on_positive_side (*it)) - *points_on_positive_side++ = *it; - else if (plane.has_on_negative_side (*it)) - *points_on_negative_side++ = *it; - else - { - if (strictly_negative) - *points_on_positive_side++ = *it; - else - *points_on_negative_side++ = *it; - } -} - - - -/*****************************************************************************/ -template -InputIterator hierarchical_clustering_3 (InputIterator first, - InputIterator beyond, - const unsigned int& size, - const typename K::FT& var_max) -/*****************************************************************************/ -{ - typedef typename K::FT FT; - typedef typename K::Plane_3 Plane; - typedef typename K::Point_3 Point; - typedef typename K::Vector_3 Vector; - - // We define a cluster as a set of points + its centroid (useful for - // faster computations of centroids - to be implemented) - typedef std::pair< std::list, Point > cluster; - - CGAL_precondition (first != beyond); - CGAL_point_set_processing_precondition (var_max >= 0.0 && var_max <= 1./3.); - - // The algorithm must return a set of points - std::list points_to_keep; - - // The first cluster is the whole set of input points - std::list first_cluster; - for (InputIterator it = first; it != beyond; ++ it) - first_cluster.push_back (*it); - - // Initialize the queue - std::queue clusters_queue; - clusters_queue.push (cluster (first_cluster, centroid (first, beyond))); - - while (!(clusters_queue.empty ())) - { - cluster current_cluster = clusters_queue.front (); - clusters_queue.pop (); - - // If the cluster only has 1 element, we add it to the list of - // output points - if (current_cluster.first.size () == 1) + /*****************************************************************************/ + template + typename K::FT* covariance_matrix_3 (InputIterator first, + InputIterator beyond, + typename K::Point_3 centroid) + /*****************************************************************************/ { - points_to_keep.push_back (current_cluster.second); - continue; + typedef typename K::FT FT; + FT *out = new FT[6]; + + for (int i = 0; i < 6; ++ i) + out[i] = (FT)(0.0); + + for (InputIterator it = first; it != beyond; ++ it) + { + const Point& p = *it; + Vector d = p - centroid; + out[0] += d.x () * d.x (); + out[1] += d.x () * d.y (); + out[2] += d.y () * d.y (); + out[3] += d.x () * d.z (); + out[4] += d.y () * d.z (); + out[5] += d.z () * d.z (); + } + + return out; } - // Compute the covariance matrix of the set - FT* covariance - = covariance_matrix_3::iterator, K> - (current_cluster.first.begin (), // Beginning of the list - current_cluster.first.end(), // End of the list - current_cluster.second); // Centroid - - // Linear algebra = get eigenvalues and eigenvectors for - // PCA-like analysis - FT eigen_vectors[9]; - FT eigen_values[3]; - CGAL::internal::eigen_symmetric - (covariance, 3, eigen_vectors, eigen_values); - // Variation of the set = lambda_2 / (lambda_0 + lambda_1 + lambda_2) - FT var = (FT)(0.0); - for (int i = 0; i < 3; ++ i) - var += eigen_values[i]; - var = eigen_values[2] / var; - // Split the set if size OR variance of the cluster is too big - if (current_cluster.first.size () > size || var > var_max) + /*****************************************************************************/ + template + typename K::FT* covariance_matrix_3 (InputIterator first, + InputIterator beyond) + /*****************************************************************************/ { - std::list positive_side; - std::list negative_side; - - // Compute the plane which separates the set into 2: - // * Normal to the eigenvector with highest eigenvalue - // * Passes through the centroid of the set - Plane p (current_cluster.second, Vector (eigen_vectors[0], - eigen_vectors[1], - eigen_vectors[2])); - - // Split the point sets along this plane - typedef typename std::list::iterator Iterator; - point_set_split_plane_3 >, - std::back_insert_iterator >, - K> - (current_cluster.first.begin (), - current_cluster.first.end (), - p, - std::back_inserter (positive_side), - std::back_inserter (negative_side), true); - - // Compute the centroid (NOTE: this shall be improved -> the - // second centroid can be efficiently computed from the - // first and the previous ones). - Point centroid_positive = centroid (positive_side.begin (), - positive_side.end ()); - Point centroid_negative = centroid (negative_side.begin (), - negative_side.end ()); - - // If the sets are non-empty, add the clusters to the queue - if (positive_side.size () != 0) - clusters_queue.push (cluster (positive_side, centroid_positive)); - if (negative_side.size () != 0) - clusters_queue.push (cluster (negative_side, centroid_negative)); + return covariance_matrix_3 + (first, beyond, centroid (first, beyond)); } - // If the size/variance are small enough, add the centroid as - // and output point - else - points_to_keep.push_back (current_cluster.second); - - } - // The output of the function is to the first point to be removed - InputIterator first_point_to_remove = - std::copy (points_to_keep.begin (), points_to_keep.end (), first); - return first_point_to_remove; -} + + /*****************************************************************************/ + template + void point_set_split_plane_3 (InputIterator first, + InputIterator beyond, + typename K::Plane_3& plane, + OutputIterator1 points_on_positive_side, + OutputIterator2 points_on_negative_side, + OutputIterator3 points_on_plane) + /*****************************************************************************/ + { + for (InputIterator it = first; it != beyond; ++ it) + if (plane.has_on_positive_side (*it)) + *points_on_positive_side++ = *it; + else if (plane.has_on_negative_side (*it)) + *points_on_negative_side++ = *it; + else + *points_on_plane++ = *it; + } + + + // TODO: move to separate file with exposed API? + // we may need this kind of functions for future interactive editing of point sets. + + /*****************************************************************************/ + template + void point_set_split_plane_3 (InputIterator first, + InputIterator beyond, + typename K::Plane_3& plane, + OutputIterator1 points_on_positive_side, + OutputIterator2 points_on_negative_side, + bool strictly_negative = true) // design to discuss + /*****************************************************************************/ + { + for (InputIterator it = first; it != beyond; ++ it) + if (plane.has_on_positive_side (*it)) + *points_on_positive_side++ = *it; + else if (plane.has_on_negative_side (*it)) + *points_on_negative_side++ = *it; + else + { + if (strictly_negative) + *points_on_positive_side++ = *it; + else + *points_on_negative_side++ = *it; + } + } + + + + /*****************************************************************************/ + template + InputIterator hierarchical_clustering (InputIterator first, + InputIterator beyond, + const unsigned int& size, + const typename K::FT& var_max) + /*****************************************************************************/ + { + typedef typename K::FT FT; + typedef typename K::Plane_3 Plane; + typedef typename K::Point_3 Point; + typedef typename K::Vector_3 Vector; + + // We define a cluster as a point set + its centroid (useful for + // faster computations of centroids - to be implemented) + typedef std::pair< std::list, Point > cluster; + + CGAL_precondition (first != beyond); + CGAL_point_set_processing_precondition (var_max >= (FT)0.0 && var_max <= (FT)(1./3.)); + + // The first cluster is the whole input point set + std::list first_cluster; + + // FIXME: use std::copy instead + for (InputIterator it = first; it != beyond; ++ it) + first_cluster.push_back (*it); + + // Initialize a queue of clusters + std::queue clusters_queue; + clusters_queue.push (cluster (first_cluster, centroid (first, beyond))); + + // The algorithm must return a set of points + std::list points_to_keep; + + while ( !clusters_queue.empty () ) + { + cluster current_cluster = clusters_queue.front (); + clusters_queue.pop (); + + // If the cluster only has 1 element, we add it to the list of + // output points + if (current_cluster.first.size () == 1) + { + points_to_keep.push_back (current_cluster.second); + continue; + } + + // Compute the covariance matrix of the set + FT* covariance + = covariance_matrix_3::iterator, K> + (current_cluster.first.begin (), // Beginning of the list + current_cluster.first.end(), // End of the list + current_cluster.second); // Centroid + + // Linear algebra = get eigenvalues and eigenvectors for + // PCA-like analysis + FT eigen_vectors[9]; + FT eigen_values[3]; + CGAL::internal::eigen_symmetric + (covariance, 3, eigen_vectors, eigen_values); + + // Variation of the set defined as lambda_2 / (lambda_0 + lambda_1 + lambda_2) + FT var = (FT)(0.0); + for (int i = 0; i < 3; ++ i) + var += eigen_values[i]; + var = eigen_values[2] / var; + + // Split the set if size OR variance of the cluster is too large + if (current_cluster.first.size () > size || var > var_max) + { + std::list positive_side; + std::list negative_side; + + // Compute the plane which splits the point set into 2 point sets: + // * Normal to the eigenvector with highest eigenvalue + // * Passes through the centroid of the set + Vector normal (eigen_vectors[0], eigen_vectors[1], eigen_vectors[2]); + Plane plane (current_cluster.second, normal); + + // Split the point sets along this plane + typedef typename std::list::iterator Iterator; + point_set_split_plane_3 >, + std::back_insert_iterator >, + K> + (current_cluster.first.begin (), + current_cluster.first.end (), + plane, + std::back_inserter (positive_side), + std::back_inserter (negative_side), true); + + // Compute the centroid (NOTE: this shall be improved -> the + // second centroid can be efficiently computed from the + // first and the previous ones). + Point centroid_positive = centroid (positive_side.begin (), positive_side.end ()); + Point centroid_negative = centroid (negative_side.begin (), negative_side.end ()); + + // If the sets are non-empty, add the clusters to the queue + if (positive_side.size () != 0) + clusters_queue.push (cluster (positive_side, centroid_positive)); + if (negative_side.size () != 0) + clusters_queue.push (cluster (negative_side, centroid_negative)); + } + // If the size/variance are small enough, add the centroid as + // and output point + else + points_to_keep.push_back (current_cluster.second); + } + + // The output of the function is to the first point to be removed + InputIterator first_point_to_remove = + std::copy (points_to_keep.begin (), points_to_keep.end (), first); + + return first_point_to_remove; + } + +} // namespace CGAL #endif // HIERARCHICAL_CLUSTERING_H From ee1e7ee7148827a29eafb00c51c1967131c48220 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Mon, 5 Mar 2012 08:50:15 +0000 Subject: [PATCH 03/35] Added the alternative implementation of hierarchical clustering using the Eigen library. For now, both implementations are available (switchable with a --- .../include/CGAL/hierarchical_clustering.h | 468 ++++++++++-------- 1 file changed, 266 insertions(+), 202 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index 989f341f4f4..c2c46247562 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -31,6 +31,16 @@ #include #include + +// For testing purposes, both CGAL linear algebra and Eigen library +// are used. You can switch from one to the other by commenting the +// following "define" line. +#define USE_EIGEN_LIB + +#ifdef USE_EIGEN_LIB +#include +#endif + #undef min #undef max @@ -38,223 +48,277 @@ namespace CGAL { - // TODO: move to PCA + // TODO: * move to PCA + // * adapt to the Eigen library? - /*****************************************************************************/ - template - typename K::FT* covariance_matrix_3 (InputIterator first, - InputIterator beyond, - typename K::Point_3 centroid) - /*****************************************************************************/ + /***************************************************************************/ + template + typename K::FT* covariance_matrix_3 (InputIterator first, + InputIterator beyond, + typename K::Point_3 centroid) + /***************************************************************************/ + { + typedef typename K::FT FT; + FT *out = new FT[6]; + + for (int i = 0; i < 6; ++ i) + out[i] = (FT)(0.0); + + for (InputIterator it = first; it != beyond; ++ it) + { + const Point& p = *it; + Vector d = p - centroid; + out[0] += d.x () * d.x (); + out[1] += d.x () * d.y (); + out[2] += d.y () * d.y (); + out[3] += d.x () * d.z (); + out[4] += d.y () * d.z (); + out[5] += d.z () * d.z (); + } + + return out; + } + + + + + /***************************************************************************/ + template + typename K::FT* covariance_matrix_3 (InputIterator first, + InputIterator beyond) + /***************************************************************************/ + { + return covariance_matrix_3 + (first, beyond, centroid (first, beyond)); + } + + + + + /***************************************************************************/ + template + void point_set_split_plane_3 (InputIterator first, + InputIterator beyond, + typename K::Plane_3& plane, + OutputIterator1 points_on_positive_side, + OutputIterator2 points_on_negative_side, + OutputIterator3 points_on_plane) + /***************************************************************************/ + { + for (InputIterator it = first; it != beyond; ++ it) + if (plane.has_on_positive_side (*it)) + *points_on_positive_side++ = *it; + else if (plane.has_on_negative_side (*it)) + *points_on_negative_side++ = *it; + else + *points_on_plane++ = *it; + } + + + // TODO: move to separate file with exposed API? + // we may need this kind of functions for future interactive editing of point sets. + + /***************************************************************************/ + template + void point_set_split_plane_3 (InputIterator first, + InputIterator beyond, + typename K::Plane_3& plane, + OutputIterator1 points_on_positive_side, + OutputIterator2 points_on_negative_side, + bool strictly_negative = true) // design to discuss + /***************************************************************************/ + { + for (InputIterator it = first; it != beyond; ++ it) + if (plane.has_on_positive_side (*it)) + *points_on_positive_side++ = *it; + else if (plane.has_on_negative_side (*it)) + *points_on_negative_side++ = *it; + else { - typedef typename K::FT FT; - FT *out = new FT[6]; - - for (int i = 0; i < 6; ++ i) - out[i] = (FT)(0.0); - - for (InputIterator it = first; it != beyond; ++ it) - { - const Point& p = *it; - Vector d = p - centroid; - out[0] += d.x () * d.x (); - out[1] += d.x () * d.y (); - out[2] += d.y () * d.y (); - out[3] += d.x () * d.z (); - out[4] += d.y () * d.z (); - out[5] += d.z () * d.z (); - } - - return out; + if (strictly_negative) + *points_on_positive_side++ = *it; + else + *points_on_negative_side++ = *it; } + } + /***************************************************************************/ + template + InputIterator hierarchical_clustering (InputIterator first, + InputIterator beyond, + const unsigned int& size, + const typename K::FT& var_max) + /***************************************************************************/ + { + typedef typename K::FT FT; + typedef typename K::Plane_3 Plane; + typedef typename K::Point_3 Point; + typedef typename K::Vector_3 Vector; - /*****************************************************************************/ - template - typename K::FT* covariance_matrix_3 (InputIterator first, - InputIterator beyond) - /*****************************************************************************/ - { - return covariance_matrix_3 - (first, beyond, centroid (first, beyond)); - } + // We define a cluster as a point set + its centroid (useful for + // faster computations of centroids - to be implemented) + typedef std::pair< std::list, Point > cluster; + + CGAL_precondition (first != beyond); + CGAL_point_set_processing_precondition + (var_max >= (FT)0.0 && var_max <= (FT)(1./3.)); + + // The first cluster is the whole input point set + std::list first_cluster; + + // FIXME: use std::copy instead + for (InputIterator it = first; it != beyond; ++ it) + first_cluster.push_back (*it); + + // Initialize a queue of clusters + std::queue clusters_queue; + clusters_queue.push (cluster (first_cluster, centroid (first, beyond))); + + // The algorithm must return a set of points + std::list points_to_keep; + + while ( !clusters_queue.empty () ) + { + cluster current_cluster = clusters_queue.front (); + clusters_queue.pop (); + + // If the cluster only has 1 element, we add it to the list of + // output points + if (current_cluster.first.size () == 1) + { + points_to_keep.push_back (current_cluster.second); + continue; + } + +#ifndef USE_EIGEN_LIB + + // Compute the covariance matrix of the set + FT* covariance + = covariance_matrix_3::iterator, K> + (current_cluster.first.begin (), // Beginning of the list + current_cluster.first.end(), // End of the list + current_cluster.second); // Centroid + + // Linear algebra = get eigenvalues and eigenvectors for + // PCA-like analysis + FT eigen_vectors[9]; + FT eigen_values[3]; + CGAL::internal::eigen_symmetric + (covariance, 3, eigen_vectors, eigen_values); + + // Variation of the set defined as lambda_2 / (lambda_0 + lambda_1 + lambda_2) + FT var = (FT)(0.0); + for (int i = 0; i < 3; ++ i) + var += eigen_values[i]; + var = eigen_values[2] / var; + + // Compute the normal to the eigenvector with highest eigenvalue + Vector normal (eigen_vectors[0], eigen_vectors[1], eigen_vectors[2]); + +#else + + // Compute the covariance matrix of the set + Eigen::Matrix3d covariance; + for (int i = 0; i < 3; ++ i) + for (int j = 0; j < 3; j ++) + covariance (i, j) = 0.; + + for (typename std::list::iterator it = current_cluster.first.begin (); + it != current_cluster.first.end (); ++ it) + { + const Point& p = *it; + Vector d = p - current_cluster.second; + covariance (0, 0) += d.x () * d.x (); + covariance (1, 0) += d.x () * d.y (); + covariance (1, 1) += d.y () * d.y (); + covariance (2, 0) += d.x () * d.z (); + covariance (2, 1) += d.y () * d.z (); + covariance (2, 2) += d.z () * d.z (); + } + + covariance (0, 1) = covariance (1, 0); + covariance (0, 2) = covariance (2, 0); + covariance (1, 2) = covariance (2, 1); + + // Linear algebra = get eigenvalues and eigenvectors for + // PCA-like analysis + Eigen::SelfAdjointEigenSolver + eigensolver (covariance); + if (eigensolver.info () != Eigen::Success) + std::abort (); + Eigen::Vector3d eigen_values = eigensolver.eigenvalues (); + Eigen::Vector3d eigen_vector = eigensolver.eigenvectors ().col (2); + + // Variation of the set defined as lambda_2 / (lambda_0 + lambda_1 + lambda_2) + FT var = (FT)(0.0); + for (int i = 0; i < 3; ++ i) + var += eigen_values[i]; + var = eigen_values (0) / var; + + // Compute the normal to the eigenvector with highest eigenvalue + Vector normal (eigen_vector(0), eigen_vector(1), eigen_vector(2)); + +#endif + // Split the set if size OR variance of the cluster is too large + if (current_cluster.first.size () > size || var > var_max) + { + std::list positive_side; + std::list negative_side; + // Compute the plane which splits the point set into 2 point sets: + // * Normal to the eigenvector with highest eigenvalue + // * Passes through the centroid of the set + Plane plane (current_cluster.second, normal); - /*****************************************************************************/ - template - void point_set_split_plane_3 (InputIterator first, - InputIterator beyond, - typename K::Plane_3& plane, - OutputIterator1 points_on_positive_side, - OutputIterator2 points_on_negative_side, - OutputIterator3 points_on_plane) - /*****************************************************************************/ - { - for (InputIterator it = first; it != beyond; ++ it) - if (plane.has_on_positive_side (*it)) - *points_on_positive_side++ = *it; - else if (plane.has_on_negative_side (*it)) - *points_on_negative_side++ = *it; - else - *points_on_plane++ = *it; - } + // Split the point sets along this plane + typedef typename std::list::iterator Iterator; + point_set_split_plane_3 >, + std::back_insert_iterator >, + K> + (current_cluster.first.begin (), + current_cluster.first.end (), + plane, + std::back_inserter (positive_side), + std::back_inserter (negative_side), true); + // Compute the centroid (NOTE: this shall be improved -> the + // second centroid can be efficiently computed from the + // first and the previous ones). + Point centroid_positive + = centroid (positive_side.begin (), positive_side.end ()); + Point centroid_negative + = centroid (negative_side.begin (), negative_side.end ()); - // TODO: move to separate file with exposed API? - // we may need this kind of functions for future interactive editing of point sets. + // If the sets are non-empty, add the clusters to the queue + if (positive_side.size () != 0) + clusters_queue.push (cluster (positive_side, centroid_positive)); + if (negative_side.size () != 0) + clusters_queue.push (cluster (negative_side, centroid_negative)); + } + // If the size/variance are small enough, add the centroid as + // and output point + else + points_to_keep.push_back (current_cluster.second); + } - /*****************************************************************************/ - template - void point_set_split_plane_3 (InputIterator first, - InputIterator beyond, - typename K::Plane_3& plane, - OutputIterator1 points_on_positive_side, - OutputIterator2 points_on_negative_side, - bool strictly_negative = true) // design to discuss - /*****************************************************************************/ - { - for (InputIterator it = first; it != beyond; ++ it) - if (plane.has_on_positive_side (*it)) - *points_on_positive_side++ = *it; - else if (plane.has_on_negative_side (*it)) - *points_on_negative_side++ = *it; - else - { - if (strictly_negative) - *points_on_positive_side++ = *it; - else - *points_on_negative_side++ = *it; - } - } + // The output of the function is to the first point to be removed + InputIterator first_point_to_remove = + std::copy (points_to_keep.begin (), points_to_keep.end (), first); - - - /*****************************************************************************/ - template - InputIterator hierarchical_clustering (InputIterator first, - InputIterator beyond, - const unsigned int& size, - const typename K::FT& var_max) - /*****************************************************************************/ - { - typedef typename K::FT FT; - typedef typename K::Plane_3 Plane; - typedef typename K::Point_3 Point; - typedef typename K::Vector_3 Vector; - - // We define a cluster as a point set + its centroid (useful for - // faster computations of centroids - to be implemented) - typedef std::pair< std::list, Point > cluster; - - CGAL_precondition (first != beyond); - CGAL_point_set_processing_precondition (var_max >= (FT)0.0 && var_max <= (FT)(1./3.)); - - // The first cluster is the whole input point set - std::list first_cluster; - - // FIXME: use std::copy instead - for (InputIterator it = first; it != beyond; ++ it) - first_cluster.push_back (*it); - - // Initialize a queue of clusters - std::queue clusters_queue; - clusters_queue.push (cluster (first_cluster, centroid (first, beyond))); - - // The algorithm must return a set of points - std::list points_to_keep; - - while ( !clusters_queue.empty () ) - { - cluster current_cluster = clusters_queue.front (); - clusters_queue.pop (); - - // If the cluster only has 1 element, we add it to the list of - // output points - if (current_cluster.first.size () == 1) - { - points_to_keep.push_back (current_cluster.second); - continue; - } - - // Compute the covariance matrix of the set - FT* covariance - = covariance_matrix_3::iterator, K> - (current_cluster.first.begin (), // Beginning of the list - current_cluster.first.end(), // End of the list - current_cluster.second); // Centroid - - // Linear algebra = get eigenvalues and eigenvectors for - // PCA-like analysis - FT eigen_vectors[9]; - FT eigen_values[3]; - CGAL::internal::eigen_symmetric - (covariance, 3, eigen_vectors, eigen_values); - - // Variation of the set defined as lambda_2 / (lambda_0 + lambda_1 + lambda_2) - FT var = (FT)(0.0); - for (int i = 0; i < 3; ++ i) - var += eigen_values[i]; - var = eigen_values[2] / var; - - // Split the set if size OR variance of the cluster is too large - if (current_cluster.first.size () > size || var > var_max) - { - std::list positive_side; - std::list negative_side; - - // Compute the plane which splits the point set into 2 point sets: - // * Normal to the eigenvector with highest eigenvalue - // * Passes through the centroid of the set - Vector normal (eigen_vectors[0], eigen_vectors[1], eigen_vectors[2]); - Plane plane (current_cluster.second, normal); - - // Split the point sets along this plane - typedef typename std::list::iterator Iterator; - point_set_split_plane_3 >, - std::back_insert_iterator >, - K> - (current_cluster.first.begin (), - current_cluster.first.end (), - plane, - std::back_inserter (positive_side), - std::back_inserter (negative_side), true); - - // Compute the centroid (NOTE: this shall be improved -> the - // second centroid can be efficiently computed from the - // first and the previous ones). - Point centroid_positive = centroid (positive_side.begin (), positive_side.end ()); - Point centroid_negative = centroid (negative_side.begin (), negative_side.end ()); - - // If the sets are non-empty, add the clusters to the queue - if (positive_side.size () != 0) - clusters_queue.push (cluster (positive_side, centroid_positive)); - if (negative_side.size () != 0) - clusters_queue.push (cluster (negative_side, centroid_negative)); - } - // If the size/variance are small enough, add the centroid as - // and output point - else - points_to_keep.push_back (current_cluster.second); - } - - // The output of the function is to the first point to be removed - InputIterator first_point_to_remove = - std::copy (points_to_keep.begin (), points_to_keep.end (), first); - - return first_point_to_remove; - } + return first_point_to_remove; + } } // namespace CGAL From e8dd34cb21c9372dae20ccf2841999d62d7a76a4 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Thu, 8 Mar 2012 13:29:32 +0000 Subject: [PATCH 04/35] Implemented the efficient computation of the second centroid. The hierarchical clustering algorithm gets about 15% faster (on test Eglise Fontaine, from 91s to 76s). --- .../include/CGAL/hierarchical_clustering.h | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index c2c46247562..4a4eb32a852 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -293,13 +293,24 @@ namespace CGAL { std::back_inserter (positive_side), std::back_inserter (negative_side), true); - // Compute the centroid (NOTE: this shall be improved -> the - // second centroid can be efficiently computed from the - // first and the previous ones). + // Compute the centroids Point centroid_positive = centroid (positive_side.begin (), positive_side.end ()); + + // The second centroid can be computed with the first and + // the previous ones : + // centroid_neg = (n_total * centroid - n_pos * centroid_pos) + // / n_neg; Point centroid_negative - = centroid (negative_side.begin (), negative_side.end ()); + ((current_cluster.first.size () * current_cluster.second.x () + - positive_side.size () * centroid_positive.x ()) + / negative_side.size (), + (current_cluster.first.size () * current_cluster.second.y () + - positive_side.size () * centroid_positive.y ()) + / negative_side.size (), + (current_cluster.first.size () * current_cluster.second.z () + - positive_side.size () * centroid_positive.z ()) + / negative_side.size ()); // If the sets are non-empty, add the clusters to the queue if (positive_side.size () != 0) From eb7266a42dc7f1c54bcabaca7a8c33a17d3e245a Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Fri, 11 Sep 2015 10:28:31 +0200 Subject: [PATCH 05/35] Cleaning/reorganizing code, use diagonalize_traits and add variants with default parameters and template deduction --- .../include/CGAL/hierarchical_clustering.h | 268 ++++++------------ 1 file changed, 91 insertions(+), 177 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index 4a4eb32a852..fca7d2cf258 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -24,105 +24,18 @@ #include #include +#include #include #include #include #include -#include #include +#include -// For testing purposes, both CGAL linear algebra and Eigen library -// are used. You can switch from one to the other by commenting the -// following "define" line. -#define USE_EIGEN_LIB - -#ifdef USE_EIGEN_LIB -#include -#endif - -#undef min -#undef max - namespace CGAL { - - // TODO: * move to PCA - // * adapt to the Eigen library? - - /***************************************************************************/ - template - typename K::FT* covariance_matrix_3 (InputIterator first, - InputIterator beyond, - typename K::Point_3 centroid) - /***************************************************************************/ - { - typedef typename K::FT FT; - FT *out = new FT[6]; - - for (int i = 0; i < 6; ++ i) - out[i] = (FT)(0.0); - - for (InputIterator it = first; it != beyond; ++ it) - { - const Point& p = *it; - Vector d = p - centroid; - out[0] += d.x () * d.x (); - out[1] += d.x () * d.y (); - out[2] += d.y () * d.y (); - out[3] += d.x () * d.z (); - out[4] += d.y () * d.z (); - out[5] += d.z () * d.z (); - } - - return out; - } - - - - - /***************************************************************************/ - template - typename K::FT* covariance_matrix_3 (InputIterator first, - InputIterator beyond) - /***************************************************************************/ - { - return covariance_matrix_3 - (first, beyond, centroid (first, beyond)); - } - - - - - /***************************************************************************/ - template - void point_set_split_plane_3 (InputIterator first, - InputIterator beyond, - typename K::Plane_3& plane, - OutputIterator1 points_on_positive_side, - OutputIterator2 points_on_negative_side, - OutputIterator3 points_on_plane) - /***************************************************************************/ - { - for (InputIterator it = first; it != beyond; ++ it) - if (plane.has_on_positive_side (*it)) - *points_on_positive_side++ = *it; - else if (plane.has_on_negative_side (*it)) - *points_on_negative_side++ = *it; - else - *points_on_plane++ = *it; - } - - - // TODO: move to separate file with exposed API? - // we may need this kind of functions for future interactive editing of point sets. - - /***************************************************************************/ template - InputIterator hierarchical_clustering (InputIterator first, - InputIterator beyond, - const unsigned int& size, - const typename K::FT& var_max) - /***************************************************************************/ + template + void hierarchical_clustering (InputIterator begin, + InputIterator end, + PointPMap point_pmap, + OutputIterator out, + const unsigned int size, + const double var_max, + const DiagonalizeTraits&, + const Kernel&) { - typedef typename K::FT FT; - typedef typename K::Plane_3 Plane; - typedef typename K::Point_3 Point; - typedef typename K::Vector_3 Vector; + typedef typename Kernel::Plane_3 Plane; + typedef typename Kernel::Point_3 Point; + typedef typename Kernel::Vector_3 Vector; // We define a cluster as a point set + its centroid (useful for // faster computations of centroids - to be implemented) typedef std::pair< std::list, Point > cluster; - CGAL_precondition (first != beyond); + CGAL_precondition (begin != end); CGAL_point_set_processing_precondition (var_max >= (FT)0.0 && var_max <= (FT)(1./3.)); // The first cluster is the whole input point set std::list first_cluster; - - // FIXME: use std::copy instead - for (InputIterator it = first; it != beyond; ++ it) - first_cluster.push_back (*it); + for(InputIterator it = begin; it != end; it++) + { +#ifdef CGAL_USE_PROPERTY_MAPS_API_V1 + Point point = get(point_pmap, it); +#else + Point point = get(point_pmap, *it); +#endif + first_cluster.push_back (point); + } // Initialize a queue of clusters std::queue clusters_queue; - clusters_queue.push (cluster (first_cluster, centroid (first, beyond))); + clusters_queue.push (cluster (first_cluster, centroid (begin, end))); - // The algorithm must return a set of points - std::list points_to_keep; - - while ( !clusters_queue.empty () ) + while (!(clusters_queue.empty ())) { cluster current_cluster = clusters_queue.front (); clusters_queue.pop (); @@ -195,80 +114,43 @@ namespace CGAL { // output points if (current_cluster.first.size () == 1) { - points_to_keep.push_back (current_cluster.second); + *(out ++) = current_cluster.second; continue; } -#ifndef USE_EIGEN_LIB - // Compute the covariance matrix of the set - FT* covariance - = covariance_matrix_3::iterator, K> - (current_cluster.first.begin (), // Beginning of the list - current_cluster.first.end(), // End of the list - current_cluster.second); // Centroid - - // Linear algebra = get eigenvalues and eigenvectors for - // PCA-like analysis - FT eigen_vectors[9]; - FT eigen_values[3]; - CGAL::internal::eigen_symmetric - (covariance, 3, eigen_vectors, eigen_values); - - // Variation of the set defined as lambda_2 / (lambda_0 + lambda_1 + lambda_2) - FT var = (FT)(0.0); - for (int i = 0; i < 3; ++ i) - var += eigen_values[i]; - var = eigen_values[2] / var; - - // Compute the normal to the eigenvector with highest eigenvalue - Vector normal (eigen_vectors[0], eigen_vectors[1], eigen_vectors[2]); - -#else - - // Compute the covariance matrix of the set - Eigen::Matrix3d covariance; - for (int i = 0; i < 3; ++ i) - for (int j = 0; j < 3; j ++) - covariance (i, j) = 0.; + cpp11::array covariance = {{ 0., 0., 0., 0., 0., 0. }}; for (typename std::list::iterator it = current_cluster.first.begin (); it != current_cluster.first.end (); ++ it) { const Point& p = *it; Vector d = p - current_cluster.second; - covariance (0, 0) += d.x () * d.x (); - covariance (1, 0) += d.x () * d.y (); - covariance (1, 1) += d.y () * d.y (); - covariance (2, 0) += d.x () * d.z (); - covariance (2, 1) += d.y () * d.z (); - covariance (2, 2) += d.z () * d.z (); + covariance[0] += d.x () * d.x (); + covariance[1] += d.x () * d.y (); + covariance[2] += d.x () * d.z (); + covariance[3] += d.y () * d.y (); + covariance[4] += d.y () * d.z (); + covariance[5] += d.z () * d.z (); } - covariance (0, 1) = covariance (1, 0); - covariance (0, 2) = covariance (2, 0); - covariance (1, 2) = covariance (2, 1); - + cpp11::array eigenvalues = {{ 0., 0., 0. }}; + cpp11::array eigenvectors = {{ 0., 0., 0., + 0., 0., 0., + 0., 0., 0. }}; // Linear algebra = get eigenvalues and eigenvectors for // PCA-like analysis - Eigen::SelfAdjointEigenSolver - eigensolver (covariance); - if (eigensolver.info () != Eigen::Success) - std::abort (); - Eigen::Vector3d eigen_values = eigensolver.eigenvalues (); - Eigen::Vector3d eigen_vector = eigensolver.eigenvectors ().col (2); + DiagonalizeTraits::diagonalize_selfadjoint_covariance_matrix + (covariance, eigenvalues, eigenvectors); - // Variation of the set defined as lambda_2 / (lambda_0 + lambda_1 + lambda_2) - FT var = (FT)(0.0); + // Variation of the set defined as lambda_min / (lambda_0 + lambda_1 + lambda_2) + double var = 0.; for (int i = 0; i < 3; ++ i) - var += eigen_values[i]; - var = eigen_values (0) / var; - - // Compute the normal to the eigenvector with highest eigenvalue - Vector normal (eigen_vector(0), eigen_vector(1), eigen_vector(2)); - -#endif + var += eigenvalues[i]; + var = eigenvalues[0] / var; + // Eigenvector with smallest eigenvalue + Vector normal (eigenvectors[0], eigenvectors[1], eigenvectors[2]); // Split the set if size OR variance of the cluster is too large if (current_cluster.first.size () > size || var > var_max) @@ -284,9 +166,9 @@ namespace CGAL { // Split the point sets along this plane typedef typename std::list::iterator Iterator; point_set_split_plane_3 >, - std::back_insert_iterator >, - K> + std::back_insert_iterator >, + std::back_insert_iterator >, + Kernel> (current_cluster.first.begin (), current_cluster.first.end (), plane, @@ -321,14 +203,46 @@ namespace CGAL { // If the size/variance are small enough, add the centroid as // and output point else - points_to_keep.push_back (current_cluster.second); + *(out ++) = current_cluster.second; } - // The output of the function is to the first point to be removed - InputIterator first_point_to_remove = - std::copy (points_to_keep.begin (), points_to_keep.end (), first); + } - return first_point_to_remove; + + // This variant deduces the kernel from the iterator type. + template + void hierarchical_clustering (InputIterator begin, + InputIterator end, + PointPMap point_pmap, + OutputIterator out, + const unsigned int size = 10, + const double var_max = 0.333) + { + typedef typename boost::property_traits::value_type Point; + typedef typename Kernel_traits::Kernel Kernel; + hierarchical_clustering (begin, end, point_pmap, out, size, var_max, + Default_diagonalize_traits (), Kernel()); + } + + // This variant creates a default point property map = Identity_property_map. + template + void hierarchical_clustering (InputIterator begin, + InputIterator end, + OutputIterator out, + const unsigned int size = 10, + const double var_max = 0.333) + { + hierarchical_clustering + (begin, end, +#ifdef CGAL_USE_PROPERTY_MAPS_API_V1 + make_dereference_property_map(first), +#else + make_identity_property_map (typename std::iterator_traits::value_type()), +#endif + out, size, var_max); } } // namespace CGAL From 0841e423a2041b2936df59abee0484d50fc3d052 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Fri, 11 Sep 2015 10:29:26 +0200 Subject: [PATCH 06/35] Add example for hierarchical clustering --- .../Point_set_processing_3/CMakeLists.txt | 1 + .../hierarchical_clustering_example.cpp | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 Point_set_processing_3/examples/Point_set_processing_3/hierarchical_clustering_example.cpp diff --git a/Point_set_processing_3/examples/Point_set_processing_3/CMakeLists.txt b/Point_set_processing_3/examples/Point_set_processing_3/CMakeLists.txt index 770761fa4cb..307c15d4098 100644 --- a/Point_set_processing_3/examples/Point_set_processing_3/CMakeLists.txt +++ b/Point_set_processing_3/examples/Point_set_processing_3/CMakeLists.txt @@ -57,6 +57,7 @@ if ( CGAL_FOUND ) create_single_source_cgal_program( "bilateral_smooth_point_set_example.cpp" ) create_single_source_cgal_program( "grid_simplification_example.cpp" ) create_single_source_cgal_program( "grid_simplify_indices.cpp" ) + create_single_source_cgal_program( "hierarchical_clustering_example.cpp" ) create_single_source_cgal_program( "normals_example.cpp" ) create_single_source_cgal_program( "property_map.cpp" ) create_single_source_cgal_program( "random_simplification_example.cpp" ) diff --git a/Point_set_processing_3/examples/Point_set_processing_3/hierarchical_clustering_example.cpp b/Point_set_processing_3/examples/Point_set_processing_3/hierarchical_clustering_example.cpp new file mode 100644 index 00000000000..6a0b75a5dc0 --- /dev/null +++ b/Point_set_processing_3/examples/Point_set_processing_3/hierarchical_clustering_example.cpp @@ -0,0 +1,31 @@ +#include +#include +#include + +#include +#include + +// types +typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel; +typedef Kernel::Point_3 Point; + +int main(int argc, char*argv[]) +{ + // Reads a .xyz point set file in points[]. + std::vector points; + const char* fname = (argc>1)?argv[1]:"data/oni.xyz"; + std::ifstream stream(fname); + if (!stream || + !CGAL::read_xyz_points(stream, std::back_inserter(points))) + { + std::cerr << "Error: cannot read file " << fname << std::endl; + return EXIT_FAILURE; + } + + std::vector output; // Algorithm generate a new set of points + CGAL::hierarchical_clustering (points.begin (), points.end (), + std::back_inserter (output)); + + return EXIT_SUCCESS; +} + From 7babff9f0525200b5bdd204b39682a428e4d1f7d Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Fri, 11 Sep 2015 11:05:09 +0200 Subject: [PATCH 07/35] Optimizations (using splice for lists and reference for queue.front()) --- .../include/CGAL/hierarchical_clustering.h | 58 ++++++------------- 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index fca7d2cf258..a7d5221f530 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -36,33 +36,6 @@ namespace CGAL { - template - void point_set_split_plane_3 (InputIterator first, - InputIterator beyond, - typename K::Plane_3& plane, - OutputIterator1 points_on_positive_side, - OutputIterator2 points_on_negative_side, - bool strictly_negative = true) - { - for (InputIterator it = first; it != beyond; ++ it) - if (plane.has_on_positive_side (*it)) - *points_on_positive_side++ = *it; - else if (plane.has_on_negative_side (*it)) - *points_on_negative_side++ = *it; - else - { - if (strictly_negative) - *points_on_positive_side++ = *it; - else - *points_on_negative_side++ = *it; - } - } - - - template ::iterator Iterator; - point_set_split_plane_3 >, - std::back_insert_iterator >, - Kernel> - (current_cluster.first.begin (), - current_cluster.first.end (), - plane, - std::back_inserter (positive_side), - std::back_inserter (negative_side), true); + typename std::list::iterator it = current_cluster.first.begin (); + while (it != current_cluster.first.end ()) + { + typename std::list::iterator current = it ++; + + std::list& side = (plane.has_on_positive_side (*current) + ? positive_side : negative_side); + side.splice (side.end (), current_cluster.first, current); + } // Compute the centroids Point centroid_positive @@ -194,6 +165,8 @@ namespace CGAL { - positive_side.size () * centroid_positive.z ()) / negative_side.size ()); + clusters_queue.pop (); + // If the sets are non-empty, add the clusters to the queue if (positive_side.size () != 0) clusters_queue.push (cluster (positive_side, centroid_positive)); @@ -203,7 +176,10 @@ namespace CGAL { // If the size/variance are small enough, add the centroid as // and output point else - *(out ++) = current_cluster.second; + { + *(out ++) = current_cluster.second; + clusters_queue.pop (); + } } } From ed2d3167e2af6dea561d98b89532f78f50d79ae7 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Fri, 11 Sep 2015 11:11:40 +0200 Subject: [PATCH 08/35] Stack is faster than queue in this case --- .../include/CGAL/hierarchical_clustering.h | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index a7d5221f530..bb2bedffcf9 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -22,7 +22,7 @@ #define HIERARCHICAL_CLUSTERING_H #include -#include +#include #include #include @@ -74,20 +74,20 @@ namespace CGAL { first_cluster.push_back (point); } - // Initialize a queue of clusters - std::queue clusters_queue; - clusters_queue.push (cluster (first_cluster, centroid (begin, end))); + // Initialize a stack of clusters + std::stack clusters_stack; + clusters_stack.push (cluster (first_cluster, centroid (begin, end))); - while (!(clusters_queue.empty ())) + while (!(clusters_stack.empty ())) { - cluster& current_cluster = clusters_queue.front (); + cluster& current_cluster = clusters_stack.top (); // If the cluster only has 1 element, we add it to the list of // output points if (current_cluster.first.size () == 1) { *(out ++) = current_cluster.second; - clusters_queue.pop (); + clusters_stack.pop (); continue; } @@ -165,20 +165,20 @@ namespace CGAL { - positive_side.size () * centroid_positive.z ()) / negative_side.size ()); - clusters_queue.pop (); + clusters_stack.pop (); - // If the sets are non-empty, add the clusters to the queue + // If the sets are non-empty, add the clusters to the stack if (positive_side.size () != 0) - clusters_queue.push (cluster (positive_side, centroid_positive)); + clusters_stack.push (cluster (positive_side, centroid_positive)); if (negative_side.size () != 0) - clusters_queue.push (cluster (negative_side, centroid_negative)); + clusters_stack.push (cluster (negative_side, centroid_negative)); } // If the size/variance are small enough, add the centroid as // and output point else { *(out ++) = current_cluster.second; - clusters_queue.pop (); + clusters_stack.pop (); } } From 5ab793531ca1a18fc8e775c3c150788c80762d13 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Fri, 11 Sep 2015 11:31:00 +0200 Subject: [PATCH 09/35] Bugfix: if one of the two sides is empty, only treat the non-empty side --- .../include/CGAL/hierarchical_clustering.h | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index bb2bedffcf9..15e3ff8101b 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -32,7 +32,6 @@ #include #include - namespace CGAL { @@ -78,6 +77,7 @@ namespace CGAL { std::stack clusters_stack; clusters_stack.push (cluster (first_cluster, centroid (begin, end))); + while (!(clusters_stack.empty ())) { cluster& current_cluster = clusters_stack.top (); @@ -115,7 +115,7 @@ namespace CGAL { // PCA-like analysis DiagonalizeTraits::diagonalize_selfadjoint_covariance_matrix (covariance, eigenvalues, eigenvectors); - + // Variation of the set defined as lambda_min / (lambda_0 + lambda_1 + lambda_2) double var = 0.; for (int i = 0; i < 3; ++ i) @@ -146,32 +146,44 @@ namespace CGAL { side.splice (side.end (), current_cluster.first, current); } - // Compute the centroids - Point centroid_positive - = centroid (positive_side.begin (), positive_side.end ()); + if (positive_side.empty () || negative_side.empty ()) + { + std::list& side = (positive_side.empty () ? + negative_side : positive_side); + Point c = centroid (side.begin (), side.end ()); + + clusters_stack.pop (); + clusters_stack.push (cluster (side, c)); + } + else + { + // Compute the centroids + Point centroid_positive = centroid (positive_side.begin (), positive_side.end ()); - // The second centroid can be computed with the first and - // the previous ones : - // centroid_neg = (n_total * centroid - n_pos * centroid_pos) - // / n_neg; - Point centroid_negative - ((current_cluster.first.size () * current_cluster.second.x () - - positive_side.size () * centroid_positive.x ()) - / negative_side.size (), - (current_cluster.first.size () * current_cluster.second.y () - - positive_side.size () * centroid_positive.y ()) - / negative_side.size (), - (current_cluster.first.size () * current_cluster.second.z () - - positive_side.size () * centroid_positive.z ()) - / negative_side.size ()); + // The second centroid can be computed with the first and + // the previous ones : + // centroid_neg = (n_total * centroid - n_pos * centroid_pos) + // / n_neg; - clusters_stack.pop (); - // If the sets are non-empty, add the clusters to the stack - if (positive_side.size () != 0) - clusters_stack.push (cluster (positive_side, centroid_positive)); - if (negative_side.size () != 0) - clusters_stack.push (cluster (negative_side, centroid_negative)); + Point centroid_negative ((current_cluster.first.size () * current_cluster.second.x () + - positive_side.size () * centroid_positive.x ()) + / negative_side.size (), + (current_cluster.first.size () * current_cluster.second.y () + - positive_side.size () * centroid_positive.y ()) + / negative_side.size (), + (current_cluster.first.size () * current_cluster.second.z () + - positive_side.size () * centroid_positive.z ()) + / negative_side.size ()); + + clusters_stack.pop (); + + // If the sets are non-empty, add the clusters to the stack + if (positive_side.size () != 0) + clusters_stack.push (cluster (positive_side, centroid_positive)); + if (negative_side.size () != 0) + clusters_stack.push (cluster (negative_side, centroid_negative)); + } } // If the size/variance are small enough, add the centroid as // and output point From fea22733ee374518e1673f702c0c00f07f2330b0 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Fri, 11 Sep 2015 12:34:46 +0200 Subject: [PATCH 10/35] Speed up using list instead of queue (avoid multiple copies) + bugfix --- .../include/CGAL/hierarchical_clustering.h | 100 +++++++++--------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index 15e3ff8101b..0a544317e31 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -57,12 +57,15 @@ namespace CGAL { // faster computations of centroids - to be implemented) typedef std::pair< std::list, Point > cluster; + std::list clusters_stack; + typedef typename std::list::iterator cluster_iterator; + CGAL_precondition (begin != end); CGAL_point_set_processing_precondition - (var_max >= (FT)0.0 && var_max <= (FT)(1./3.)); + (var_max >= 0.0 && var_max <= 1./3.); - // The first cluster is the whole input point set - std::list first_cluster; + // The first cluster is the whole input point set + clusters_stack.push_front (cluster (std::list(), Point (0., 0., 0.))); for(InputIterator it = begin; it != end; it++) { #ifdef CGAL_USE_PROPERTY_MAPS_API_V1 @@ -70,24 +73,21 @@ namespace CGAL { #else Point point = get(point_pmap, *it); #endif - first_cluster.push_back (point); + clusters_stack.front ().first.push_back (point); } - - // Initialize a stack of clusters - std::stack clusters_stack; - clusters_stack.push (cluster (first_cluster, centroid (begin, end))); - + clusters_stack.front ().second = centroid (clusters_stack.front ().first.begin (), + clusters_stack.front ().first.end ()); while (!(clusters_stack.empty ())) { - cluster& current_cluster = clusters_stack.top (); + cluster& current_cluster = clusters_stack.back (); // If the cluster only has 1 element, we add it to the list of // output points if (current_cluster.first.size () == 1) { *(out ++) = current_cluster.second; - clusters_stack.pop (); + clusters_stack.pop_back (); continue; } @@ -122,67 +122,70 @@ namespace CGAL { var += eigenvalues[i]; var = eigenvalues[0] / var; - // Eigenvector with smallest eigenvalue - Vector normal (eigenvectors[0], eigenvectors[1], eigenvectors[2]); - // Split the set if size OR variance of the cluster is too large if (current_cluster.first.size () > size || var > var_max) { - std::list positive_side; - std::list negative_side; - + clusters_stack.push_front (cluster (std::list(), Point (0., 0., 0.))); + cluster_iterator positive_side = clusters_stack.begin (); + clusters_stack.push_front (cluster (std::list(), Point (0., 0., 0.))); + cluster_iterator negative_side = clusters_stack.begin (); + // Compute the plane which splits the point set into 2 point sets: // * Normal to the eigenvector with highest eigenvalue // * Passes through the centroid of the set - Plane plane (current_cluster.second, normal); + Plane plane (current_cluster.second, Vector (eigenvectors[6], eigenvectors[7], eigenvectors[8])); + std::size_t current_cluster_size = 0; typename std::list::iterator it = current_cluster.first.begin (); while (it != current_cluster.first.end ()) { typename std::list::iterator current = it ++; std::list& side = (plane.has_on_positive_side (*current) - ? positive_side : negative_side); + ? positive_side->first : negative_side->first); side.splice (side.end (), current_cluster.first, current); + ++ current_cluster_size; } - if (positive_side.empty () || negative_side.empty ()) + if (positive_side->first.empty () || negative_side->first.empty ()) { - std::list& side = (positive_side.empty () ? - negative_side : positive_side); - Point c = centroid (side.begin (), side.end ()); - - clusters_stack.pop (); - clusters_stack.push (cluster (side, c)); + cluster_iterator empty, nonempty; + if (positive_side->first.empty ()) + { + empty = positive_side; + nonempty = negative_side; + } + else + { + empty = negative_side; + nonempty = positive_side; + } + + nonempty->second = centroid (nonempty->first.begin (), nonempty->first.end ()); + + clusters_stack.erase (empty); + clusters_stack.pop_back (); } else { // Compute the centroids - Point centroid_positive = centroid (positive_side.begin (), positive_side.end ()); + positive_side->second = centroid (positive_side->first.begin (), positive_side->first.end ()); // The second centroid can be computed with the first and // the previous ones : // centroid_neg = (n_total * centroid - n_pos * centroid_pos) // / n_neg; + negative_side->second = Point ((current_cluster_size * current_cluster.second.x () + - positive_side->first.size () * positive_side->second.x ()) + / negative_side->first.size (), + (current_cluster_size * current_cluster.second.y () + - positive_side->first.size () * positive_side->second.y ()) + / negative_side->first.size (), + (current_cluster_size * current_cluster.second.z () + - positive_side->first.size () * positive_side->second.z ()) + / negative_side->first.size ()); - - Point centroid_negative ((current_cluster.first.size () * current_cluster.second.x () - - positive_side.size () * centroid_positive.x ()) - / negative_side.size (), - (current_cluster.first.size () * current_cluster.second.y () - - positive_side.size () * centroid_positive.y ()) - / negative_side.size (), - (current_cluster.first.size () * current_cluster.second.z () - - positive_side.size () * centroid_positive.z ()) - / negative_side.size ()); - - clusters_stack.pop (); - - // If the sets are non-empty, add the clusters to the stack - if (positive_side.size () != 0) - clusters_stack.push (cluster (positive_side, centroid_positive)); - if (negative_side.size () != 0) - clusters_stack.push (cluster (negative_side, centroid_negative)); + clusters_stack.pop_back (); } } // If the size/variance are small enough, add the centroid as @@ -190,10 +193,9 @@ namespace CGAL { else { *(out ++) = current_cluster.second; - clusters_stack.pop (); + clusters_stack.pop_back (); } } - } @@ -205,8 +207,8 @@ namespace CGAL { InputIterator end, PointPMap point_pmap, OutputIterator out, - const unsigned int size = 10, - const double var_max = 0.333) + const unsigned int size, + const double var_max) { typedef typename boost::property_traits::value_type Point; typedef typename Kernel_traits::Kernel Kernel; From 14567ae2618c52848bcfdfca51d733d7f71f6969 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Fri, 11 Sep 2015 12:43:22 +0200 Subject: [PATCH 11/35] Add variant with default diagonalize traits --- .../include/CGAL/hierarchical_clustering.h | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index 0a544317e31..ad58e32acb5 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -164,7 +164,6 @@ namespace CGAL { nonempty->second = centroid (nonempty->first.begin (), nonempty->first.end ()); clusters_stack.erase (empty); - clusters_stack.pop_back (); } else { @@ -185,8 +184,8 @@ namespace CGAL { - positive_side->first.size () * positive_side->second.z ()) / negative_side->first.size ()); - clusters_stack.pop_back (); } + clusters_stack.pop_back (); } // If the size/variance are small enough, add the centroid as // and output point @@ -200,6 +199,25 @@ namespace CGAL { // This variant deduces the kernel from the iterator type. + template + void hierarchical_clustering (InputIterator begin, + InputIterator end, + PointPMap point_pmap, + OutputIterator out, + const unsigned int size, + const double var_max, + const DiagonalizeTraits& diagonalize_traits) + { + typedef typename boost::property_traits::value_type Point; + typedef typename Kernel_traits::Kernel Kernel; + hierarchical_clustering (begin, end, point_pmap, out, size, var_max, + diagonalize_traits, Kernel()); + } + + // This variant uses default diagonalize traits template From e6c757f4634ee5905b68b2effed8b4b2b3e510ef Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Fri, 11 Sep 2015 13:02:13 +0200 Subject: [PATCH 12/35] Begin working on reference manual --- .../include/CGAL/hierarchical_clustering.h | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index ad58e32acb5..a624dfa5a14 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -35,6 +35,33 @@ namespace CGAL { + /// \ingroup PkgPointSetProcessing + +/// Recursively split the point set in smaller clusters until the +/// clusters have less than `size` elements or until their variation +/// factor is below `var_max`. +/// +/// This method does not change the input point set: the output is not +/// a subset of the input and is stored in a different container. +/// +/// \pre `1/3 > var_max > 0` +/// \pre 'size > 0` +/// +/// @tparam InputIterator iterator over input points. +/// @tparam PointPMap is a model of `ReadablePropertyMap` with value type `Point_3`. +/// It can be omitted if the value type of `InputIterator` is convertible to `Point_3`. +/// @tparam OuputIterator back inserter on a container with value type `Point_3`. +/// @tparam DiagonalizeTraits is a model of `DiagonalizeTraits`. It +/// can be omitted: if Eigen 3 (or greater) is available and +/// `CGAL_EIGEN3_ENABLED` is defined then an overload using +/// `Eigen_diagonalize_traits` is provided. Otherwise, the internal +/// implementation `Internal_diagonalize_traits` is used. +/// @tparam Kernel Geometric traits class. +/// It can be omitted and deduced automatically from the value type of `PointPMap`. +/// + +// This variant requires all parameters. + template (), Kernel()); } +/// @endcond +/// @cond SKIP_IN_MANUAL // This variant creates a default point property map = Identity_property_map. template @@ -252,6 +285,7 @@ namespace CGAL { #endif out, size, var_max); } +/// @endcond } // namespace CGAL From bd5262040428926d670bd84483a1b3d4177f6432 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Fri, 11 Sep 2015 15:33:03 +0200 Subject: [PATCH 13/35] Add a test for hierarchical_clustering --- .../Point_set_processing_3/CMakeLists.txt | 1 + .../hierarchical_clustering_test.cpp | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 Point_set_processing_3/test/Point_set_processing_3/hierarchical_clustering_test.cpp diff --git a/Point_set_processing_3/test/Point_set_processing_3/CMakeLists.txt b/Point_set_processing_3/test/Point_set_processing_3/CMakeLists.txt index 6ee5d8641b6..f055a789ff4 100644 --- a/Point_set_processing_3/test/Point_set_processing_3/CMakeLists.txt +++ b/Point_set_processing_3/test/Point_set_processing_3/CMakeLists.txt @@ -71,6 +71,7 @@ if ( CGAL_FOUND ) if(EIGEN3_FOUND OR LAPACK_FOUND) # Executables that require Eigen or BLAS and LAPACK create_single_source_cgal_program( "normal_estimation_test.cpp" ) + create_single_source_cgal_program( "hierarchical_clustering_test.cpp" ) create_single_source_cgal_program( "smoothing_test.cpp" ) create_single_source_cgal_program( "vcm_plane_test.cpp" ) create_single_source_cgal_program( "vcm_all_test.cpp" ) diff --git a/Point_set_processing_3/test/Point_set_processing_3/hierarchical_clustering_test.cpp b/Point_set_processing_3/test/Point_set_processing_3/hierarchical_clustering_test.cpp new file mode 100644 index 00000000000..32c3e2819cb --- /dev/null +++ b/Point_set_processing_3/test/Point_set_processing_3/hierarchical_clustering_test.cpp @@ -0,0 +1,90 @@ +#include +#include +#include +#include +#include +#include + +#include +#include + +// types +typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel; +typedef Kernel::Point_3 Point; +typedef Kernel::FT FT; + +void test (std::vector& input, + int result1 = 1, int result2 = 1, int result3 = 1, int result4 = 1) +{ + std::vector output; + + CGAL::hierarchical_clustering (input.begin (), input.end (), + std::back_inserter (output)); + if (result1 > 0 && output.size () != static_cast(result1)) + exit (EXIT_FAILURE); + output.clear (); + + CGAL::hierarchical_clustering (input.begin (), input.end (), + std::back_inserter (output), 100); + if (result2 > 0 && output.size () != static_cast(result2)) + exit (EXIT_FAILURE); + output.clear (); + + CGAL::hierarchical_clustering (input.begin (), input.end (), + std::back_inserter (output), 1000, 0.1); + if (result3 > 0 && output.size () != static_cast(result3)) + exit (EXIT_FAILURE); + output.clear (); + + CGAL::hierarchical_clustering (input.begin (), input.end (), + CGAL::Identity_property_map(), + std::back_inserter (output), + std::numeric_limits::max(), + 0.0001); + if (result4 > 0 && output.size () != static_cast(result4)) + exit (EXIT_FAILURE); + + input.clear (); +} + + +int main(void) +{ + + std::vector input; + + // Test 1 point + input.push_back (Point (0., 0., 0.)); + test (input); + + // Test twice the same point + input.push_back (Point (0., 0., 0.)); + input.push_back (Point (0., 0., 0.)); + test (input); + + // Test 2 points + input.push_back (Point (0., 0., 0.)); + input.push_back (Point (1., 0., 0.)); + test (input); + + // Test line + for (std::size_t i = 0; i < 1000; ++ i) + input.push_back (Point (0., 0., i)); + test (input, 128, 16, 1, 1); + + // Test plane + for (std::size_t i = 0; i < 128; ++ i) + for (std::size_t j = 0; j < 128; ++ j) + input.push_back (Point (0., j, i)); + test (input, 2048, 256, 32, 1); + + // Test random + for (std::size_t i = 0; i < 10000; ++ i) + input.push_back (Point (rand() / (FT)RAND_MAX, + rand() / (FT)RAND_MAX, + rand() / (FT)RAND_MAX)); + test (input, -1, -1, -1, -1); + + return EXIT_SUCCESS; +} + From bf8d876d6d75c868f61202203712fdf4caa474c9 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Fri, 11 Sep 2015 15:44:49 +0200 Subject: [PATCH 14/35] More on user manual and reference manual --- .../Point_set_processing_3/PackageDescription.txt | 1 + .../Point_set_processing_3.txt | 12 ++++++++++++ .../doc/Point_set_processing_3/examples.txt | 1 + .../include/CGAL/hierarchical_clustering.h | 4 ++-- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Point_set_processing_3/doc/Point_set_processing_3/PackageDescription.txt b/Point_set_processing_3/doc/Point_set_processing_3/PackageDescription.txt index 95815baa255..80b5417f5cb 100644 --- a/Point_set_processing_3/doc/Point_set_processing_3/PackageDescription.txt +++ b/Point_set_processing_3/doc/Point_set_processing_3/PackageDescription.txt @@ -31,6 +31,7 @@ - `CGAL::grid_simplify_point_set()` - `CGAL::random_simplify_point_set()` - `CGAL::wlop_simplify_and_regularize_point_set()` +- `CGAL::hierarchical_clustering()` - `CGAL::jet_smooth_point_set()` - `CGAL::bilateral_smooth_point_set()` - `CGAL::jet_estimate_normals()` diff --git a/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt b/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt index 4011ef657d1..82f69a23bf7 100644 --- a/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt +++ b/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt @@ -154,6 +154,12 @@ Function `wlop_simplify_and_regularize_point_set()` not only simplifies, but also regularizes downsampled points. This is an implementation of the Weighted Locally Optimal Projection (WLOP) algorithm \cgalCite{wlop-2009}. +Function `hierarchical_clustering()` is not strictly speaking a +simplification algorithm as its output is not a subset of the input +point set. However, it provides an adaptative simplified +representation of the point set through local clusters: the size of +the clusters is either directly selected by the user or it +automatically adapts to the local variation of the point set. \subsection Point_set_processing_3Example_3 Grid Simplification Example @@ -199,6 +205,12 @@ for more details. We provide below a speed-up chart generated using the parallel Parallel WLOP speed-up, compared to the sequential version of the algorithm. \cgalFigureEnd +\subsection Point_set_processing_3Example_9 Hierarchical Clustering Example + +\todo Document me + +\cgalExample{Point_set_processing_3/hierarchical_clustering_example.cpp} + \section Point_set_processing_3Smoothing Smoothing Two smoothing functions are devised to smooth an input point set. diff --git a/Point_set_processing_3/doc/Point_set_processing_3/examples.txt b/Point_set_processing_3/doc/Point_set_processing_3/examples.txt index aeff45529c1..73f842d423b 100644 --- a/Point_set_processing_3/doc/Point_set_processing_3/examples.txt +++ b/Point_set_processing_3/doc/Point_set_processing_3/examples.txt @@ -4,6 +4,7 @@ \example Point_set_processing_3/remove_outliers_example.cpp \example Point_set_processing_3/grid_simplification_example.cpp \example Point_set_processing_3/grid_simplify_indices.cpp +\example Point_set_processing_3/hierarchical_clustering_example.cpp \example Point_set_processing_3/jet_smoothing_example.cpp \example Point_set_processing_3/normals_example.cpp \example Point_set_processing_3/wlop_simplify_and_regularize_point_set_example.cpp diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index a624dfa5a14..aa7f58666a8 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -44,8 +44,8 @@ namespace CGAL { /// This method does not change the input point set: the output is not /// a subset of the input and is stored in a different container. /// -/// \pre `1/3 > var_max > 0` -/// \pre 'size > 0` +/// \pre `0 < var_max < 1/3` +/// \pre `size > 0` /// /// @tparam InputIterator iterator over input points. /// @tparam PointPMap is a model of `ReadablePropertyMap` with value type `Point_3`. From b84b7249848c023c1050506250721a90ca6f46fd Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Mon, 14 Sep 2015 10:59:24 +0200 Subject: [PATCH 15/35] Add citation and update manual --- Documentation/biblio/cgal_manual.bib | 9 +++++++ .../Point_set_processing_3.txt | 25 ++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Documentation/biblio/cgal_manual.bib b/Documentation/biblio/cgal_manual.bib index 03c94607efb..2590e3c8f52 100644 --- a/Documentation/biblio/cgal_manual.bib +++ b/Documentation/biblio/cgal_manual.bib @@ -1550,6 +1550,15 @@ ABSTRACT = {We present the first complete, exact and efficient C++ implementatio } +@inproceedings{cgal:pgk-esops-02, + title={Efficient simplification of point-sampled surfaces}, + author={Pauly, Mark and Gross, Markus and Kobbelt, Leif P}, + booktitle={Proceedings of the conference on Visualization'02}, + pages={163--170}, + year={2002}, + organization={IEEE Computer Society} +} + @article{ cgal:pp-cdmsc-93, author = "U. Pinkall and K. Polthier", title = "Computing discrete minimal surfaces and their conjugates", diff --git a/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt b/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt index 82f69a23bf7..354873612ea 100644 --- a/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt +++ b/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt @@ -159,7 +159,8 @@ simplification algorithm as its output is not a subset of the input point set. However, it provides an adaptative simplified representation of the point set through local clusters: the size of the clusters is either directly selected by the user or it -automatically adapts to the local variation of the point set. +automatically adapts to the local variation of the point set +\cgalCite{cgal:pgk-esops-02}. \subsection Point_set_processing_3Example_3 Grid Simplification Example @@ -206,11 +207,29 @@ Parallel WLOP speed-up, compared to the sequential version of the algorithm. \cgalFigureEnd \subsection Point_set_processing_3Example_9 Hierarchical Clustering Example - -\todo Document me +The following example reads a point set and produces a set of clusters. \cgalExample{Point_set_processing_3/hierarchical_clustering_example.cpp} +\subsubsection Point_set_processing_3Hierarchical_clustering_parameter_size Parameter: size +The hierarchical clustering algorithm recursively split the point set +in two until each cluster's size is less than the parameter `size`. + +\subsubsection Point_set_processing_3Hierarchical_clustering_parameter_var_max Parameter: var_max +In addition to the size parameter, a variation parameter allows to +increase simplification in monotoneous regions. For each cluster, a +surface variation measure is computed using the sorted eigenvalues of +the covariance matrix: \f[ \sigma(p) = \frac{\lambda_0}{\lambda_0 + + \lambda_1 + \lambda_2}. \f] + +This function goes from \f$0\f$ if the cluster is coplanar to +\f$1/3\f$ if it is fully isotropic. If a cluster's variation is above +`var_max`, it is splitted. If `var_max` is equal to \f$1/3\f$, this +parameter has no effect and the clustering is regular on the whole +point set. + + + \section Point_set_processing_3Smoothing Smoothing Two smoothing functions are devised to smooth an input point set. From 59e11b4ae6ed90ab78ecb9d637397483281fa6e9 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Mon, 14 Sep 2015 12:15:28 +0200 Subject: [PATCH 16/35] More on user manual --- .../Point_set_processing_3.txt | 10 ++++++++++ .../fig/hierarchical_clustering_size.jpg | Bin 0 -> 68376 bytes .../fig/hierarchical_clustering_var_max.jpg | Bin 0 -> 89203 bytes 3 files changed, 10 insertions(+) create mode 100644 Point_set_processing_3/doc/Point_set_processing_3/fig/hierarchical_clustering_size.jpg create mode 100644 Point_set_processing_3/doc/Point_set_processing_3/fig/hierarchical_clustering_var_max.jpg diff --git a/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt b/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt index 354873612ea..f9fb91427d6 100644 --- a/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt +++ b/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt @@ -215,6 +215,12 @@ The following example reads a point set and produces a set of clusters. The hierarchical clustering algorithm recursively split the point set in two until each cluster's size is less than the parameter `size`. +\cgalFigureBegin{Point_set_processing_3figHierarchical_clustering_size, hierarchical_clustering_size.jpg} +Input point set and hierarchical clustering with different `size` +parameter: \f$10\f$, \f$100\f$ and \f$1000\f$. In the 3 cases, `var_max`\f$=1/3\f$. +\cgalFigureEnd + + \subsubsection Point_set_processing_3Hierarchical_clustering_parameter_var_max Parameter: var_max In addition to the size parameter, a variation parameter allows to increase simplification in monotoneous regions. For each cluster, a @@ -228,6 +234,10 @@ This function goes from \f$0\f$ if the cluster is coplanar to parameter has no effect and the clustering is regular on the whole point set. +\cgalFigureBegin{Point_set_processing_3figHierarchical_clustering_var_max, hierarchical_clustering_var_max.jpg} +Input point set and hierarchical clustering with different `var_max` +parameter: \f$0.00001\f$, \f$0.001\f$ and \f$0.1\f$. In the 3 cases, `size`\f$=1000\f$. +\cgalFigureEnd \section Point_set_processing_3Smoothing Smoothing diff --git a/Point_set_processing_3/doc/Point_set_processing_3/fig/hierarchical_clustering_size.jpg b/Point_set_processing_3/doc/Point_set_processing_3/fig/hierarchical_clustering_size.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8183a0fdf12f0df906b37cd39c05936e872e7643 GIT binary patch literal 68376 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 M4E Date: Mon, 14 Sep 2015 12:16:24 +0200 Subject: [PATCH 17/35] Hierarchical clustering plugin for Polyhedron demo --- Polyhedron/demo/Polyhedron/CMakeLists.txt | 4 + ...int_set_hierarchical_clustering_plugin.cpp | 114 ++++++++++++++++++ ...oint_set_hierarchical_clustering_plugin.ui | 113 +++++++++++++++++ .../demo/Polyhedron/cgal_test_with_cmake | 1 + 4 files changed, 232 insertions(+) create mode 100644 Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.cpp create mode 100644 Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.ui diff --git a/Polyhedron/demo/Polyhedron/CMakeLists.txt b/Polyhedron/demo/Polyhedron/CMakeLists.txt index 2df990619b6..267458ed9e7 100644 --- a/Polyhedron/demo/Polyhedron/CMakeLists.txt +++ b/Polyhedron/demo/Polyhedron/CMakeLists.txt @@ -442,6 +442,10 @@ if(CGAL_Qt5_FOUND AND Qt5_FOUND AND OPENGL_FOUND AND QGLVIEWER_FOUND) polyhedron_demo_plugin(point_set_simplification_plugin Polyhedron_demo_point_set_simplification_plugin ${point_set_simplificationUI_FILES}) target_link_libraries(point_set_simplification_plugin scene_points_with_normal_item) + qt5_wrap_ui(point_set_hierarchical_clusteringUI_FILES Polyhedron_demo_point_set_hierarchical_clustering_plugin.ui) + polyhedron_demo_plugin(point_set_hierarchical_clustering_plugin Polyhedron_demo_point_set_hierarchical_clustering_plugin ${point_set_hierarchical_clusteringUI_FILES}) + target_link_libraries(point_set_hierarchical_clustering_plugin scene_points_with_normal_item) + qt5_wrap_ui( ps_outliers_removal_UI_FILES Polyhedron_demo_point_set_outliers_removal_plugin.ui) polyhedron_demo_plugin(point_set_selection_plugin Polyhedron_demo_point_set_selection_plugin ${point_set_selectionUI_FILES}) diff --git a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.cpp b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.cpp new file mode 100644 index 00000000000..176402f3752 --- /dev/null +++ b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.cpp @@ -0,0 +1,114 @@ +#include "config.h" +#include "Scene_points_with_normal_item.h" +#include "Polyhedron_demo_plugin_helper.h" +#include "Polyhedron_demo_plugin_interface.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "ui_Polyhedron_demo_point_set_hierarchical_clustering_plugin.h" + +class Polyhedron_demo_point_set_hierarchical_clustering_plugin : + public QObject, + public Polyhedron_demo_plugin_helper +{ + Q_OBJECT + Q_INTERFACES(Polyhedron_demo_plugin_interface) + Q_PLUGIN_METADATA(IID "com.geometryfactory.PolyhedronDemo.PluginInterface/1.0") + + QAction* actionHierarchicalCluster; + +public: + void init(QMainWindow* mainWindow, Scene_interface* scene_interface) { + actionHierarchicalCluster = new QAction(tr("Point set hierarchical clustering"), mainWindow); + actionHierarchicalCluster->setObjectName("actionHierarchicalCluster"); + + Polyhedron_demo_plugin_helper::init(mainWindow, scene_interface); + } + + bool applicable(QAction*) const { + return qobject_cast(scene->item(scene->mainSelectionIndex())); + } + + QList actions() const { + return QList() << actionHierarchicalCluster; + } + +public Q_SLOTS: + void on_actionHierarchicalCluster_triggered(); + +}; // end Polyhedron_demo_point_set_hierarchical_clustering_plugin + +class Point_set_demo_point_set_hierarchical_clustering_dialog : public QDialog, private Ui::PointSetHierarchicalClusteringDialog +{ + Q_OBJECT +public: + Point_set_demo_point_set_hierarchical_clustering_dialog(QWidget * /*parent*/ = 0) + { + setupUi(this); + } + + int size() const { return m_maximumClusterSize->value(); } + double var_max() const { return m_maximumSurfaceVariation->value(); } +}; + +void Polyhedron_demo_point_set_hierarchical_clustering_plugin::on_actionHierarchicalCluster_triggered() +{ + const Scene_interface::Item_id index = scene->mainSelectionIndex(); + + Scene_points_with_normal_item* item = + qobject_cast(scene->item(index)); + + if(item) + { + // Gets point set + Point_set* points = item->point_set(); + if(points == NULL) + return; + + // Gets options + Point_set_demo_point_set_hierarchical_clustering_dialog dialog; + if(!dialog.exec()) + return; + + QApplication::setOverrideCursor(Qt::WaitCursor); + + CGAL::Timer task_timer; task_timer.start(); + + std::cerr << "Hierarchical clustering (cluster size = " << dialog.size() + << ", maximum variation = " << dialog.var_max() << ")" << std::endl; + + + + Scene_points_with_normal_item* new_item = new Scene_points_with_normal_item(); + CGAL::hierarchical_clustering(points->begin(), points->end(), + std::back_inserter (*(new_item->point_set())), + dialog.size(), dialog.var_max()); + + new_item->setName(QString("%1 (hierarchical clustering)").arg(item->name())); + new_item->set_has_normals (false); + new_item->setColor(item->color()); + new_item->setRenderingMode(item->renderingMode()); + new_item->setVisible(item->visible()); + scene->addItem(new_item); + + std::size_t memory = CGAL::Memory_sizer().virtual_size(); + std::cerr << "Clustering: " << new_item->point_set()->size () << " point(s) generated (" + << task_timer.time() << " seconds, " + << (memory>>20) << " Mb allocated)" + << std::endl; + + QApplication::restoreOverrideCursor(); + + } +} + +#include "Polyhedron_demo_point_set_hierarchical_clustering_plugin.moc" diff --git a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.ui b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.ui new file mode 100644 index 00000000000..3c43cd18c76 --- /dev/null +++ b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.ui @@ -0,0 +1,113 @@ + + + PointSetHierarchicalClusteringDialog + + + + 0 + 0 + 403 + 137 + + + + Hierarchical Clustering + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Maximum cluster size + + + + + + + + + + 5 + + + 0.000010000000000 + + + 0.333330000000000 + + + 0.012340000000000 + + + 0.333330000000000 + + + + + + + Maximum surface variation + + + + + + + 1 + + + 2147483647 + + + 10 + + + + + + + + + buttonBox + accepted() + PointSetHierarchicalClusteringDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + PointSetHierarchicalClusteringDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/Polyhedron/demo/Polyhedron/cgal_test_with_cmake b/Polyhedron/demo/Polyhedron/cgal_test_with_cmake index 2f49750a143..388d1588828 100755 --- a/Polyhedron/demo/Polyhedron/cgal_test_with_cmake +++ b/Polyhedron/demo/Polyhedron/cgal_test_with_cmake @@ -130,6 +130,7 @@ else point_inside_polyhedron_plugin \ point_set_average_spacing_plugin \ point_set_bilateral_smoothing_plugin \ + point_set_hierarchical_clustering_plugin \ point_set_outliers_removal_plugin \ point_set_selection_plugin \ point_set_shape_detection_plugin \ From c965754d00051b05a218ff91329ce0d709839bc3 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Mon, 14 Sep 2015 12:52:05 +0200 Subject: [PATCH 18/35] Update example of hierarchical clustering --- .../hierarchical_clustering_example.cpp | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Point_set_processing_3/examples/Point_set_processing_3/hierarchical_clustering_example.cpp b/Point_set_processing_3/examples/Point_set_processing_3/hierarchical_clustering_example.cpp index 6a0b75a5dc0..60616564a15 100644 --- a/Point_set_processing_3/examples/Point_set_processing_3/hierarchical_clustering_example.cpp +++ b/Point_set_processing_3/examples/Point_set_processing_3/hierarchical_clustering_example.cpp @@ -1,6 +1,10 @@ #include #include #include +#include +#include +#include + #include #include @@ -21,11 +25,23 @@ int main(int argc, char*argv[]) std::cerr << "Error: cannot read file " << fname << std::endl; return EXIT_FAILURE; } + std::cout << "Read " << points.size () << " point(s)" << std::endl; - std::vector output; // Algorithm generate a new set of points + CGAL::Timer task_timer; task_timer.start(); + + std::vector output; // Algorithm generates a new set of points CGAL::hierarchical_clustering (points.begin (), points.end (), - std::back_inserter (output)); + std::back_inserter (output), 100); + std::size_t memory = CGAL::Memory_sizer().virtual_size(); + + std::cout << output.size () << " point(s) generated in " + << task_timer.time() << " seconds, " + << (memory>>20) << " Mib allocated." << std::endl; + + std::ofstream f ("out.xyz"); + CGAL::write_xyz_points (f, output.begin (), output.end ()); + return EXIT_SUCCESS; } From 5df527d2f00c6447ff739e4c16034b901a478af9 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Mon, 14 Sep 2015 14:06:09 +0200 Subject: [PATCH 19/35] Correct preconditions --- Point_set_processing_3/include/CGAL/hierarchical_clustering.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index aa7f58666a8..c0302d66317 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -88,8 +88,8 @@ namespace CGAL { typedef typename std::list::iterator cluster_iterator; CGAL_precondition (begin != end); - CGAL_point_set_processing_precondition - (var_max >= 0.0 && var_max <= 1./3.); + CGAL_point_set_processing_precondition (size > 0); + CGAL_point_set_processing_precondition (var_max > 0.0); // The first cluster is the whole input point set clusters_stack.push_front (cluster (std::list(), Point (0., 0., 0.))); From cf9b7230d8027395db0b23d2460b4ba5d09bba53 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Mon, 14 Sep 2015 14:27:42 +0200 Subject: [PATCH 20/35] Algorithm is faster if the plane is not constructed --- .../include/CGAL/hierarchical_clustering.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index c0302d66317..296c9881154 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -157,10 +157,10 @@ namespace CGAL { clusters_stack.push_front (cluster (std::list(), Point (0., 0., 0.))); cluster_iterator negative_side = clusters_stack.begin (); - // Compute the plane which splits the point set into 2 point sets: + // The plane which splits the point set into 2 point sets: // * Normal to the eigenvector with highest eigenvalue // * Passes through the centroid of the set - Plane plane (current_cluster.second, Vector (eigenvectors[6], eigenvectors[7], eigenvectors[8])); + Vector v (eigenvectors[6], eigenvectors[7], eigenvectors[8]); std::size_t current_cluster_size = 0; typename std::list::iterator it = current_cluster.first.begin (); @@ -168,7 +168,8 @@ namespace CGAL { { typename std::list::iterator current = it ++; - std::list& side = (plane.has_on_positive_side (*current) + // Test if point is on one side or the other of the plane + std::list& side = (Vector (current_cluster.second, *current) * v > 0 ? positive_side->first : negative_side->first); side.splice (side.end (), current_cluster.first, current); ++ current_cluster_size; From b16f7605d689332ed78158d01a82e92af8d74775 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Mon, 14 Sep 2015 14:39:37 +0200 Subject: [PATCH 21/35] Enhancement: keep current cluster an only build one side instead of two --- .../include/CGAL/hierarchical_clustering.h | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index 296c9881154..b2de4dd253a 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -107,25 +107,25 @@ namespace CGAL { while (!(clusters_stack.empty ())) { - cluster& current_cluster = clusters_stack.back (); + cluster_iterator current_cluster = clusters_stack.begin (); // If the cluster only has 1 element, we add it to the list of // output points - if (current_cluster.first.size () == 1) + if (current_cluster->first.size () == 1) { - *(out ++) = current_cluster.second; - clusters_stack.pop_back (); + *(out ++) = current_cluster->second; + clusters_stack.pop_front (); continue; } // Compute the covariance matrix of the set cpp11::array covariance = {{ 0., 0., 0., 0., 0., 0. }}; - for (typename std::list::iterator it = current_cluster.first.begin (); - it != current_cluster.first.end (); ++ it) + for (typename std::list::iterator it = current_cluster->first.begin (); + it != current_cluster->first.end (); ++ it) { const Point& p = *it; - Vector d = p - current_cluster.second; + Vector d = p - current_cluster->second; covariance[0] += d.x () * d.x (); covariance[1] += d.x () * d.y (); covariance[2] += d.x () * d.z (); @@ -150,12 +150,11 @@ namespace CGAL { var = eigenvalues[0] / var; // Split the set if size OR variance of the cluster is too large - if (current_cluster.first.size () > size || var > var_max) + if (current_cluster->first.size () > size || var > var_max) { - clusters_stack.push_front (cluster (std::list(), Point (0., 0., 0.))); - cluster_iterator positive_side = clusters_stack.begin (); clusters_stack.push_front (cluster (std::list(), Point (0., 0., 0.))); cluster_iterator negative_side = clusters_stack.begin (); + // positive_side is built directly from current_cluster // The plane which splits the point set into 2 point sets: // * Normal to the eigenvector with highest eigenvalue @@ -163,30 +162,32 @@ namespace CGAL { Vector v (eigenvectors[6], eigenvectors[7], eigenvectors[8]); std::size_t current_cluster_size = 0; - typename std::list::iterator it = current_cluster.first.begin (); - while (it != current_cluster.first.end ()) + typename std::list::iterator it = current_cluster->first.begin (); + while (it != current_cluster->first.end ()) { typename std::list::iterator current = it ++; - // Test if point is on one side or the other of the plane - std::list& side = (Vector (current_cluster.second, *current) * v > 0 - ? positive_side->first : negative_side->first); - side.splice (side.end (), current_cluster.first, current); + // Test if point is on negative side of plane and + // transfer it to the negative_side cluster if it is + if (Vector (current_cluster->second, *current) * v < 0) + negative_side->first.splice (negative_side->first.end (), + current_cluster->first, current); ++ current_cluster_size; } - if (positive_side->first.empty () || negative_side->first.empty ()) + // If one of the clusters is empty, only keep the non-empty one + if (current_cluster->first.empty () || negative_side->first.empty ()) { cluster_iterator empty, nonempty; - if (positive_side->first.empty ()) + if (current_cluster->first.empty ()) { - empty = positive_side; + empty = current_cluster; nonempty = negative_side; } else { empty = negative_side; - nonempty = positive_side; + nonempty = current_cluster; } nonempty->second = centroid (nonempty->first.begin (), nonempty->first.end ()); @@ -195,32 +196,33 @@ namespace CGAL { } else { - // Compute the centroids - positive_side->second = centroid (positive_side->first.begin (), positive_side->first.end ()); + // Save old centroid for faster computation + Point old_centroid = current_cluster->second; + + // Compute the first centroid + current_cluster->second = centroid (current_cluster->first.begin (), current_cluster->first.end ()); // The second centroid can be computed with the first and - // the previous ones : - // centroid_neg = (n_total * centroid - n_pos * centroid_pos) + // the old ones : + // centroid_neg = (n_total * old_centroid - n_pos * first_centroid) // / n_neg; - negative_side->second = Point ((current_cluster_size * current_cluster.second.x () - - positive_side->first.size () * positive_side->second.x ()) + negative_side->second = Point ((current_cluster_size * old_centroid.x () + - current_cluster->first.size () * current_cluster->second.x ()) / negative_side->first.size (), - (current_cluster_size * current_cluster.second.y () - - positive_side->first.size () * positive_side->second.y ()) + (current_cluster_size * old_centroid.y () + - current_cluster->first.size () * current_cluster->second.y ()) / negative_side->first.size (), - (current_cluster_size * current_cluster.second.z () - - positive_side->first.size () * positive_side->second.z ()) + (current_cluster_size * old_centroid.z () + - current_cluster->first.size () * current_cluster->second.z ()) / negative_side->first.size ()); - } - clusters_stack.pop_back (); } // If the size/variance are small enough, add the centroid as // and output point else { - *(out ++) = current_cluster.second; - clusters_stack.pop_back (); + *(out ++) = current_cluster->second; + clusters_stack.pop_front (); } } } From 648c19b6f6d3d8b158f140016897f186a412de17 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Mon, 14 Sep 2015 15:55:28 +0200 Subject: [PATCH 22/35] Remove unused typedef warning --- Point_set_processing_3/include/CGAL/hierarchical_clustering.h | 1 - 1 file changed, 1 deletion(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index b2de4dd253a..c01d11f3193 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -76,7 +76,6 @@ namespace CGAL { const DiagonalizeTraits&, const Kernel&) { - typedef typename Kernel::Plane_3 Plane; typedef typename Kernel::Point_3 Point; typedef typename Kernel::Vector_3 Vector; From 28cf05f189a52fc3dcdb1b346786cb11174c3a5a Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Mon, 14 Sep 2015 18:10:55 +0200 Subject: [PATCH 23/35] Remove duplicate code (use PCA_util.h to assemble covariance matrix) --- .../include/CGAL/hierarchical_clustering.h | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index c01d11f3193..1380e2ea8ff 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -31,6 +31,7 @@ #include #include #include +#include namespace CGAL { @@ -76,6 +77,7 @@ namespace CGAL { const DiagonalizeTraits&, const Kernel&) { + typedef typename Kernel::FT FT; typedef typename Kernel::Point_3 Point; typedef typename Kernel::Vector_3 Vector; @@ -118,25 +120,30 @@ namespace CGAL { } // Compute the covariance matrix of the set - cpp11::array covariance = {{ 0., 0., 0., 0., 0., 0. }}; + cpp11::array covariance = {{ 0., 0., 0., 0., 0., 0. }}; - for (typename std::list::iterator it = current_cluster->first.begin (); - it != current_cluster->first.end (); ++ it) - { - const Point& p = *it; - Vector d = p - current_cluster->second; - covariance[0] += d.x () * d.x (); - covariance[1] += d.x () * d.y (); - covariance[2] += d.x () * d.z (); - covariance[3] += d.y () * d.y (); - covariance[4] += d.y () * d.z (); - covariance[5] += d.z () * d.z (); - } + internal::assemble_covariance_matrix_3 (current_cluster->first.begin (), + current_cluster->first.end (), + covariance, + current_cluster->second, Kernel(), + (Point*)NULL, CGAL::Dimension_tag<0>()); + // for (typename std::list::iterator it = current_cluster->first.begin (); + // it != current_cluster->first.end (); ++ it) + // { + // const Point& p = *it; + // Vector d = p - current_cluster->second; + // covariance[0] += d.x () * d.x (); + // covariance[1] += d.x () * d.y (); + // covariance[2] += d.x () * d.z (); + // covariance[3] += d.y () * d.y (); + // covariance[4] += d.y () * d.z (); + // covariance[5] += d.z () * d.z (); + // } - cpp11::array eigenvalues = {{ 0., 0., 0. }}; - cpp11::array eigenvectors = {{ 0., 0., 0., - 0., 0., 0., - 0., 0., 0. }}; + cpp11::array eigenvalues = {{ 0., 0., 0. }}; + cpp11::array eigenvectors = {{ 0., 0., 0., + 0., 0., 0., + 0., 0., 0. }}; // Linear algebra = get eigenvalues and eigenvectors for // PCA-like analysis DiagonalizeTraits::diagonalize_selfadjoint_covariance_matrix From 347af82e8020cc9d4cc63f48c0dc845aafc380f1 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 15 Sep 2015 10:25:09 +0200 Subject: [PATCH 24/35] Fix testsuite error (missing #include ) --- .../Point_set_processing_3/hierarchical_clustering_test.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Point_set_processing_3/test/Point_set_processing_3/hierarchical_clustering_test.cpp b/Point_set_processing_3/test/Point_set_processing_3/hierarchical_clustering_test.cpp index 32c3e2819cb..4c4f3e54993 100644 --- a/Point_set_processing_3/test/Point_set_processing_3/hierarchical_clustering_test.cpp +++ b/Point_set_processing_3/test/Point_set_processing_3/hierarchical_clustering_test.cpp @@ -1,3 +1,5 @@ +#include + #include #include #include From 20698d4dde427e1e637021daaf09eff32eb24b1f Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 15 Sep 2015 11:34:21 +0200 Subject: [PATCH 25/35] Make the algorithm a simplification algorithm instead of a clustering one --- .../include/CGAL/hierarchical_clustering.h | 298 +++++++++++------- 1 file changed, 189 insertions(+), 109 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h index 1380e2ea8ff..c48ae4a06ab 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchical_clustering.h @@ -36,54 +36,128 @@ namespace CGAL { + namespace internal { + + template < typename InputIterator, + typename PointPMap, + typename K > + typename K::Point_3 + hcs_centroid(InputIterator begin, + InputIterator end, + PointPMap point_pmap, + const K&) + { + typedef typename K::Vector_3 Vector; + typedef typename K::Point_3 Point; + typedef typename K::FT FT; + + CGAL_precondition(begin != end); + + Vector v = NULL_VECTOR; + unsigned int nb_pts = 0; + while(begin != end) + { +#ifdef CGAL_USE_PROPERTY_MAPS_API_V1 + Point point = get(point_pmap, begin); +#else + Point point = get(point_pmap, *begin); +#endif + v = v + (point - ORIGIN); + ++ nb_pts; + ++ begin; + } + return ORIGIN + v / (FT)nb_pts; + } + + template < typename Input_type, + typename PointPMap, + typename K > + void + hsc_terminate_cluster (std::list& cluster, + std::list& points_to_keep, + std::list& points_to_remove, + PointPMap point_pmap, + typename K::Point_3 centroid, + const K&) + { + typedef typename std::list::iterator Iterator; + typedef typename K::FT FT; + typedef typename K::Point_3 Point; + + FT dist_min = std::numeric_limits::max(); + + typename std::list::iterator point_min; + for (Iterator it = cluster.begin (); it != cluster.end (); ++ it) + { +#ifdef CGAL_USE_PROPERTY_MAPS_API_V1 + Point point = get(point_pmap, it); +#else + Point point = get(point_pmap, *it); +#endif + FT dist = CGAL::squared_distance (point, centroid); + if (dist < dist_min) + { + dist_min = dist; + point_min = it; + } + } + + points_to_keep.splice (points_to_keep.end (), cluster, point_min); + points_to_remove.splice (points_to_remove.end (), cluster, cluster.begin (), cluster.end ()); + } + + + + + } // namespace internal + + /// \ingroup PkgPointSetProcessing -/// Recursively split the point set in smaller clusters until the -/// clusters have less than `size` elements or until their variation -/// factor is below `var_max`. -/// -/// This method does not change the input point set: the output is not -/// a subset of the input and is stored in a different container. -/// -/// \pre `0 < var_max < 1/3` -/// \pre `size > 0` -/// -/// @tparam InputIterator iterator over input points. -/// @tparam PointPMap is a model of `ReadablePropertyMap` with value type `Point_3`. -/// It can be omitted if the value type of `InputIterator` is convertible to `Point_3`. -/// @tparam OuputIterator back inserter on a container with value type `Point_3`. -/// @tparam DiagonalizeTraits is a model of `DiagonalizeTraits`. It -/// can be omitted: if Eigen 3 (or greater) is available and -/// `CGAL_EIGEN3_ENABLED` is defined then an overload using -/// `Eigen_diagonalize_traits` is provided. Otherwise, the internal -/// implementation `Internal_diagonalize_traits` is used. -/// @tparam Kernel Geometric traits class. -/// It can be omitted and deduced automatically from the value type of `PointPMap`. -/// + /// Recursively split the point set in smaller clusters until the + /// clusters have less than `size` elements or until their variation + /// factor is below `var_max`. + /// + /// This method does not change the input point set: the output is not + /// a subset of the input and is stored in a different container. + /// + /// \pre `0 < var_max < 1/3` + /// \pre `size > 0` + /// + /// @tparam ForwardIterator iterator over input points. + /// @tparam PointPMap is a model of `ReadablePropertyMap` with value type `Point_3`. + /// It can be omitted if the value type of `ForwardIterator` is convertible to `Point_3`. + /// @tparam DiagonalizeTraits is a model of `DiagonalizeTraits`. It + /// can be omitted: if Eigen 3 (or greater) is available and + /// `CGAL_EIGEN3_ENABLED` is defined then an overload using + /// `Eigen_diagonalize_traits` is provided. Otherwise, the internal + /// implementation `Internal_diagonalize_traits` is used. + /// @tparam Kernel Geometric traits class. + /// It can be omitted and deduced automatically from the value type of `PointPMap`. + /// -// This variant requires all parameters. + // This variant requires all parameters. - template - void hierarchical_clustering (InputIterator begin, - InputIterator end, - PointPMap point_pmap, - OutputIterator out, - const unsigned int size, - const double var_max, - const DiagonalizeTraits&, - const Kernel&) + ForwardIterator hierarchical_clustering (ForwardIterator begin, + ForwardIterator end, + PointPMap point_pmap, + const unsigned int size, + const double var_max, + const DiagonalizeTraits&, + const Kernel&) { + typedef typename std::iterator_traits::value_type Input_type; typedef typename Kernel::FT FT; typedef typename Kernel::Point_3 Point; typedef typename Kernel::Vector_3 Vector; // We define a cluster as a point set + its centroid (useful for // faster computations of centroids - to be implemented) - typedef std::pair< std::list, Point > cluster; + typedef std::pair< std::list, Point > cluster; std::list clusters_stack; typedef typename std::list::iterator cluster_iterator; @@ -93,18 +167,16 @@ namespace CGAL { CGAL_point_set_processing_precondition (var_max > 0.0); // The first cluster is the whole input point set - clusters_stack.push_front (cluster (std::list(), Point (0., 0., 0.))); - for(InputIterator it = begin; it != end; it++) - { -#ifdef CGAL_USE_PROPERTY_MAPS_API_V1 - Point point = get(point_pmap, it); -#else - Point point = get(point_pmap, *it); -#endif - clusters_stack.front ().first.push_back (point); - } - clusters_stack.front ().second = centroid (clusters_stack.front ().first.begin (), - clusters_stack.front ().first.end ()); + clusters_stack.push_front (cluster (std::list(), Point (0., 0., 0.))); + for(ForwardIterator it = begin; it != end; it++) + clusters_stack.front ().first.push_back (*it); + + clusters_stack.front ().second = internal::hcs_centroid (clusters_stack.front ().first.begin (), + clusters_stack.front ().first.end (), + point_pmap, Kernel()); + + std::list points_to_keep; + std::list points_to_remove; while (!(clusters_stack.empty ())) { @@ -114,7 +186,8 @@ namespace CGAL { // output points if (current_cluster->first.size () == 1) { - *(out ++) = current_cluster->second; + points_to_keep.splice (points_to_keep.end (), current_cluster->first, + current_cluster->first.begin ()); clusters_stack.pop_front (); continue; } @@ -122,23 +195,22 @@ namespace CGAL { // Compute the covariance matrix of the set cpp11::array covariance = {{ 0., 0., 0., 0., 0., 0. }}; - internal::assemble_covariance_matrix_3 (current_cluster->first.begin (), - current_cluster->first.end (), - covariance, - current_cluster->second, Kernel(), - (Point*)NULL, CGAL::Dimension_tag<0>()); - // for (typename std::list::iterator it = current_cluster->first.begin (); - // it != current_cluster->first.end (); ++ it) - // { - // const Point& p = *it; - // Vector d = p - current_cluster->second; - // covariance[0] += d.x () * d.x (); - // covariance[1] += d.x () * d.y (); - // covariance[2] += d.x () * d.z (); - // covariance[3] += d.y () * d.y (); - // covariance[4] += d.y () * d.z (); - // covariance[5] += d.z () * d.z (); - // } + for (typename std::list::iterator it = current_cluster->first.begin (); + it != current_cluster->first.end (); ++ it) + { +#ifdef CGAL_USE_PROPERTY_MAPS_API_V1 + Point point = get(point_pmap, it); +#else + Point point = get(point_pmap, *it); +#endif + Vector d = point - current_cluster->second; + covariance[0] += d.x () * d.x (); + covariance[1] += d.x () * d.y (); + covariance[2] += d.x () * d.z (); + covariance[3] += d.y () * d.y (); + covariance[4] += d.y () * d.z (); + covariance[5] += d.z () * d.z (); + } cpp11::array eigenvalues = {{ 0., 0., 0. }}; cpp11::array eigenvectors = {{ 0., 0., 0., @@ -158,7 +230,7 @@ namespace CGAL { // Split the set if size OR variance of the cluster is too large if (current_cluster->first.size () > size || var > var_max) { - clusters_stack.push_front (cluster (std::list(), Point (0., 0., 0.))); + clusters_stack.push_front (cluster (std::list(), Point (0., 0., 0.))); cluster_iterator negative_side = clusters_stack.begin (); // positive_side is built directly from current_cluster @@ -168,10 +240,10 @@ namespace CGAL { Vector v (eigenvectors[6], eigenvectors[7], eigenvectors[8]); std::size_t current_cluster_size = 0; - typename std::list::iterator it = current_cluster->first.begin (); + typename std::list::iterator it = current_cluster->first.begin (); while (it != current_cluster->first.end ()) { - typename std::list::iterator current = it ++; + typename std::list::iterator current = it ++; // Test if point is on negative side of plane and // transfer it to the negative_side cluster if it is @@ -196,7 +268,8 @@ namespace CGAL { nonempty = current_cluster; } - nonempty->second = centroid (nonempty->first.begin (), nonempty->first.end ()); + nonempty->second = internal::hcs_centroid (nonempty->first.begin (), nonempty->first.end (), + point_pmap, Kernel()); clusters_stack.erase (empty); } @@ -206,7 +279,9 @@ namespace CGAL { Point old_centroid = current_cluster->second; // Compute the first centroid - current_cluster->second = centroid (current_cluster->first.begin (), current_cluster->first.end ()); + current_cluster->second = internal::hcs_centroid (current_cluster->first.begin (), + current_cluster->first.end (), + point_pmap, Kernel()); // The second centroid can be computed with the first and // the old ones : @@ -227,74 +302,79 @@ namespace CGAL { // and output point else { - *(out ++) = current_cluster->second; + internal::hsc_terminate_cluster (current_cluster->first, + points_to_keep, + points_to_remove, + point_pmap, + current_cluster->second, + Kernel ()); clusters_stack.pop_front (); } } + ForwardIterator first_point_to_remove = + std::copy (points_to_keep.begin(), points_to_keep.end(), begin); + std::copy (points_to_remove.begin(), points_to_remove.end(), first_point_to_remove); + + return first_point_to_remove; + } -/// @endcond + /// @endcond -/// @cond SKIP_IN_MANUAL + /// @cond SKIP_IN_MANUAL // This variant deduces the kernel from the iterator type. - template - void hierarchical_clustering (InputIterator begin, - InputIterator end, - PointPMap point_pmap, - OutputIterator out, - const unsigned int size, - const double var_max, - const DiagonalizeTraits& diagonalize_traits) + ForwardIterator hierarchical_clustering (ForwardIterator begin, + ForwardIterator end, + PointPMap point_pmap, + const unsigned int size, + const double var_max, + const DiagonalizeTraits& diagonalize_traits) { typedef typename boost::property_traits::value_type Point; typedef typename Kernel_traits::Kernel Kernel; - hierarchical_clustering (begin, end, point_pmap, out, size, var_max, - diagonalize_traits, Kernel()); + return hierarchical_clustering (begin, end, point_pmap, size, var_max, + diagonalize_traits, Kernel()); } -/// @endcond + /// @endcond -/// @cond SKIP_IN_MANUAL + /// @cond SKIP_IN_MANUAL // This variant uses default diagonalize traits - template - void hierarchical_clustering (InputIterator begin, - InputIterator end, - PointPMap point_pmap, - OutputIterator out, - const unsigned int size, - const double var_max) + template + ForwardIterator hierarchical_clustering (ForwardIterator begin, + ForwardIterator end, + PointPMap point_pmap, + const unsigned int size, + const double var_max) { typedef typename boost::property_traits::value_type Point; typedef typename Kernel_traits::Kernel Kernel; - hierarchical_clustering (begin, end, point_pmap, out, size, var_max, - Default_diagonalize_traits (), Kernel()); + return hierarchical_clustering (begin, end, point_pmap, size, var_max, + Default_diagonalize_traits (), Kernel()); } -/// @endcond + /// @endcond -/// @cond SKIP_IN_MANUAL + /// @cond SKIP_IN_MANUAL // This variant creates a default point property map = Identity_property_map. - template - void hierarchical_clustering (InputIterator begin, - InputIterator end, - OutputIterator out, - const unsigned int size = 10, - const double var_max = 0.333) + template + ForwardIterator hierarchical_clustering (ForwardIterator begin, + ForwardIterator end, + const unsigned int size = 10, + const double var_max = 0.333) { - hierarchical_clustering + return hierarchical_clustering (begin, end, #ifdef CGAL_USE_PROPERTY_MAPS_API_V1 make_dereference_property_map(first), #else - make_identity_property_map (typename std::iterator_traits::value_type()), + make_identity_property_map (typename std::iterator_traits::value_type()), #endif - out, size, var_max); + size, var_max); } -/// @endcond + /// @endcond } // namespace CGAL From e6054bfdeb219a21e8fb7d72dfb6a40161b880ac Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 15 Sep 2015 11:54:13 +0200 Subject: [PATCH 26/35] Rename function and files in accordance to content --- .../Point_set_processing_3/CMakeLists.txt | 2 +- ...p => hierarchy_simplification_example.cpp} | 14 ++-- ...ering.h => hierarchy_simplify_point_set.h} | 77 ++++++++++--------- .../Point_set_processing_3/CMakeLists.txt | 2 +- ....cpp => hierarchy_simplification_test.cpp} | 44 +++++------ 5 files changed, 68 insertions(+), 71 deletions(-) rename Point_set_processing_3/examples/Point_set_processing_3/{hierarchical_clustering_example.cpp => hierarchy_simplification_example.cpp} (75%) rename Point_set_processing_3/include/CGAL/{hierarchical_clustering.h => hierarchy_simplify_point_set.h} (85%) rename Point_set_processing_3/test/Point_set_processing_3/{hierarchical_clustering_test.cpp => hierarchy_simplification_test.cpp} (61%) diff --git a/Point_set_processing_3/examples/Point_set_processing_3/CMakeLists.txt b/Point_set_processing_3/examples/Point_set_processing_3/CMakeLists.txt index 307c15d4098..26b06b6de10 100644 --- a/Point_set_processing_3/examples/Point_set_processing_3/CMakeLists.txt +++ b/Point_set_processing_3/examples/Point_set_processing_3/CMakeLists.txt @@ -57,7 +57,7 @@ if ( CGAL_FOUND ) create_single_source_cgal_program( "bilateral_smooth_point_set_example.cpp" ) create_single_source_cgal_program( "grid_simplification_example.cpp" ) create_single_source_cgal_program( "grid_simplify_indices.cpp" ) - create_single_source_cgal_program( "hierarchical_clustering_example.cpp" ) + create_single_source_cgal_program( "hierarchy_simplification_example.cpp" ) create_single_source_cgal_program( "normals_example.cpp" ) create_single_source_cgal_program( "property_map.cpp" ) create_single_source_cgal_program( "random_simplification_example.cpp" ) diff --git a/Point_set_processing_3/examples/Point_set_processing_3/hierarchical_clustering_example.cpp b/Point_set_processing_3/examples/Point_set_processing_3/hierarchy_simplification_example.cpp similarity index 75% rename from Point_set_processing_3/examples/Point_set_processing_3/hierarchical_clustering_example.cpp rename to Point_set_processing_3/examples/Point_set_processing_3/hierarchy_simplification_example.cpp index 60616564a15..0fe5772d977 100644 --- a/Point_set_processing_3/examples/Point_set_processing_3/hierarchical_clustering_example.cpp +++ b/Point_set_processing_3/examples/Point_set_processing_3/hierarchy_simplification_example.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include @@ -28,19 +28,19 @@ int main(int argc, char*argv[]) std::cout << "Read " << points.size () << " point(s)" << std::endl; CGAL::Timer task_timer; task_timer.start(); - - std::vector output; // Algorithm generates a new set of points - CGAL::hierarchical_clustering (points.begin (), points.end (), - std::back_inserter (output), 100); + + // simplification by clustering using erase-remove idiom + points.erase (CGAL::hierarchy_simplify_point_set (points.begin (), points.end (), 100), + points.end ()); std::size_t memory = CGAL::Memory_sizer().virtual_size(); - std::cout << output.size () << " point(s) generated in " + std::cout << points.size () << " point(s) kept, computed in " << task_timer.time() << " seconds, " << (memory>>20) << " Mib allocated." << std::endl; std::ofstream f ("out.xyz"); - CGAL::write_xyz_points (f, output.begin (), output.end ()); + CGAL::write_xyz_points (f, points.begin (), points.end ()); return EXIT_SUCCESS; } diff --git a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h b/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h similarity index 85% rename from Point_set_processing_3/include/CGAL/hierarchical_clustering.h rename to Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h index c48ae4a06ab..8c4dc8a1a31 100644 --- a/Point_set_processing_3/include/CGAL/hierarchical_clustering.h +++ b/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h @@ -18,8 +18,8 @@ // Author(s) : Simon Giraudot, Pierre Alliez -#ifndef HIERARCHICAL_CLUSTERING_H -#define HIERARCHICAL_CLUSTERING_H +#ifndef HIERARCHY_SIMPLIFY_POINT_SET_H +#define HIERARCHY_SIMPLIFY_POINT_SET_H #include #include @@ -42,7 +42,7 @@ namespace CGAL { typename PointPMap, typename K > typename K::Point_3 - hcs_centroid(InputIterator begin, + hsps_centroid(InputIterator begin, InputIterator end, PointPMap point_pmap, const K&) @@ -116,10 +116,11 @@ namespace CGAL { /// Recursively split the point set in smaller clusters until the /// clusters have less than `size` elements or until their variation - /// factor is below `var_max`. + /// factor is below `var_max`. /// - /// This method does not change the input point set: the output is not - /// a subset of the input and is stored in a different container. + /// This method modifies the order of input points so as to pack all remaining points first, + /// and returns an iterator over the first point to remove (see erase-remove idiom). + /// For this reason it should not be called on sorted containers. /// /// \pre `0 < var_max < 1/3` /// \pre `size > 0` @@ -135,6 +136,8 @@ namespace CGAL { /// @tparam Kernel Geometric traits class. /// It can be omitted and deduced automatically from the value type of `PointPMap`. /// + /// @return iterator over the first point to remove. + // This variant requires all parameters. @@ -142,13 +145,13 @@ namespace CGAL { typename PointPMap, typename DiagonalizeTraits, typename Kernel> - ForwardIterator hierarchical_clustering (ForwardIterator begin, - ForwardIterator end, - PointPMap point_pmap, - const unsigned int size, - const double var_max, - const DiagonalizeTraits&, - const Kernel&) + ForwardIterator hierarchy_simplify_point_set (ForwardIterator begin, + ForwardIterator end, + PointPMap point_pmap, + const unsigned int size, + const double var_max, + const DiagonalizeTraits&, + const Kernel&) { typedef typename std::iterator_traits::value_type Input_type; typedef typename Kernel::FT FT; @@ -171,7 +174,7 @@ namespace CGAL { for(ForwardIterator it = begin; it != end; it++) clusters_stack.front ().first.push_back (*it); - clusters_stack.front ().second = internal::hcs_centroid (clusters_stack.front ().first.begin (), + clusters_stack.front ().second = internal::hsps_centroid (clusters_stack.front ().first.begin (), clusters_stack.front ().first.end (), point_pmap, Kernel()); @@ -268,7 +271,7 @@ namespace CGAL { nonempty = current_cluster; } - nonempty->second = internal::hcs_centroid (nonempty->first.begin (), nonempty->first.end (), + nonempty->second = internal::hsps_centroid (nonempty->first.begin (), nonempty->first.end (), point_pmap, Kernel()); clusters_stack.erase (empty); @@ -279,7 +282,7 @@ namespace CGAL { Point old_centroid = current_cluster->second; // Compute the first centroid - current_cluster->second = internal::hcs_centroid (current_cluster->first.begin (), + current_cluster->second = internal::hsps_centroid (current_cluster->first.begin (), current_cluster->first.end (), point_pmap, Kernel()); @@ -326,17 +329,17 @@ namespace CGAL { template - ForwardIterator hierarchical_clustering (ForwardIterator begin, - ForwardIterator end, - PointPMap point_pmap, - const unsigned int size, - const double var_max, - const DiagonalizeTraits& diagonalize_traits) + ForwardIterator hierarchy_simplify_point_set (ForwardIterator begin, + ForwardIterator end, + PointPMap point_pmap, + const unsigned int size, + const double var_max, + const DiagonalizeTraits& diagonalize_traits) { typedef typename boost::property_traits::value_type Point; typedef typename Kernel_traits::Kernel Kernel; - return hierarchical_clustering (begin, end, point_pmap, size, var_max, - diagonalize_traits, Kernel()); + return hierarchy_simplify_point_set (begin, end, point_pmap, size, var_max, + diagonalize_traits, Kernel()); } /// @endcond @@ -344,28 +347,28 @@ namespace CGAL { // This variant uses default diagonalize traits template - ForwardIterator hierarchical_clustering (ForwardIterator begin, - ForwardIterator end, - PointPMap point_pmap, - const unsigned int size, - const double var_max) + ForwardIterator hierarchy_simplify_point_set (ForwardIterator begin, + ForwardIterator end, + PointPMap point_pmap, + const unsigned int size, + const double var_max) { typedef typename boost::property_traits::value_type Point; typedef typename Kernel_traits::Kernel Kernel; - return hierarchical_clustering (begin, end, point_pmap, size, var_max, - Default_diagonalize_traits (), Kernel()); + return hierarchy_simplify_point_set (begin, end, point_pmap, size, var_max, + Default_diagonalize_traits (), Kernel()); } /// @endcond /// @cond SKIP_IN_MANUAL // This variant creates a default point property map = Identity_property_map. template - ForwardIterator hierarchical_clustering (ForwardIterator begin, - ForwardIterator end, - const unsigned int size = 10, - const double var_max = 0.333) + ForwardIterator hierarchy_simplify_point_set (ForwardIterator begin, + ForwardIterator end, + const unsigned int size = 10, + const double var_max = 0.333) { - return hierarchical_clustering + return hierarchy_simplify_point_set (begin, end, #ifdef CGAL_USE_PROPERTY_MAPS_API_V1 make_dereference_property_map(first), @@ -378,4 +381,4 @@ namespace CGAL { } // namespace CGAL -#endif // HIERARCHICAL_CLUSTERING_H +#endif // HIERARCHY_SIMPLIFY_POINT_SET_H diff --git a/Point_set_processing_3/test/Point_set_processing_3/CMakeLists.txt b/Point_set_processing_3/test/Point_set_processing_3/CMakeLists.txt index f055a789ff4..4b427e1d5e7 100644 --- a/Point_set_processing_3/test/Point_set_processing_3/CMakeLists.txt +++ b/Point_set_processing_3/test/Point_set_processing_3/CMakeLists.txt @@ -71,7 +71,7 @@ if ( CGAL_FOUND ) if(EIGEN3_FOUND OR LAPACK_FOUND) # Executables that require Eigen or BLAS and LAPACK create_single_source_cgal_program( "normal_estimation_test.cpp" ) - create_single_source_cgal_program( "hierarchical_clustering_test.cpp" ) + create_single_source_cgal_program( "hierarchy_simplification_test.cpp" ) create_single_source_cgal_program( "smoothing_test.cpp" ) create_single_source_cgal_program( "vcm_plane_test.cpp" ) create_single_source_cgal_program( "vcm_all_test.cpp" ) diff --git a/Point_set_processing_3/test/Point_set_processing_3/hierarchical_clustering_test.cpp b/Point_set_processing_3/test/Point_set_processing_3/hierarchy_simplification_test.cpp similarity index 61% rename from Point_set_processing_3/test/Point_set_processing_3/hierarchical_clustering_test.cpp rename to Point_set_processing_3/test/Point_set_processing_3/hierarchy_simplification_test.cpp index 4c4f3e54993..c996924fb3f 100644 --- a/Point_set_processing_3/test/Point_set_processing_3/hierarchical_clustering_test.cpp +++ b/Point_set_processing_3/test/Point_set_processing_3/hierarchy_simplification_test.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include #include #include #include @@ -18,32 +18,26 @@ typedef Kernel::FT FT; void test (std::vector& input, int result1 = 1, int result2 = 1, int result3 = 1, int result4 = 1) { - std::vector output; - - CGAL::hierarchical_clustering (input.begin (), input.end (), - std::back_inserter (output)); - if (result1 > 0 && output.size () != static_cast(result1)) + typename std::vector::iterator it = + CGAL::hierarchy_simplify_point_set (input.begin (), input.end ()); + if (result1 > 0 && std::distance (input.begin (), it) != (result1)) exit (EXIT_FAILURE); - output.clear (); - - CGAL::hierarchical_clustering (input.begin (), input.end (), - std::back_inserter (output), 100); - if (result2 > 0 && output.size () != static_cast(result2)) - exit (EXIT_FAILURE); - output.clear (); - - CGAL::hierarchical_clustering (input.begin (), input.end (), - std::back_inserter (output), 1000, 0.1); - if (result3 > 0 && output.size () != static_cast(result3)) - exit (EXIT_FAILURE); - output.clear (); - CGAL::hierarchical_clustering (input.begin (), input.end (), - CGAL::Identity_property_map(), - std::back_inserter (output), - std::numeric_limits::max(), - 0.0001); - if (result4 > 0 && output.size () != static_cast(result4)) + it = CGAL::hierarchy_simplify_point_set (input.begin (), input.end (), 100); + if (result2 > 0 && std::distance (input.begin (), it) != (result2)) + exit (EXIT_FAILURE); + + + it = CGAL::hierarchy_simplify_point_set (input.begin (), input.end (), 1000, 0.1); + if (result3 > 0 && std::distance (input.begin (), it) != (result3)) + exit (EXIT_FAILURE); + + + it = CGAL::hierarchy_simplify_point_set (input.begin (), input.end (), + CGAL::Identity_property_map(), + std::numeric_limits::max(), + 0.0001); + if (result4 > 0 && std::distance (input.begin (), it) != (result4)) exit (EXIT_FAILURE); input.clear (); From 70502ca75acb15e819a689c676113521d3504784 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 15 Sep 2015 12:22:04 +0200 Subject: [PATCH 27/35] Integrate hierarchy simplification in reworked simplification plugin --- Polyhedron/demo/Polyhedron/CMakeLists.txt | 4 - ...int_set_hierarchical_clustering_plugin.cpp | 114 ------------ ...oint_set_hierarchical_clustering_plugin.ui | 113 ------------ ...n_demo_point_set_simplification_plugin.cpp | 37 +++- ...on_demo_point_set_simplification_plugin.ui | 171 ++++++++++++------ .../demo/Polyhedron/cgal_test_with_cmake | 1 - 6 files changed, 142 insertions(+), 298 deletions(-) delete mode 100644 Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.cpp delete mode 100644 Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.ui diff --git a/Polyhedron/demo/Polyhedron/CMakeLists.txt b/Polyhedron/demo/Polyhedron/CMakeLists.txt index 267458ed9e7..2df990619b6 100644 --- a/Polyhedron/demo/Polyhedron/CMakeLists.txt +++ b/Polyhedron/demo/Polyhedron/CMakeLists.txt @@ -442,10 +442,6 @@ if(CGAL_Qt5_FOUND AND Qt5_FOUND AND OPENGL_FOUND AND QGLVIEWER_FOUND) polyhedron_demo_plugin(point_set_simplification_plugin Polyhedron_demo_point_set_simplification_plugin ${point_set_simplificationUI_FILES}) target_link_libraries(point_set_simplification_plugin scene_points_with_normal_item) - qt5_wrap_ui(point_set_hierarchical_clusteringUI_FILES Polyhedron_demo_point_set_hierarchical_clustering_plugin.ui) - polyhedron_demo_plugin(point_set_hierarchical_clustering_plugin Polyhedron_demo_point_set_hierarchical_clustering_plugin ${point_set_hierarchical_clusteringUI_FILES}) - target_link_libraries(point_set_hierarchical_clustering_plugin scene_points_with_normal_item) - qt5_wrap_ui( ps_outliers_removal_UI_FILES Polyhedron_demo_point_set_outliers_removal_plugin.ui) polyhedron_demo_plugin(point_set_selection_plugin Polyhedron_demo_point_set_selection_plugin ${point_set_selectionUI_FILES}) diff --git a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.cpp b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.cpp deleted file mode 100644 index 176402f3752..00000000000 --- a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.cpp +++ /dev/null @@ -1,114 +0,0 @@ -#include "config.h" -#include "Scene_points_with_normal_item.h" -#include "Polyhedron_demo_plugin_helper.h" -#include "Polyhedron_demo_plugin_interface.h" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "ui_Polyhedron_demo_point_set_hierarchical_clustering_plugin.h" - -class Polyhedron_demo_point_set_hierarchical_clustering_plugin : - public QObject, - public Polyhedron_demo_plugin_helper -{ - Q_OBJECT - Q_INTERFACES(Polyhedron_demo_plugin_interface) - Q_PLUGIN_METADATA(IID "com.geometryfactory.PolyhedronDemo.PluginInterface/1.0") - - QAction* actionHierarchicalCluster; - -public: - void init(QMainWindow* mainWindow, Scene_interface* scene_interface) { - actionHierarchicalCluster = new QAction(tr("Point set hierarchical clustering"), mainWindow); - actionHierarchicalCluster->setObjectName("actionHierarchicalCluster"); - - Polyhedron_demo_plugin_helper::init(mainWindow, scene_interface); - } - - bool applicable(QAction*) const { - return qobject_cast(scene->item(scene->mainSelectionIndex())); - } - - QList actions() const { - return QList() << actionHierarchicalCluster; - } - -public Q_SLOTS: - void on_actionHierarchicalCluster_triggered(); - -}; // end Polyhedron_demo_point_set_hierarchical_clustering_plugin - -class Point_set_demo_point_set_hierarchical_clustering_dialog : public QDialog, private Ui::PointSetHierarchicalClusteringDialog -{ - Q_OBJECT -public: - Point_set_demo_point_set_hierarchical_clustering_dialog(QWidget * /*parent*/ = 0) - { - setupUi(this); - } - - int size() const { return m_maximumClusterSize->value(); } - double var_max() const { return m_maximumSurfaceVariation->value(); } -}; - -void Polyhedron_demo_point_set_hierarchical_clustering_plugin::on_actionHierarchicalCluster_triggered() -{ - const Scene_interface::Item_id index = scene->mainSelectionIndex(); - - Scene_points_with_normal_item* item = - qobject_cast(scene->item(index)); - - if(item) - { - // Gets point set - Point_set* points = item->point_set(); - if(points == NULL) - return; - - // Gets options - Point_set_demo_point_set_hierarchical_clustering_dialog dialog; - if(!dialog.exec()) - return; - - QApplication::setOverrideCursor(Qt::WaitCursor); - - CGAL::Timer task_timer; task_timer.start(); - - std::cerr << "Hierarchical clustering (cluster size = " << dialog.size() - << ", maximum variation = " << dialog.var_max() << ")" << std::endl; - - - - Scene_points_with_normal_item* new_item = new Scene_points_with_normal_item(); - CGAL::hierarchical_clustering(points->begin(), points->end(), - std::back_inserter (*(new_item->point_set())), - dialog.size(), dialog.var_max()); - - new_item->setName(QString("%1 (hierarchical clustering)").arg(item->name())); - new_item->set_has_normals (false); - new_item->setColor(item->color()); - new_item->setRenderingMode(item->renderingMode()); - new_item->setVisible(item->visible()); - scene->addItem(new_item); - - std::size_t memory = CGAL::Memory_sizer().virtual_size(); - std::cerr << "Clustering: " << new_item->point_set()->size () << " point(s) generated (" - << task_timer.time() << " seconds, " - << (memory>>20) << " Mb allocated)" - << std::endl; - - QApplication::restoreOverrideCursor(); - - } -} - -#include "Polyhedron_demo_point_set_hierarchical_clustering_plugin.moc" diff --git a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.ui b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.ui deleted file mode 100644 index 3c43cd18c76..00000000000 --- a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_hierarchical_clustering_plugin.ui +++ /dev/null @@ -1,113 +0,0 @@ - - - PointSetHierarchicalClusteringDialog - - - - 0 - 0 - 403 - 137 - - - - Hierarchical Clustering - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - Maximum cluster size - - - - - - - - - - 5 - - - 0.000010000000000 - - - 0.333330000000000 - - - 0.012340000000000 - - - 0.333330000000000 - - - - - - - Maximum surface variation - - - - - - - 1 - - - 2147483647 - - - 10 - - - - - - - - - buttonBox - accepted() - PointSetHierarchicalClusteringDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - PointSetHierarchicalClusteringDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.cpp b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.cpp index b2eb7872fe0..dfb34b2bc02 100644 --- a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -66,9 +67,19 @@ class Point_set_demo_point_set_simplification_dialog : public QDialog, private U setupUi(this); } - QString simplificationMethod() const { return m_simplificationMethod->currentText(); } - double randomSimplificationPercentage() const { return m_randomSimplificationPercentage->value(); } - double gridCellSize() const { return m_gridCellSize->value(); } + unsigned int simplificationMethod() const + { + if (Random->isChecked()) + return 0; + else if (Grid->isChecked()) + return 1; + else + return 2; + } + double randomSimplificationPercentage() const { return m_randomSimplificationPercentage->value(); } + double gridCellSize() const { return m_gridCellSize->value(); } + unsigned int maximumClusterSize() const { return m_maximumClusterSize->value(); } + double maximumSurfaceVariation() const { return m_maximumSurfaceVariation->value(); } }; void Polyhedron_demo_point_set_simplification_plugin::on_actionSimplify_triggered() @@ -97,18 +108,19 @@ void Polyhedron_demo_point_set_simplification_plugin::on_actionSimplify_triggere // First point to delete Point_set::iterator first_point_to_remove = points->end(); - if (dialog.simplificationMethod() == "Random") + unsigned int method = dialog.simplificationMethod (); + if (method == 0) { - std::cerr << "Random point cloud simplification (" << dialog.randomSimplificationPercentage() <<"%)...\n"; + std::cerr << "Point set random simplification (" << dialog.randomSimplificationPercentage() <<"%)...\n"; // Computes points to remove by random simplification first_point_to_remove = CGAL::random_simplify_point_set(points->begin(), points->end(), dialog.randomSimplificationPercentage()); } - else if (dialog.simplificationMethod() == "Grid Clustering") + else if (method == 1) { - std::cerr << "Point cloud simplification by clustering (cell size = " << dialog.gridCellSize() <<" * average spacing)...\n"; + std::cerr << "Point set grid simplification (cell size = " << dialog.gridCellSize() <<" * average spacing)...\n"; // Computes average spacing double average_spacing = CGAL::compute_average_spacing( @@ -120,6 +132,17 @@ void Polyhedron_demo_point_set_simplification_plugin::on_actionSimplify_triggere CGAL::grid_simplify_point_set(points->begin(), points->end(), dialog.gridCellSize()*average_spacing); } + else + { + std::cerr << "Point set hierarchy simplification (cluster size = " << dialog.maximumClusterSize() + << ", maximum variation = " << dialog.maximumSurfaceVariation() << ")...\n"; + + // Computes points to remove by Grid Clustering + first_point_to_remove = + CGAL::hierarchy_simplify_point_set(points->begin(), points->end(), + dialog.maximumClusterSize(), + dialog.maximumSurfaceVariation()); + } std::size_t nb_points_to_remove = std::distance(first_point_to_remove, points->end()); std::size_t memory = CGAL::Memory_sizer().virtual_size(); diff --git a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.ui b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.ui index 7c45142f0eb..9664e834d2c 100644 --- a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.ui +++ b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.ui @@ -1,104 +1,157 @@ - + + PointSetSimplificationDialog - - + + 0 0 - 403 - 153 + 450 + 251 - + Simplification - - - - - Method: + + + + + Maximum cluster size - - - - - Random - - - - - Grid Clustering - - - - - - - - Points to Remove Randomly + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - + + + + Random + + + true + + + + + + + Grid + + + + + + % - + 2 - + 0.100000000000000 - + 100.000000000000000 - + 0.100000000000000 - + 50.000000000000000 - - - - Grid Cell Size + + + + Hierarchy - - - + + + * average spacing - + 2 - + 0.100000000000000 - + 10.000000000000000 - + 0.100000000000000 - + 1.000000000000000 - - - - Qt::Horizontal + + + + Points to Remove Randomly - - QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok + + + + + + Grid Cell Size + + + + + + + 1 + + + 2147483647 + + + 10 + + + + + + + Maximum surface variation + + + + + + + + + + 5 + + + 0.000010000000000 + + + 0.333330000000000 + + + 0.012340000000000 + + + 0.333330000000000 @@ -112,11 +165,11 @@ PointSetSimplificationDialog accept() - + 248 254 - + 157 274 @@ -128,11 +181,11 @@ PointSetSimplificationDialog reject() - + 316 260 - + 286 274 diff --git a/Polyhedron/demo/Polyhedron/cgal_test_with_cmake b/Polyhedron/demo/Polyhedron/cgal_test_with_cmake index 388d1588828..2f49750a143 100755 --- a/Polyhedron/demo/Polyhedron/cgal_test_with_cmake +++ b/Polyhedron/demo/Polyhedron/cgal_test_with_cmake @@ -130,7 +130,6 @@ else point_inside_polyhedron_plugin \ point_set_average_spacing_plugin \ point_set_bilateral_smoothing_plugin \ - point_set_hierarchical_clustering_plugin \ point_set_outliers_removal_plugin \ point_set_selection_plugin \ point_set_shape_detection_plugin \ From e028ff89558fc5e48d7c2767e04d31a512f4da62 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 15 Sep 2015 13:54:43 +0200 Subject: [PATCH 28/35] Update doc and example --- .../PackageDescription.txt | 2 +- .../Point_set_processing_3.txt | 80 +++++++++---------- .../doc/Point_set_processing_3/examples.txt | 2 +- .../hierarchy_simplification_example.cpp | 5 +- 4 files changed, 45 insertions(+), 44 deletions(-) diff --git a/Point_set_processing_3/doc/Point_set_processing_3/PackageDescription.txt b/Point_set_processing_3/doc/Point_set_processing_3/PackageDescription.txt index 80b5417f5cb..fc99524025a 100644 --- a/Point_set_processing_3/doc/Point_set_processing_3/PackageDescription.txt +++ b/Point_set_processing_3/doc/Point_set_processing_3/PackageDescription.txt @@ -30,8 +30,8 @@ - `CGAL::remove_outliers()` - `CGAL::grid_simplify_point_set()` - `CGAL::random_simplify_point_set()` +- `CGAL::hierarchy_simplify_point_set()` - `CGAL::wlop_simplify_and_regularize_point_set()` -- `CGAL::hierarchical_clustering()` - `CGAL::jet_smooth_point_set()` - `CGAL::bilateral_smooth_point_set()` - `CGAL::jet_estimate_normals()` diff --git a/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt b/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt index f9fb91427d6..d3a793a9689 100644 --- a/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt +++ b/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt @@ -150,17 +150,16 @@ points sharing the same cell of the grid by picking as representant one arbitrarily chosen point. This algorithm is slower than `random_simplify_point_set()`. +Function `hierarchy_simplify_point_set()` provides an adaptative +simplification of the point set through local +clusters\cgalCite{cgal:pgk-esops-02}. The size of the clusters is +either directly selected by the user or it automatically adapts to the +local variation of the point set . + Function `wlop_simplify_and_regularize_point_set()` not only simplifies, but also regularizes downsampled points. This is an implementation of the Weighted Locally Optimal Projection (WLOP) algorithm \cgalCite{wlop-2009}. -Function `hierarchical_clustering()` is not strictly speaking a -simplification algorithm as its output is not a subset of the input -point set. However, it provides an adaptative simplified -representation of the point set through local clusters: the size of -the clusters is either directly selected by the user or it -automatically adapts to the local variation of the point set -\cgalCite{cgal:pgk-esops-02}. \subsection Point_set_processing_3Example_3 Grid Simplification Example @@ -171,6 +170,40 @@ The following example reads a point set and simplifies it by clustering. Point set simplification through grid-based clustering. Removed points are depicted in red. Notice how low-density areas (in green) are not simplified. \cgalFigureEnd +\subsection Point_set_processing_3Example_9 Hierarchy Simplification Example +The following example reads a point set and produces a set of clusters. + +\cgalExample{Point_set_processing_3/hierarchy_simplification_example.cpp} + +\subsubsection Point_set_processing_3Hierarchy_simplification_parameter_size Parameter: size +The hierarchy simplification algorithm recursively split the point set +in two until each cluster's size is less than the parameter `size`. + +\cgalFigureBegin{Point_set_processing_3figHierarchy_simplification_size, hierarchical_clustering_size.jpg} +Input point set and hierarchy simplification with different `size` +parameter: \f$10\f$, \f$100\f$ and \f$1000\f$. In the 3 cases, +`var_max`\f$=1/3\f$. \cgalFigureEnd + + +\subsubsection Point_set_processing_3Hierarchy_simplification_parameter_var_max Parameter: var_max +In addition to the size parameter, a variation parameter allows to +increase simplification in monotoneous regions. For each cluster, a +surface variation measure is computed using the sorted eigenvalues of +the covariance matrix: \f[ \sigma(p) = \frac{\lambda_0}{\lambda_0 + + \lambda_1 + \lambda_2}. \f] + +This function goes from \f$0\f$ if the cluster is coplanar to +\f$1/3\f$ if it is fully isotropic. If a cluster's variation is above +`var_max`, it is splitted. If `var_max` is equal to \f$1/3\f$, this +parameter has no effect and the clustering is regular on the whole +point set. + +\cgalFigureBegin{Point_set_processing_3figHierarchical_clustering_var_max, hierarchical_clustering_var_max.jpg} +Input point set and hierarchy simplification with different `var_max` +parameter: \f$0.00001\f$, \f$0.001\f$ and \f$0.1\f$. In the 3 cases, +`size`\f$=1000\f$. \cgalFigureEnd + + \subsection Point_set_processing_3Example_4 WLOP Simplification Example The following example reads a point set, simplifies and regularizes it by WLOP. @@ -206,39 +239,6 @@ for more details. We provide below a speed-up chart generated using the parallel Parallel WLOP speed-up, compared to the sequential version of the algorithm. \cgalFigureEnd -\subsection Point_set_processing_3Example_9 Hierarchical Clustering Example -The following example reads a point set and produces a set of clusters. - -\cgalExample{Point_set_processing_3/hierarchical_clustering_example.cpp} - -\subsubsection Point_set_processing_3Hierarchical_clustering_parameter_size Parameter: size -The hierarchical clustering algorithm recursively split the point set -in two until each cluster's size is less than the parameter `size`. - -\cgalFigureBegin{Point_set_processing_3figHierarchical_clustering_size, hierarchical_clustering_size.jpg} -Input point set and hierarchical clustering with different `size` -parameter: \f$10\f$, \f$100\f$ and \f$1000\f$. In the 3 cases, `var_max`\f$=1/3\f$. -\cgalFigureEnd - - -\subsubsection Point_set_processing_3Hierarchical_clustering_parameter_var_max Parameter: var_max -In addition to the size parameter, a variation parameter allows to -increase simplification in monotoneous regions. For each cluster, a -surface variation measure is computed using the sorted eigenvalues of -the covariance matrix: \f[ \sigma(p) = \frac{\lambda_0}{\lambda_0 + - \lambda_1 + \lambda_2}. \f] - -This function goes from \f$0\f$ if the cluster is coplanar to -\f$1/3\f$ if it is fully isotropic. If a cluster's variation is above -`var_max`, it is splitted. If `var_max` is equal to \f$1/3\f$, this -parameter has no effect and the clustering is regular on the whole -point set. - -\cgalFigureBegin{Point_set_processing_3figHierarchical_clustering_var_max, hierarchical_clustering_var_max.jpg} -Input point set and hierarchical clustering with different `var_max` -parameter: \f$0.00001\f$, \f$0.001\f$ and \f$0.1\f$. In the 3 cases, `size`\f$=1000\f$. -\cgalFigureEnd - \section Point_set_processing_3Smoothing Smoothing diff --git a/Point_set_processing_3/doc/Point_set_processing_3/examples.txt b/Point_set_processing_3/doc/Point_set_processing_3/examples.txt index 73f842d423b..b407046f25b 100644 --- a/Point_set_processing_3/doc/Point_set_processing_3/examples.txt +++ b/Point_set_processing_3/doc/Point_set_processing_3/examples.txt @@ -4,7 +4,7 @@ \example Point_set_processing_3/remove_outliers_example.cpp \example Point_set_processing_3/grid_simplification_example.cpp \example Point_set_processing_3/grid_simplify_indices.cpp -\example Point_set_processing_3/hierarchical_clustering_example.cpp +\example Point_set_processing_3/hierarchy_simplification_example.cpp \example Point_set_processing_3/jet_smoothing_example.cpp \example Point_set_processing_3/normals_example.cpp \example Point_set_processing_3/wlop_simplify_and_regularize_point_set_example.cpp diff --git a/Point_set_processing_3/examples/Point_set_processing_3/hierarchy_simplification_example.cpp b/Point_set_processing_3/examples/Point_set_processing_3/hierarchy_simplification_example.cpp index 0fe5772d977..beb2bb77fa4 100644 --- a/Point_set_processing_3/examples/Point_set_processing_3/hierarchy_simplification_example.cpp +++ b/Point_set_processing_3/examples/Point_set_processing_3/hierarchy_simplification_example.cpp @@ -5,7 +5,6 @@ #include #include - #include #include @@ -30,7 +29,9 @@ int main(int argc, char*argv[]) CGAL::Timer task_timer; task_timer.start(); // simplification by clustering using erase-remove idiom - points.erase (CGAL::hierarchy_simplify_point_set (points.begin (), points.end (), 100), + points.erase (CGAL::hierarchy_simplify_point_set (points.begin (), points.end (), + 100, // Max cluster size + 0.01), // Max surface variation points.end ()); std::size_t memory = CGAL::Memory_sizer().virtual_size(); From a068249ac5076bb1468f9c21543b3dbb98765b00 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 15 Sep 2015 14:53:01 +0200 Subject: [PATCH 29/35] Minor code cleaning/rewriting --- .../CGAL/hierarchy_simplify_point_set.h | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h b/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h index 8c4dc8a1a31..3388b7c202f 100644 --- a/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h +++ b/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h @@ -44,29 +44,28 @@ namespace CGAL { typename K::Point_3 hsps_centroid(InputIterator begin, InputIterator end, - PointPMap point_pmap, + PointPMap& point_pmap, const K&) { - typedef typename K::Vector_3 Vector; typedef typename K::Point_3 Point; typedef typename K::FT FT; CGAL_precondition(begin != end); - Vector v = NULL_VECTOR; + FT x = (FT)0., y = (FT)0., z = (FT)0.; unsigned int nb_pts = 0; while(begin != end) { #ifdef CGAL_USE_PROPERTY_MAPS_API_V1 - Point point = get(point_pmap, begin); + const Point& point = get(point_pmap, begin); #else - Point point = get(point_pmap, *begin); + const Point& point = get(point_pmap, *begin); #endif - v = v + (point - ORIGIN); + x += point.x (); y += point.y (); z += point.z (); ++ nb_pts; ++ begin; } - return ORIGIN + v / (FT)nb_pts; + return Point (x/nb_pts, y/nb_pts, z/nb_pts); } template < typename Input_type, @@ -76,8 +75,8 @@ namespace CGAL { hsc_terminate_cluster (std::list& cluster, std::list& points_to_keep, std::list& points_to_remove, - PointPMap point_pmap, - typename K::Point_3 centroid, + PointPMap& point_pmap, + const typename K::Point_3& centroid, const K&) { typedef typename std::list::iterator Iterator; @@ -90,9 +89,9 @@ namespace CGAL { for (Iterator it = cluster.begin (); it != cluster.end (); ++ it) { #ifdef CGAL_USE_PROPERTY_MAPS_API_V1 - Point point = get(point_pmap, it); + const Point& point = get(point_pmap, it); #else - Point point = get(point_pmap, *it); + const Point& point = get(point_pmap, *it); #endif FT dist = CGAL::squared_distance (point, centroid); if (dist < dist_min) @@ -171,8 +170,7 @@ namespace CGAL { // The first cluster is the whole input point set clusters_stack.push_front (cluster (std::list(), Point (0., 0., 0.))); - for(ForwardIterator it = begin; it != end; it++) - clusters_stack.front ().first.push_back (*it); + std::copy (begin, end, std::back_inserter (clusters_stack.front ().first)); clusters_stack.front ().second = internal::hsps_centroid (clusters_stack.front ().first.begin (), clusters_stack.front ().first.end (), @@ -202,9 +200,9 @@ namespace CGAL { it != current_cluster->first.end (); ++ it) { #ifdef CGAL_USE_PROPERTY_MAPS_API_V1 - Point point = get(point_pmap, it); + const Point& point = get(point_pmap, it); #else - Point point = get(point_pmap, *it); + const Point& point = get(point_pmap, *it); #endif Vector d = point - current_cluster->second; covariance[0] += d.x () * d.x (); @@ -225,10 +223,7 @@ namespace CGAL { (covariance, eigenvalues, eigenvectors); // Variation of the set defined as lambda_min / (lambda_0 + lambda_1 + lambda_2) - double var = 0.; - for (int i = 0; i < 3; ++ i) - var += eigenvalues[i]; - var = eigenvalues[0] / var; + double var = eigenvalues[0] / (eigenvalues[0] + eigenvalues[1] + eigenvalues[2]); // Split the set if size OR variance of the cluster is too large if (current_cluster->first.size () > size || var > var_max) From e161dab5df136f67299b94cc1e702634196d28a3 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 15 Sep 2015 14:53:27 +0200 Subject: [PATCH 30/35] Improve GUI of point set simplification plugin in Polyhedron demo --- ...n_demo_point_set_simplification_plugin.cpp | 26 +++++++++++++++++++ ...on_demo_point_set_simplification_plugin.ui | 9 +++++++ 2 files changed, 35 insertions(+) diff --git a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.cpp b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.cpp index dfb34b2bc02..23167f95111 100644 --- a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.cpp @@ -56,6 +56,7 @@ public: public Q_SLOTS: void on_actionSimplify_triggered(); + }; // end Polyhedron_demo_point_set_simplification_plugin class Point_set_demo_point_set_simplification_dialog : public QDialog, private Ui::PointSetSimplificationDialog @@ -80,6 +81,31 @@ class Point_set_demo_point_set_simplification_dialog : public QDialog, private U double gridCellSize() const { return m_gridCellSize->value(); } unsigned int maximumClusterSize() const { return m_maximumClusterSize->value(); } double maximumSurfaceVariation() const { return m_maximumSurfaceVariation->value(); } + +public Q_SLOTS: + + void on_Random_toggled (bool toggled) + { + m_randomSimplificationPercentage->setEnabled (toggled); + m_gridCellSize->setEnabled (!toggled); + m_maximumClusterSize->setEnabled (!toggled); + m_maximumSurfaceVariation->setEnabled (!toggled); + } + void on_Grid_toggled (bool toggled) + { + m_randomSimplificationPercentage->setEnabled (!toggled); + m_gridCellSize->setEnabled (toggled); + m_maximumClusterSize->setEnabled (!toggled); + m_maximumSurfaceVariation->setEnabled (!toggled); + } + void on_Hierarchy_toggled (bool toggled) + { + m_randomSimplificationPercentage->setEnabled (!toggled); + m_gridCellSize->setEnabled (!toggled); + m_maximumClusterSize->setEnabled (toggled); + m_maximumSurfaceVariation->setEnabled (toggled); + } + }; void Polyhedron_demo_point_set_simplification_plugin::on_actionSimplify_triggered() diff --git a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.ui b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.ui index 9664e834d2c..8bf9d448c31 100644 --- a/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.ui +++ b/Polyhedron/demo/Polyhedron/Polyhedron_demo_point_set_simplification_plugin.ui @@ -79,6 +79,9 @@ + + false + * average spacing @@ -115,6 +118,9 @@ + + false + 1 @@ -135,6 +141,9 @@ + + false + From 564f156eeb71565984dfc6bef88ff55b250805d3 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 15 Sep 2015 16:15:35 +0200 Subject: [PATCH 31/35] Fix typos in doc --- .../Point_set_processing_3.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt b/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt index d3a793a9689..70762bab334 100644 --- a/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt +++ b/Point_set_processing_3/doc/Point_set_processing_3/Point_set_processing_3.txt @@ -138,7 +138,7 @@ functions in this component.) \section Point_set_processing_3Simplification Simplification -Three simplification functions are devised to reduce an input point set. +Four simplification functions are devised to reduce an input point set. Function `random_simplify_point_set()` randomly deletes a user-specified fraction of points from the input point set. This @@ -150,11 +150,11 @@ points sharing the same cell of the grid by picking as representant one arbitrarily chosen point. This algorithm is slower than `random_simplify_point_set()`. -Function `hierarchy_simplify_point_set()` provides an adaptative -simplification of the point set through local -clusters\cgalCite{cgal:pgk-esops-02}. The size of the clusters is -either directly selected by the user or it automatically adapts to the -local variation of the point set . +Function `hierarchy_simplify_point_set()` provides an adaptive +simplification of the point set through local clusters +\cgalCite{cgal:pgk-esops-02}. The size of the clusters is either +directly selected by the user or it automatically adapts to the local +variation of the point set. Function `wlop_simplify_and_regularize_point_set()` not only simplifies, but also regularizes downsampled points. This is an implementation of From b35e34238a20a953120d2db884096b133ebae4cb Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 15 Sep 2015 17:20:25 +0200 Subject: [PATCH 32/35] Bugfix: if 2 input points are equal, avoid infinite loop and terminate non-empty cluster --- .../CGAL/hierarchy_simplify_point_set.h | 37 ++++++++++--------- .../hierarchy_simplification_test.cpp | 20 ++++++---- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h b/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h index 3388b7c202f..ac6976aae45 100644 --- a/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h +++ b/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h @@ -251,25 +251,26 @@ namespace CGAL { ++ current_cluster_size; } - // If one of the clusters is empty, only keep the non-empty one + // If one of the clusters is empty, stop to avoid infinite + // loop and keep the non-empty one if (current_cluster->first.empty () || negative_side->first.empty ()) { - cluster_iterator empty, nonempty; - if (current_cluster->first.empty ()) - { - empty = current_cluster; - nonempty = negative_side; - } - else - { - empty = negative_side; - nonempty = current_cluster; - } + cluster_iterator nonempty = (current_cluster->first.empty () + ? negative_side : current_cluster); - nonempty->second = internal::hsps_centroid (nonempty->first.begin (), nonempty->first.end (), - point_pmap, Kernel()); - - clusters_stack.erase (empty); + // Compute the centroid + nonempty->second = internal::hsps_centroid (nonempty->first.begin (), + nonempty->first.end (), + point_pmap, Kernel()); + + internal::hsc_terminate_cluster (nonempty->first, + points_to_keep, + points_to_remove, + point_pmap, + nonempty->second, + Kernel ()); + clusters_stack.pop_front (); + clusters_stack.pop_front (); } else { @@ -278,8 +279,8 @@ namespace CGAL { // Compute the first centroid current_cluster->second = internal::hsps_centroid (current_cluster->first.begin (), - current_cluster->first.end (), - point_pmap, Kernel()); + current_cluster->first.end (), + point_pmap, Kernel()); // The second centroid can be computed with the first and // the old ones : diff --git a/Point_set_processing_3/test/Point_set_processing_3/hierarchy_simplification_test.cpp b/Point_set_processing_3/test/Point_set_processing_3/hierarchy_simplification_test.cpp index c996924fb3f..65cc416e5d9 100644 --- a/Point_set_processing_3/test/Point_set_processing_3/hierarchy_simplification_test.cpp +++ b/Point_set_processing_3/test/Point_set_processing_3/hierarchy_simplification_test.cpp @@ -16,10 +16,14 @@ typedef Kernel::Point_3 Point; typedef Kernel::FT FT; void test (std::vector& input, - int result1 = 1, int result2 = 1, int result3 = 1, int result4 = 1) + int result0 = 1, int result1 = 1, int result2 = 1, int result3 = 1, int result4 = 1) { typename std::vector::iterator it = - CGAL::hierarchy_simplify_point_set (input.begin (), input.end ()); + CGAL::hierarchy_simplify_point_set (input.begin (), input.end (), 1); + if (result0 > 0 && std::distance (input.begin (), it) != (result0)) + exit (EXIT_FAILURE); + + it = CGAL::hierarchy_simplify_point_set (input.begin (), input.end ()); if (result1 > 0 && std::distance (input.begin (), it) != (result1)) exit (EXIT_FAILURE); @@ -61,25 +65,25 @@ int main(void) // Test 2 points input.push_back (Point (0., 0., 0.)); input.push_back (Point (1., 0., 0.)); - test (input); + test (input, 2); // Test line for (std::size_t i = 0; i < 1000; ++ i) input.push_back (Point (0., 0., i)); - test (input, 128, 16, 1, 1); + test (input, input.size (), 128, 16, 1, 1); // Test plane for (std::size_t i = 0; i < 128; ++ i) for (std::size_t j = 0; j < 128; ++ j) input.push_back (Point (0., j, i)); - test (input, 2048, 256, 32, 1); + test (input, input.size (), 2048, 256, 32, 1); // Test random for (std::size_t i = 0; i < 10000; ++ i) input.push_back (Point (rand() / (FT)RAND_MAX, - rand() / (FT)RAND_MAX, - rand() / (FT)RAND_MAX)); - test (input, -1, -1, -1, -1); + rand() / (FT)RAND_MAX, + rand() / (FT)RAND_MAX)); + test (input, input.size (), -1, -1, -1, -1); return EXIT_SUCCESS; } From 0a0737a86342dcd1a0a4048e13aa71f118afb033 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Wed, 16 Sep 2015 17:32:53 +0200 Subject: [PATCH 33/35] Update changes.html --- Installation/changes.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Installation/changes.html b/Installation/changes.html index e2a4fca19e3..33ba6326ba2 100644 --- a/Installation/changes.html +++ b/Installation/changes.html @@ -154,6 +154,13 @@ and src/ directories). or CGAL::Parallel_tag when calling one of these functions.
  • CGAL::Parallel_tag can no longer be used in Point Set Processing algorithms if TBB is not available.
  • +
  • + Add a new simplification algorithm based on hierarchical + clustering: CGAL::hierarchy_simplify_point_set(). It + allows either to uniformly simplify the point set or to + automatically adapt the local density of points to the local + variation of the input computed by principal component analysis. +
  • Surface Mesh Parameterization

      From bc2a944deda45e359d9cb1de0e43428b83a33113 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 29 Sep 2015 10:42:15 +0200 Subject: [PATCH 34/35] Fix Visual Studio errors (typename + max macro bug) --- .../Point_set_processing_3/hierarchy_simplification_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Point_set_processing_3/test/Point_set_processing_3/hierarchy_simplification_test.cpp b/Point_set_processing_3/test/Point_set_processing_3/hierarchy_simplification_test.cpp index 65cc416e5d9..adf55e98e0b 100644 --- a/Point_set_processing_3/test/Point_set_processing_3/hierarchy_simplification_test.cpp +++ b/Point_set_processing_3/test/Point_set_processing_3/hierarchy_simplification_test.cpp @@ -18,7 +18,7 @@ typedef Kernel::FT FT; void test (std::vector& input, int result0 = 1, int result1 = 1, int result2 = 1, int result3 = 1, int result4 = 1) { - typename std::vector::iterator it = + std::vector::iterator it = CGAL::hierarchy_simplify_point_set (input.begin (), input.end (), 1); if (result0 > 0 && std::distance (input.begin (), it) != (result0)) exit (EXIT_FAILURE); @@ -39,7 +39,7 @@ void test (std::vector& input, it = CGAL::hierarchy_simplify_point_set (input.begin (), input.end (), CGAL::Identity_property_map(), - std::numeric_limits::max(), + (std::numeric_limits::max)(), 0.0001); if (result4 > 0 && std::distance (input.begin (), it) != (result4)) exit (EXIT_FAILURE); From 0916c32ded87602d698d41cc8d9ee4495269d4ac Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Thu, 8 Oct 2015 07:59:56 +0200 Subject: [PATCH 35/35] Fix numeric_limits::max bug (conflict with other max) --- .../include/CGAL/hierarchy_simplify_point_set.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h b/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h index ac6976aae45..0891eb1aeb2 100644 --- a/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h +++ b/Point_set_processing_3/include/CGAL/hierarchy_simplify_point_set.h @@ -83,7 +83,7 @@ namespace CGAL { typedef typename K::FT FT; typedef typename K::Point_3 Point; - FT dist_min = std::numeric_limits::max(); + FT dist_min = (std::numeric_limits::max)(); typename std::list::iterator point_min; for (Iterator it = cluster.begin (); it != cluster.end (); ++ it)