From 35d4c574bdbad3f95ff24917074374f9588e5cae Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Mon, 24 Feb 2020 13:16:32 +0100 Subject: [PATCH 01/79] Replace ETHZ Random forest IO by internal implementation --- .../random-forest/common-libraries.hpp | 37 +++++++++++++++++++ .../ETHZ/internal/random-forest/forest.hpp | 23 ++++++++++++ .../ETHZ/internal/random-forest/node.hpp | 37 +++++++++++++++++++ .../ETHZ/internal/random-forest/tree.hpp | 12 ++++++ 4 files changed, 109 insertions(+) diff --git a/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/common-libraries.hpp b/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/common-libraries.hpp index 44c2c20e3b3..b03c88f45e7 100644 --- a/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/common-libraries.hpp +++ b/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/common-libraries.hpp @@ -105,6 +105,30 @@ struct ForestParams { ar & BOOST_SERIALIZATION_NVP(min_samples_per_node); ar & BOOST_SERIALIZATION_NVP(sample_reduction); } + + void write (std::ostream& os) + { + os.write((char*)(&n_classes), sizeof(size_t)); + os.write((char*)(&n_features), sizeof(size_t)); + os.write((char*)(&n_samples), sizeof(size_t)); + os.write((char*)(&n_in_bag_samples), sizeof(size_t)); + os.write((char*)(&max_depth), sizeof(size_t)); + os.write((char*)(&n_trees), sizeof(size_t)); + os.write((char*)(&min_samples_per_node), sizeof(size_t)); + os.write((char*)(&sample_reduction), sizeof(float)); + } + + void read (std::istream& is) + { + is.read((char*)(&n_classes), sizeof(size_t)); + is.read((char*)(&n_features), sizeof(size_t)); + is.read((char*)(&n_samples), sizeof(size_t)); + is.read((char*)(&n_in_bag_samples), sizeof(size_t)); + is.read((char*)(&max_depth), sizeof(size_t)); + is.read((char*)(&n_trees), sizeof(size_t)); + is.read((char*)(&min_samples_per_node), sizeof(size_t)); + is.read((char*)(&sample_reduction), sizeof(float)); + } }; struct QuadraticSplitter { @@ -238,6 +262,19 @@ struct AxisAlignedSplitter { ar & BOOST_SERIALIZATION_NVP(feature); ar & BOOST_SERIALIZATION_NVP(threshold); } + + void write (std::ostream& os) + { + os.write((char*)(&feature), sizeof(int)); + os.write((char*)(&threshold), sizeof(FeatureType)); + } + + void read (std::istream& is) + { + is.read((char*)(&feature), sizeof(int)); + is.read((char*)(&threshold), sizeof(FeatureType)); + } + }; struct AxisAlignedRandomSplitGenerator { diff --git a/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/forest.hpp b/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/forest.hpp index 93af45cefd6..4f62e67a16b 100644 --- a/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/forest.hpp +++ b/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/forest.hpp @@ -230,6 +230,29 @@ public: ar & BOOST_SERIALIZATION_NVP(trees); } + void write (std::ostream& os) + { + params.write(os); + + std::size_t nb_trees = trees.size(); + os.write((char*)(&nb_trees), sizeof(std::size_t)); + for (std::size_t i_tree = 0; i_tree < trees.size(); ++i_tree) + trees[i_tree].write(os); + } + + void read (std::istream& is) + { + params.read(is); + + std::size_t nb_trees; + is.read((char*)(&nb_trees), sizeof(std::size_t)); + for (std::size_t i = 0; i < nb_trees; ++ i) + { + trees.push_back (new TreeType(¶ms)); + trees.back().read(is); + } + } + void get_feature_usage (std::vector& count) const { for (std::size_t i_tree = 0; i_tree < trees.size(); ++i_tree) diff --git a/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/node.hpp b/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/node.hpp index 1b2e3d8c7f4..ab664637d57 100644 --- a/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/node.hpp +++ b/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/node.hpp @@ -244,6 +244,43 @@ public: } } + void write (std::ostream& os) + { + os.write((char*)(&is_leaf), sizeof(bool)); + os.write((char*)(&n_samples), sizeof(size_t)); + os.write((char*)(&depth), sizeof(size_t)); + splitter.write(os); + + for (const float& f : node_dist) + os.write((char*)(&f), sizeof(float)); + + if (!is_leaf) + { + left->write(os); + right->write(os); + } + } + + void read (std::istream& is) + { + is.read((char*)(&is_leaf), sizeof(bool)); + is.read((char*)(&n_samples), sizeof(size_t)); + is.read((char*)(&depth), sizeof(size_t)); + splitter.read(is); + + node_dist.resize(params->n_classes, 0.0f); + for (std::size_t i = 0; i < node_dist.size(); ++ i) + is.read((char*)(&node_dist[i]), sizeof(float)); + + if (!is_leaf) + { + left.reset(new Derived(depth + 1, params)); + right.reset(new Derived(depth + 1, params)); + left->read(is); + right->read(is); + } + } + void get_feature_usage (std::vector& count) const { if (!is_leaf) diff --git a/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/tree.hpp b/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/tree.hpp index 0b117ee1b1a..9bf69b444e9 100644 --- a/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/tree.hpp +++ b/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/tree.hpp @@ -121,6 +121,18 @@ public: ar & BOOST_SERIALIZATION_NVP(params); ar & BOOST_SERIALIZATION_NVP(root_node); } + + void write (std::ostream& os) + { + root_node->write(os); + } + + void read (std::istream& is) + { + root_node.reset(new NodeT(0, params)); + root_node->read(is); + } + void get_feature_usage (std::vector& count) const { root_node->get_feature_usage(count); From 793ac58b91fc86a67217b80ab86a0c91e1ea5058 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Mon, 24 Feb 2020 16:38:22 +0100 Subject: [PATCH 02/79] Conversion of deprecated config files --- .../examples/Classification/CMakeLists.txt | 3 +- .../data/b9_clusters_config.bin | Bin 0 -> 6173 bytes .../Classification/data/b9_clusters_config.gz | Bin 2086 -> 0 bytes .../Classification/data/b9_mesh_config.bin | Bin 0 -> 115397 bytes .../Classification/data/b9_mesh_config.gz | Bin 46396 -> 0 bytes .../example_cluster_classification.cpp | 2 +- .../example_deprecated_conversion.cpp | 21 +++++++ .../example_ethz_random_forest.cpp | 35 +++++++++++ .../example_mesh_classification.cpp | 2 +- .../ETHZ/Random_forest_classifier.h | 59 ++++++++++++++---- .../Classification/Classification_plugin.cpp | 6 +- .../Classification/Item_classification_base.h | 7 ++- 12 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 Classification/examples/Classification/data/b9_clusters_config.bin delete mode 100644 Classification/examples/Classification/data/b9_clusters_config.gz create mode 100644 Classification/examples/Classification/data/b9_mesh_config.bin delete mode 100644 Classification/examples/Classification/data/b9_mesh_config.gz create mode 100644 Classification/examples/Classification/example_deprecated_conversion.cpp diff --git a/Classification/examples/Classification/CMakeLists.txt b/Classification/examples/Classification/CMakeLists.txt index 79ad8c5bf03..daa3999a83b 100644 --- a/Classification/examples/Classification/CMakeLists.txt +++ b/Classification/examples/Classification/CMakeLists.txt @@ -57,7 +57,8 @@ set(targets example_feature example_generation_and_training example_mesh_classification - example_cluster_classification) + example_cluster_classification + example_deprecated_conversion) # Classification requires some C++11 features set(needed_cxx_features cxx_rvalue_references cxx_variadic_templates) diff --git a/Classification/examples/Classification/data/b9_clusters_config.bin b/Classification/examples/Classification/data/b9_clusters_config.bin new file mode 100644 index 0000000000000000000000000000000000000000..67ba5fe90a0be1096f56d6f43e262a622b644e50 GIT binary patch literal 6173 zcmcgwYit!o6rL57N4d(=h5`j`3xpJkp#pvE%tfP8tcV~e5`{+7C;~xaglLHUfhb8^ z5^O*q;u}pdSl&VcA)@dD{9}I+&=L$$BpNZsnh=BMn>%Oc-kD`@LxPiRXS@5|bIyF{ z%sFSam&lAq%sk4OhnoL~oB38VW8!Cmf5h)b(#SpXd*BLHddr{&NgUs8&%p+(% zKBO?ce_y5i=J@^U${#DkpqF`hi9nb*!|GB0<8gTZ&a#dEhF$_`$ z#1gF}yvRrk*p_%u1{gp*27_}Y;SEW1DdEdyRU&vEK{WVjd% zxinZ^NcRgSek`k6pT(^hdGaL{MS-s_izg0W9|SoV1a>KyMUUH#rE5#dS4mvIztCHW zVH$01(i>q{Lz==?;?;_?Q&r$bfu$hJCXS~>oBBHHpySf{BUnD&F3~a7GYa|`N%888 zT`mO(zQqkELTrSR@@`49j5a5`#jXx2jHH1^9fOcn%05|@79vfCH_-ypb|d&US)K&v z0tO+Re;m_aFa6B=_rh;yFSAkL?-tmRkrw5Gc#r|Q6u<*l-`Ieqg9eI(ZW)CyuP&DY zBZ;#wgYdX=|LTyEg-#e`mlW(^ULDkLDD6lKaZJaP5MwM)zTuXqfRnoqf?|v&6;Z(I zat$}qTng&xxBqt4E6N43yYg#Ifp8r>l|V;doknhV)(1eqJWw9L75+G3OJq^oJ#?KB+;dyh#sf@95F+-OqQp@7P?Y?MJOlG zo0os6-r3$R*T3CY(Io@d!MW;;@$TllizJcW!$|Z_a{+Ggagke|1@I;Zo*`bdnBdOA@$dg#Dq{w%PL6_W# z8EGyBNIkF5TBcJE8c_tE*B6u$=(nwXs;MuKtv^jyx<7EU%2)zDxOjGTPF7*ZCK}G+ z>w~f=;H=^^PI|cjaXMtRb|V0UK@wdGuvrUA2T(IQk0PN%_v3jAkg>}xK*c2KpU>KA zw{F>jn&(v9K4<%>+}87!j?!>54IdQffQuEz&ip1hzNN52kN53GWa z|Dl?rt=(O;o!Z(KYHN!Xx_g0#80?l3y5G}ykKP1F*1fEzwmlI>6vR6iKPL*3x4EGK zrwa^L2R+66uIPgVo+CmNx@b#gp4tLg1a{*ITdH{R1GJ@DLXm`^P4LI0<{edM z){l~ODar*ha6=_Ge2rZk1o+t6&_Du-o*k3iHYC?3$cdo< z72)Tu(fWLch6bH|)T=NBxsuH4*ue}cBT23IO{p;}6Z7ZKS3yrY`)GB{lT~L_!FJbo zFuZ0u4yIYTz>Oy>2_lR%H^wXJ)r%|p%kf`+sLRH(@fCh9c$h#-S`NvZlMAA<;Zp#^ zF=BiNN}jrUFpZsyYJd<)sbe$9@n( zZTX-;=r#oQ;DeC&HDok_zAw2HVwXS}c1_ejC6$dR`}!cr!63JAd5vFPhK4+hXC!(1 z4%@!{a=k7aMGaMY{{`-+fLPyg`_H~Z_^M3{W{|tsW%p*byNo`Zx@>DCsskkbzAug>r!} z4d7E?dGaWrXcI;6j?SS#xaknJrTgxb{*H;x5T7X`o_uwLhI298zMz7htsC}MO`tc>a%FCZ>_qjQT7va3r-_SzcyLtr-wrqkn(n)^}q?F_LIaDdMv$*6RB!a4Xr v$FG_nbkVw9V}pI3tl`nojX~I)U2_iW0)%`?mkK!OSYj+syp`fX8I}J5QdoPU literal 0 HcmV?d00001 diff --git a/Classification/examples/Classification/data/b9_clusters_config.gz b/Classification/examples/Classification/data/b9_clusters_config.gz deleted file mode 100644 index 8d1303c1ad05d1d20e1ced3732ad6801e6ca9810..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2086 zcmV+>2-)`^iwFP!00000|BYGgj^!o{{ePb#FVF~MV_@HARjt&Ks#e-o{n4kdKY+m8 zkZEQ$%e@H%j_u=PPmIT3KYo6E|NZ0N?|*;%`Ny|!-+%u4+s8jY4hV<-z!6^Iz$51) zR{Ix@n2vP#Z5g6H1Nz&3l6{W-f$EF+hjKtX8FHyv=E+wl1SBeQBb@qwPKD=Tdg)`3 z4_`mZE>7f-3dFuZX38PSYtCyV50r=rWj@J7JfcniMStPgP+f5He@5=gI<6J?O+-%|O0c2Sxm7g@{vv9Lct6v!T5zflh+J zMSgI2Kmq?z0DV+tYSh=p_cQqUBRrijD#}pWYZ+{RMSM4M-+JR#Q{qX3pu3NEGJtI;R!C(?{Cnu(F?Jmg*2mu_EPbX zM4`OLVq-1^tHGvb!{mhV=F{yBY7h7O!)OxE!WAoaEWr+R2DZ}~1z~<-Hq_vNe%tsf zVNFZ{hGU1!HDV~na;TrwK3MPB%Xdn_>FYM`@kP*|i!)}$+#$q_^3^+J9j6IBSphAC zZdpL?noYtn@{V951TA*y4h+ed4L*^aLPyE=P+_H0BeUI}I&N@P*TLUs&2Ak}wx@+6sLiRdjr zy0w#j=)uVLLW;<-uUJCIxaE(e1VlTmQhG{^i8nFgu9Z6Pr-k>0%IcYklaS<)1kngV zwv;U&X7zrjF46^TZcmJ4>?fI3K!wz01^VT2V;7qd+P9(r^z~&29Ge;2T|~EE5mnNW z8-zGBrObi*_h@(viX=Q`YMzAG1H zo7*Kdn2ks*8#8zBkCjvoxcbPhrn73-aA(ARf@16SoMT$OW(m!q9 z(ermVFgwX2xro&yJ?@CLY_hUrP{R}LR`9slgjH=hU{{fJ8B`Y;{(nwgzh&Q{vjevJy1o`e%ZXI#g zne3+0NY_X{wVA-V0p*F(g`1TlS-9TwrbvE-l&ej4#@tAE2zf(reg7s|yZK!~XGz`} zhk#jQL+8}vrV1ODflZQb*g?-2087d+auC~9;nh3vCRs5Tlg9WXKc(z`fJ&rv>0YV8 ziNO+%j}=GvhiDFADST6v=xsGoub;dzAY3gb=`=P;7tw!unf|f4w^CMC@T)GfZ9=}C z&`s)Gc#|kQz@wcJ7lCUznIk1}?;F`M3EZjR1TQvkh^oJ1DXY=(mzpER8b-TlAfle+ zXSJhvKHnCe5FZ*za?(*+!&+PWNdsf#p+3H(NlC@JnXo@;nZq=>2uL6*Zj>+*l%;;6P^Ew`;a%OX&5G)Jah z&ZKZgQ0fx5dFI;7l4$1kN4@BJDjI2G94oswl%ltv3qHUR5wt{vs{}DDtCD_ql!>O({tVtsrXstHn_*#pR z6hm)vb*^itV4yso!DWFU!ICgWL=C-q9b)MMtH`@p(^^?V*9EU_3@%t$LT9SQwj~*) z=cq`q0jj~1;>Lgt$TY$tt?6BRL{a2X8N5GdsO&wv*@tuqwMyI*sI?nAf2yg(TO?mzh4WGsTGkO&CbVBi zSLCbe2E6>k diff --git a/Classification/examples/Classification/data/b9_mesh_config.bin b/Classification/examples/Classification/data/b9_mesh_config.bin new file mode 100644 index 0000000000000000000000000000000000000000..68dc1359478c9dce582f94a69d57440ade97a7b8 GIT binary patch literal 115397 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 gANzNi_bo Clu int main (int argc, char** argv) { std::string filename = "data/b9.ply"; - std::string filename_config = "data/b9_clusters_config.gz"; + std::string filename_config = "data/b9_clusters_config.bin"; if (argc > 1) filename = argv[1]; diff --git a/Classification/examples/Classification/example_deprecated_conversion.cpp b/Classification/examples/Classification/example_deprecated_conversion.cpp new file mode 100644 index 00000000000..1e355a23d0c --- /dev/null +++ b/Classification/examples/Classification/example_deprecated_conversion.cpp @@ -0,0 +1,21 @@ +#include + +#include +#include + +int main (int argc, char** argv) +{ + if (argc != 3) + std::cerr << "Usage: " << argv[0] << " input.gz output.bin" << std::endl; + else + { + std::ifstream ifile (argv[1], std::ios_base::binary); + std::ofstream ofile (argv[2], std::ios_base::binary); + + CGAL::Classification::ETHZ::Random_forest_classifier:: + convert_deprecated_configuration_to_new_format(ifile, ofile); + } + + return EXIT_SUCCESS; +} + diff --git a/Classification/examples/Classification/example_ethz_random_forest.cpp b/Classification/examples/Classification/example_ethz_random_forest.cpp index f4cca8960b8..18403f25452 100644 --- a/Classification/examples/Classification/example_ethz_random_forest.cpp +++ b/Classification/examples/Classification/example_ethz_random_forest.cpp @@ -126,6 +126,41 @@ int main (int argc, char** argv) << "Mean F1 score = " << evaluation.mean_f1_score() << std::endl << "Mean IoU = " << evaluation.mean_intersection_over_union() << std::endl; + { + std::ofstream out ("toto.bin", std::ios_base::binary); + classifier.save_configuration(out); + + Classification::ETHZ::Random_forest_classifier classifier_2 (labels, features); + std::ifstream in ("toto.bin", std::ios_base::binary); + classifier_2.load_configuration(in); + + t.reset(); + t.start(); + Classification::classify_with_graphcut + (pts, pts.point_map(), labels, classifier_2, + generator.neighborhood().k_neighbor_query(12), + 0.2f, 1, label_indices); + t.stop(); + + std::cerr << "Classification with graphcut done in " << t.time() << " second(s)" << std::endl; + + std::cerr << "Precision, recall, F1 scores and IoU:" << std::endl; + Classification::Evaluation evaluation (labels, ground_truth, label_indices); + + for (std::size_t i = 0; i < labels.size(); ++ i) + { + std::cerr << " * " << labels[i]->name() << ": " + << evaluation.precision(labels[i]) << " ; " + << evaluation.recall(labels[i]) << " ; " + << evaluation.f1_score(labels[i]) << " ; " + << evaluation.intersection_over_union(labels[i]) << std::endl; + } + + std::cerr << "Accuracy = " << evaluation.accuracy() << std::endl + << "Mean F1 score = " << evaluation.mean_f1_score() << std::endl + << "Mean IoU = " << evaluation.mean_intersection_over_union() << std::endl; + } + // Color point set according to class UCmap red = pts.add_property_map("red", 0).first; UCmap green = pts.add_property_map("green", 0).first; diff --git a/Classification/examples/Classification/example_mesh_classification.cpp b/Classification/examples/Classification/example_mesh_classification.cpp index f6ac64f60c2..c569ad0f03c 100644 --- a/Classification/examples/Classification/example_mesh_classification.cpp +++ b/Classification/examples/Classification/example_mesh_classification.cpp @@ -32,7 +32,7 @@ typedef Classification::Mesh_feature_generator int main (int argc, char** argv) { std::string filename = "data/b9_mesh.off"; - std::string filename_config = "data/b9_mesh_config.gz"; + std::string filename_config = "data/b9_mesh_config.bin"; if (argc > 1) filename = argv[1]; diff --git a/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h b/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h index 62b6dc57d26..a1211943214 100644 --- a/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h +++ b/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h @@ -277,26 +277,28 @@ public: This allows to easily save and recover a specific classification configuration. - The output file is written in an GZIP container that is readable - by the `load_configuration()` method. + The output file is written in a binary format that is readable by + the `load_configuration()` method. */ void save_configuration (std::ostream& output) const { - boost::iostreams::filtering_ostream outs; - outs.push(boost::iostreams::gzip_compressor()); - outs.push(output); - boost::archive::text_oarchive oas(outs); - oas << BOOST_SERIALIZATION_NVP(*m_rfc); + m_rfc->write(output); } - + /*! \brief Loads a configuration from the stream `input`. - The input file should be a GZIP container written by the + The input file should be a binary file written by the `save_configuration()` method. The feature set of the classifier should contain the exact same features in the exact same order as the ones present when the file was generated using `save_configuration()`. + + \warning If the file you are trying to load was saved using CGAL + 5.1 or earlier, you have to convert it first using + `convert_deprecated_configuration_to_new_format()` as the exchange + format for ETHZ Random Forest changed in CGAL 5.2. + */ void load_configuration (std::istream& input) { @@ -304,15 +306,50 @@ public: if (m_rfc != nullptr) delete m_rfc; m_rfc = new Forest (params); - + + m_rfc->read(input); + } + + /// @} + + /// \name Deprecated Input/Output + /// @{ + + /*! + \brief Converts a deprecated GZ configuration to a new BIN + configuration. + + The input file should be a GZIP container written by the + `save_configuration()` method from CGAL 5.1 and earlier. The + output is a valid configuration for CGAL 5.2 and later. + */ + static void convert_deprecated_configuration_to_new_format (std::istream& input, std::ostream& output) + { + Label_set dummy_labels; + Feature_set dummy_features; + Random_forest_classifier classifier (dummy_labels, dummy_features); + classifier.load_deprecated_configuration(input); + classifier.save_configuration(output); + } + +/// @} + + /// \cond SKIP_IN_MANUAL + void load_deprecated_configuration (std::istream& input) + { + CGAL::internal::liblearning::RandomForest::ForestParams params; + if (m_rfc != nullptr) + delete m_rfc; + m_rfc = new Forest (params); + boost::iostreams::filtering_istream ins; ins.push(boost::iostreams::gzip_decompressor()); ins.push(input); boost::archive::text_iarchive ias(ins); ias >> BOOST_SERIALIZATION_NVP(*m_rfc); } + /// \endcond -/// @} }; diff --git a/Polyhedron/demo/Polyhedron/Plugins/Classification/Classification_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/Classification/Classification_plugin.cpp index 7ae9229ba59..b9bf0568ab3 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Classification/Classification_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/Classification/Classification_plugin.cpp @@ -658,8 +658,8 @@ public Q_SLOTS: else if (classifier == 1) // Random Forest (ETHZ) filename = QFileDialog::getSaveFileName(mw, tr("Save classification configuration"), - tr("%1 (ETHZ random forest config).gz").arg(classif->item()->name()), - "Compressed ETHZ random forest configuration (*.gz);;"); + tr("%1 (ETHZ random forest config).bin").arg(classif->item()->name()), + "ETHZ random forest configuration (*.bin);;"); #ifdef CGAL_LINKED_WITH_OPENCV else if (classifier == 2) // Random Forest (OpenCV) filename = QFileDialog::getSaveFileName(mw, @@ -713,7 +713,7 @@ public Q_SLOTS: filename = QFileDialog::getOpenFileName(mw, tr("Open ETHZ random forest configuration"), ".", - "Compressed ETHZ random forest configuration (*.gz);;All Files (*)"); + "ETHZ random forest configuration (*.bin);Deprecated compressed ETHZ random forest configuration (*.gz);All Files (*)"); #ifdef CGAL_LINKED_WITH_OPENCV else if (classifier == 2) // OpenCV filename = QFileDialog::getOpenFileName(mw, diff --git a/Polyhedron/demo/Polyhedron/Plugins/Classification/Item_classification_base.h b/Polyhedron/demo/Polyhedron/Plugins/Classification/Item_classification_base.h index 6aa645c828a..f3732a96382 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Classification/Item_classification_base.h +++ b/Polyhedron/demo/Polyhedron/Plugins/Classification/Item_classification_base.h @@ -214,7 +214,12 @@ public: if (m_ethz == NULL) m_ethz = new ETHZ_random_forest (m_labels, m_features); std::ifstream f (filename, std::ios_base::in | std::ios_base::binary); - m_ethz->load_configuration (f); + + // Handle deprecated files + if (std::string(filename).find(".gz") != std::string::npos) + m_ethz->load_deprecated_configuration(f); + else + m_ethz->load_configuration (f); } else if (classifier == 2) { From aa08a608724e2a49587cd71043e4db9a335c1eea Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Mon, 18 Feb 2019 12:53:43 +0100 Subject: [PATCH 03/79] Much better version of evaluation --- .../include/CGAL/Classification/Evaluation.h | 344 ++++++++++++++---- 1 file changed, 273 insertions(+), 71 deletions(-) diff --git a/Classification/include/CGAL/Classification/Evaluation.h b/Classification/include/CGAL/Classification/Evaluation.h index c00d128259f..8600ec5ae97 100644 --- a/Classification/include/CGAL/Classification/Evaluation.h +++ b/Classification/include/CGAL/Classification/Evaluation.h @@ -23,6 +23,7 @@ namespace CGAL { namespace Classification { + /*! \ingroup PkgClassificationDataStructures @@ -31,20 +32,20 @@ namespace Classification { */ class Evaluation { + const Label_set& m_labels; mutable std::map m_map_labels; - - std::vector m_precision; - std::vector m_recall; - std::vector m_iou; // intersection over union - float m_accuracy; - float m_mean_iou; - float m_mean_f1; + std::vector > m_confusion; // confusion matrix public: /// \name Constructor /// @{ + Evaluation (const Label_set& labels) + : m_labels (labels) + { + init(); + } /*! @@ -66,68 +67,53 @@ public: Evaluation (const Label_set& labels, const GroundTruthIndexRange& ground_truth, const ResultIndexRange& result) - : m_precision (labels.size()), - m_recall (labels.size()), - m_iou (labels.size()) + : m_labels (labels) { - for (std::size_t i = 0; i < labels.size(); ++ i) - m_map_labels[labels[i]] = i; - - std::vector true_positives (labels.size()); - std::vector false_positives (labels.size()); - std::vector false_negatives (labels.size()); - - std::size_t sum_true_positives = 0; - std::size_t total = 0; - - for (std::size_t j = 0; j < ground_truth.size(); ++ j) - { - int gt = static_cast(ground_truth[j]); - int res = static_cast(result[j]); - if (gt == -1 || res == -1) - continue; - ++ total; - if (gt == res) - { - ++ true_positives[gt]; - ++ sum_true_positives; - continue; - } - ++ false_positives[res]; - ++ false_negatives[gt]; - } - - m_mean_iou = 0.; - m_mean_f1 = 0.; - - std::size_t correct_labels = 0; - - for (std::size_t j = 0; j < labels.size(); ++ j) - { - m_precision[j] = true_positives[j] / float(true_positives[j] + false_positives[j]); - m_recall[j] = true_positives[j] / float(true_positives[j] + false_negatives[j]); - m_iou[j] = true_positives[j] / float(true_positives[j] + false_positives[j] + false_negatives[j]); - - if (std::isnan(m_iou[j])) - continue; - - ++ correct_labels; - m_mean_iou += m_iou[j]; - m_mean_f1 += 2.f * (m_precision[j] * m_recall[j]) - / (m_precision[j] + m_recall[j]); - } - - m_mean_iou /= correct_labels; - m_mean_f1 /= correct_labels; - m_accuracy = sum_true_positives / float(total); + init(); + append(ground_truth, result); } /// @} + /// \cond SKIP_IN_MANUAL + void init() + { + for (std::size_t i = 0; i < m_labels.size(); ++ i) + m_map_labels[m_labels[i]] = i; + + m_confusion.resize (m_labels.size()); + for (std::size_t i = 0; i < m_confusion.size(); ++ i) + m_confusion[i].resize (m_labels.size(), 0); + } + + bool label_has_ground_truth (std::size_t label_idx) const + { + for (std::size_t i = 0; i < m_labels.size(); ++ i) + if (m_confusion[i][label_idx] != 0) + return true; + return false; + } + /// \endcond + + + template + void append (const GroundTruthIndexRange& ground_truth, + const ResultIndexRange& result) + { + for (std::size_t i = 0; i < ground_truth.size(); ++ i) + { + int gt = static_cast(ground_truth[i]); + int res = static_cast(result[i]); + if (gt == -1 || res == -1) + continue; + + ++ m_confusion[std::size_t(res)][std::size_t(gt)]; + } + } + /// \name Label Evaluation /// @{ - /*! \brief Returns the precision of the training for the given label. @@ -138,7 +124,18 @@ public: */ float precision (Label_handle label) const { - return m_precision[m_map_labels[label]]; + std::size_t idx = m_map_labels[label]; + if (!label_has_ground_truth(idx)) + return std::numeric_limits::quiet_NaN(); + + std::size_t total = 0; + for (std::size_t i = 0; i < m_labels.size(); ++ i) + total += m_confusion[idx][i]; + + if (total == 0) + return 0.f; + + return m_confusion[idx][idx] / float(total); } /*! @@ -151,7 +148,14 @@ public: */ float recall (Label_handle label) const { - return m_recall[m_map_labels[label]]; + std::size_t idx = m_map_labels[label]; + if (!label_has_ground_truth(idx)) + return std::numeric_limits::quiet_NaN(); + + std::size_t total = 0; + for (std::size_t i = 0; i < m_labels.size(); ++ i) + total += m_confusion[i][idx]; + return m_confusion[idx][idx] / float(total); } /*! @@ -167,9 +171,13 @@ public: */ float f1_score (Label_handle label) const { - std::size_t label_idx = m_map_labels[label]; - return 2.f * (m_precision[label_idx] * m_recall[label_idx]) - / (m_precision[label_idx] + m_recall[label_idx]); + float p = precision(label); + float r = recall(label); + + if (p == 0.f && r == 0.f) + return 0.f; + + return 2.f * p * r / (p + r); } /*! @@ -182,7 +190,17 @@ public: */ float intersection_over_union (Label_handle label) const { - return m_iou[m_map_labels[label]]; + std::size_t idx = m_map_labels[label]; + + std::size_t total = 0; + for (std::size_t i = 0; i < m_labels.size(); ++ i) + { + total += m_confusion[i][idx]; + if (i != idx) + total += m_confusion[idx][i]; + } + + return m_confusion[idx][idx] / float(total); } /// @} @@ -191,30 +209,214 @@ public: /// @{ + std::size_t number_of_misclassified_items() const + { + std::size_t total = 0; + for (std::size_t i = 0; i < m_labels.size(); ++ i) + for (std::size_t j = 0; j < m_labels.size(); ++ j) + if (i != j) + total += m_confusion[i][j]; + return total; + } + + std::size_t number_of_items() const + { + std::size_t total = 0; + for (std::size_t i = 0; i < m_labels.size(); ++ i) + for (std::size_t j = 0; j < m_labels.size(); ++ j) + total += m_confusion[i][j]; + return total; + } + /*! \brief Returns the accuracy of the training. Accuracy is the total number of true positives divided by the total number of provided inliers. */ - float accuracy() const { return m_accuracy; } + float accuracy() const + { + std::size_t true_positives = 0; + std::size_t total = 0; + for (std::size_t i = 0; i < m_labels.size(); ++ i) + { + true_positives += m_confusion[i][i]; + for (std::size_t j = 0; j < m_labels.size(); ++ j) + total += m_confusion[i][j]; + } + return true_positives / float(total); + } /*! \brief Returns the mean \f$F_1\f$ score of the training over all labels (see `f1_score()`). */ - float mean_f1_score() const { return m_mean_f1; } + float mean_f1_score() const + { + float mean = 0; + std::size_t nb = 0; + for (std::size_t i = 0; i < m_labels.size(); ++ i) + if (label_has_ground_truth(i)) + { + mean += f1_score(m_labels[i]); + ++ nb; + } + return mean / nb; + } /*! \brief Returns the mean intersection over union of the training over all labels (see `intersection_over_union()`). */ - float mean_intersection_over_union() const { return m_mean_iou; } + float mean_intersection_over_union() const + { + float mean = 0; + std::size_t nb = 0; + for (std::size_t i = 0; i < m_labels.size(); ++ i) + { + float iou = intersection_over_union(m_labels[i]); + if (!std::isnan(iou)) + { + mean += iou; + ++ nb; + } + } + return mean / nb; + } /// @} + + friend std::ostream& operator<< (std::ostream& os, const Evaluation& evaluation) + { + os << "Evaluation of classification:" << std::endl; + os << " * Global results:" << std::endl; + os << " - " << evaluation.number_of_misclassified_items() + << " misclassified item(s) out of " << evaluation.number_of_items() << std::endl + << " - Accuracy = " << evaluation.accuracy() << std::endl + << " - Mean F1 score = " << evaluation.mean_f1_score() << std::endl + << " - Mean IoU = " << evaluation.mean_intersection_over_union() << std::endl; + os << " * Detailed results:" << std::endl; + for (std::size_t i = 0; i < evaluation.m_labels.size(); ++ i) + { + os << " - \"" << evaluation.m_labels[i]->name() << "\": "; + if (evaluation.label_has_ground_truth(i)) + os << "Precision = " << evaluation.precision(evaluation.m_labels[i]) << " ; " + << "Recall = " << evaluation.recall(evaluation.m_labels[i]) << " ; " + << "F1 score = " << evaluation.f1_score(evaluation.m_labels[i]) << " ; " + << "IoU = " << evaluation.intersection_over_union(evaluation.m_labels[i]) << std::endl; + else + os << "(no ground truth)" << std::endl; + } + return os; + } + + static std::ostream& write_evaluation_to_html (std::ostream& os, const Evaluation& evaluation) + { + os << "" << std::endl + << "" << std::endl + << "" << std::endl + << "" << std::endl + << "Evaluation of CGAL Classification results" << std::endl + << "" << std::endl + << "" << std::endl + << "

Evaluation of CGAL Classification results

" << std::endl; + + os << "

Global Results

" << std::endl + << "
    " << std::endl + << "
  • " << evaluation.number_of_misclassified_items() + << " misclassified item(s) out of " << evaluation.number_of_items() << "
  • " << std::endl + << "
  • Accuracy = " << evaluation.accuracy() << "
  • " << std::endl + << "
  • Mean F1 score = " << evaluation.mean_f1_score() << "
  • " << std::endl + << "
  • Mean IoU = " << evaluation.mean_intersection_over_union() << "
  • " << std::endl + << "
" << std::endl; + + const Label_set& labels = evaluation.m_labels; + + os << "

Detailed Results

" << std::endl + << "" << std::endl + << " " << std::endl + << " " << std::endl + << " " << std::endl + << " " << std::endl + << " " << std::endl + << " " << std::endl + << " " << std::endl; + for (std::size_t i = 0; i < labels.size(); ++ i) + if (evaluation.label_has_ground_truth(i)) + os << " " << std::endl + << " " << std::endl + << " " << std::endl + << " " << std::endl + << " " << std::endl + << " " << std::endl + << " " << std::endl; + else + os << " " << std::endl + << " " << std::endl + << " " << std::endl + << " " << std::endl + << " " << std::endl + << " " << std::endl + << " " << std::endl; + + os << "
LabelPrecisionRecallF1 scoreIoU
" << labels[i]->name() << "" << evaluation.precision(labels[i]) << "" << evaluation.recall(labels[i]) << "" << evaluation.f1_score(labels[i]) << "" << evaluation.intersection_over_union(labels[i]) << "
" << labels[i]->name() << "(no ground truth)
" << std::endl; + + os << "

Confusion Matrix

" << std::endl + << "" << std::endl + << " " << std::endl + << " " << std::endl; + for (std::size_t i = 0; i < labels.size(); ++ i) + os << " " << std::endl; + os << " " << std::endl; + os << " " << std::endl; + + std::vector sums (labels.size(), 0); + for (std::size_t i = 0; i < labels.size(); ++ i) + { + os << " " << std::endl + << " " << std::endl; + std::size_t sum = 0; + for (std::size_t j = 0; j < labels.size(); ++ j) + { + if (i == j) + os << " " << std::endl; + else + os << " " << std::endl; + sum += evaluation.m_confusion[i][j]; + sums[j] += evaluation.m_confusion[i][j]; + } + os << " " << std::endl; + os << " " << std::endl; + } + + os << " " << std::endl + << " " << std::endl; + std::size_t total = 0; + for (std::size_t j = 0; j < labels.size(); ++ j) + { + os << " " << std::endl; + total += sums[j]; + } + os << " " << std::endl + << " " << std::endl + << "
" << labels[i]->name() << "PREDICTIONS
" << labels[i]->name() << "" << evaluation.m_confusion[i][j] << "" << evaluation.m_confusion[i][j] << "" << sum << "
GROUND TRUTH" << sums[j] << "" << total << "
" << std::endl + << "
" << std::endl + << "" << std::endl + << "" << std::endl; + + return os; + } }; - + } // namespace Classification From 886d1b7810fc23cda270c980d9bb3428e0d567b8 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 25 Feb 2020 12:06:27 +0100 Subject: [PATCH 04/79] Update doc of Evaluation --- .../include/CGAL/Classification/Evaluation.h | 70 +++++++++++++++---- 1 file changed, 57 insertions(+), 13 deletions(-) diff --git a/Classification/include/CGAL/Classification/Evaluation.h b/Classification/include/CGAL/Classification/Evaluation.h index 8600ec5ae97..4ce6d0b53ad 100644 --- a/Classification/include/CGAL/Classification/Evaluation.h +++ b/Classification/include/CGAL/Classification/Evaluation.h @@ -41,28 +41,33 @@ public: /// \name Constructor /// @{ + /*! + \brief Instantiates an empty evaluation object. + + \param labels labels used. + */ Evaluation (const Label_set& labels) : m_labels (labels) { init(); } -/*! + /*! - \brief Instantiates an evaluation object and computes all - measurements. + \brief Instantiates an evaluation object and computes all + measurements. - \param labels labels used. + \param labels labels used. - \param ground_truth vector of label indices: it should contain the - index of the corresponding label in the `Label_set` provided in the - constructor. Input items that do not have a ground truth information - should be given the value `-1`. + \param ground_truth vector of label indices: it should contain the + index of the corresponding label in the `Label_set` provided in the + constructor. Input items that do not have a ground truth information + should be given the value `-1`. - \param result similar to `ground_truth` but contained the result of - a classification. + \param result similar to `ground_truth` but contained the result of + a classification. -*/ + */ template Evaluation (const Label_set& labels, const GroundTruthIndexRange& ground_truth, @@ -73,8 +78,6 @@ public: append(ground_truth, result); } - /// @} - /// \cond SKIP_IN_MANUAL void init() { @@ -96,6 +99,18 @@ public: /// \endcond + /*! + \brief Append more items to the evaluation object. + + \param ground_truth vector of label indices: it should contain the + index of the corresponding label in the `Label_set` provided in the + constructor. Input items that do not have a ground truth information + should be given the value `-1`. + + \param result similar to `ground_truth` but contained the result of + a classification. + + */ template void append (const GroundTruthIndexRange& ground_truth, const ResultIndexRange& result) @@ -111,6 +126,8 @@ public: } } + /// @} + /// \name Label Evaluation /// @{ @@ -208,7 +225,20 @@ public: /// \name Global Evaluation /// @{ + /*! + \brief Returns the number of items whose ground truth is + `ground_truth` and which were classified as `result`. + */ + std::size_t confusion (Label_handle ground_truth, Label_handle result) + { + std::size_t idx_gt = m_map_labels[ground_truth]; + std::size_t idx_r = m_map_labels[result]; + return m_confusion[idx_gt][idx_r]; + } + /*! + \brief Returns the number of misclassified items. + */ std::size_t number_of_misclassified_items() const { std::size_t total = 0; @@ -219,6 +249,9 @@ public: return total; } + /*! + \brief Returns the total number of items used for evaluation. + */ std::size_t number_of_items() const { std::size_t total = 0; @@ -286,6 +319,12 @@ public: /// @} + /// \name Output Formatting Functions + /// @{ + + /*! + \brief Outputs the evaluation in a simple ASCII format to the stream `os`. + */ friend std::ostream& operator<< (std::ostream& os, const Evaluation& evaluation) { os << "Evaluation of classification:" << std::endl; @@ -310,6 +349,9 @@ public: return os; } + /*! + \brief Outputs the evaluation as an HTML page to the stream `os`. + */ static std::ostream& write_evaluation_to_html (std::ostream& os, const Evaluation& evaluation) { os << "" << std::endl @@ -415,6 +457,8 @@ Classification package.

" << std::endl return os; } + /// @} + }; From 279d1584bb8c0057491a6dd8072616b9d5805a20 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Wed, 13 Mar 2019 11:15:25 +0100 Subject: [PATCH 05/79] Add convenient feature_cast operator --- Classification/include/CGAL/Classification/Feature_base.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Classification/include/CGAL/Classification/Feature_base.h b/Classification/include/CGAL/Classification/Feature_base.h index 7da3d8536de..cd87f4a964b 100644 --- a/Classification/include/CGAL/Classification/Feature_base.h +++ b/Classification/include/CGAL/Classification/Feature_base.h @@ -107,6 +107,12 @@ public: #endif +template +FeatureType* feature_cast (Feature_handle fh) +{ + return dynamic_cast(&*(fh)); +} + } // namespace Classification From 2d46d220e9460bf94940917139f69fda6b6a4dae Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Mon, 18 Mar 2019 10:11:57 +0100 Subject: [PATCH 06/79] Fix feature usage --- .../CGAL/Classification/ETHZ/internal/random-forest/node.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/node.hpp b/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/node.hpp index ab664637d57..91b79992529 100644 --- a/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/node.hpp +++ b/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/node.hpp @@ -283,7 +283,7 @@ public: void get_feature_usage (std::vector& count) const { - if (!is_leaf) + if (!is_leaf && splitter.feature != -1) { count[std::size_t(splitter.feature)] ++; left->get_feature_usage(count); From 2b2b5c48bfae757e71e0bd45ce3d0bbcb7f097ab Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 29 Jan 2019 16:34:34 +0100 Subject: [PATCH 07/79] Improve parameter interpretation in OpenCV RF --- .../OpenCV/Random_forest_classifier.h | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/Classification/include/CGAL/Classification/OpenCV/Random_forest_classifier.h b/Classification/include/CGAL/Classification/OpenCV/Random_forest_classifier.h index 4c5fb3220b5..5de7855cf57 100644 --- a/Classification/include/CGAL/Classification/OpenCV/Random_forest_classifier.h +++ b/Classification/include/CGAL/Classification/OpenCV/Random_forest_classifier.h @@ -181,12 +181,20 @@ public: for (std::size_t i = 0; i < m_labels.size(); ++ i) priors[i] = 1.; - CvRTParams params (m_max_depth, m_min_sample_count, - 0, false, m_max_categories, priors, false, 0, - m_max_number_of_trees_in_the_forest, - m_forest_accuracy, - CV_TERMCRIT_ITER | CV_TERMCRIT_EPS - ); + CvRTParams params; + + if (m_forest_accuracy == 0.f) + params = CvRTParams + (m_max_depth, m_min_sample_count, + 0, false, m_max_categories, priors, false, 0, + m_max_number_of_trees_in_the_forest, + m_forest_accuracy, CV_TERMCRIT_ITER); + else + params = CvRTParams + (m_max_depth, m_min_sample_count, + 0, false, m_max_categories, priors, false, 0, + m_max_number_of_trees_in_the_forest, + m_forest_accuracy, CV_TERMCRIT_EPS | CV_TERMCRIT_ITER); cv::Mat var_type (m_features.size() + 1, 1, CV_8U); var_type.setTo (cv::Scalar(CV_VAR_NUMERICAL)); @@ -206,8 +214,13 @@ public: rtree->setUseSurrogates(false); rtree->setPriors(cv::Mat()); rtree->setCalculateVarImportance(false); + + cv::TermCriteria criteria; + if (m_forest_accuracy == 0.f) + criteria = cv::TermCriteria (cv::TermCriteria::COUNT, m_max_number_of_trees_in_the_forest, m_forest_accuracy); + else + criteria = cv::TermCriteria (cv::TermCriteria::EPS + cv::TermCriteria::COUNT, m_max_number_of_trees_in_the_forest, m_forest_accuracy); - cv::TermCriteria criteria (cv::TermCriteria::EPS + cv::TermCriteria::COUNT, m_max_number_of_trees_in_the_forest, 0.01f); rtree->setTermCriteria (criteria); cv::Ptr tdata = cv::ml::TrainData::create From 311c111f24fdae19e8bb99335764ebecc04749b5 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 29 Jan 2019 16:35:00 +0100 Subject: [PATCH 08/79] Add verbosity info in ETHZ RF --- .../Classification/ETHZ/Random_forest_classifier.h | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h b/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h index a1211943214..d2f50acecba 100644 --- a/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h +++ b/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h @@ -179,6 +179,10 @@ public: std::vector gt; std::vector ft; + +#ifdef CGAL_CLASSIFICATION_VERBOSE + std::vector count (m_labels.size(), 0); +#endif for (std::size_t i = 0; i < ground_truth.size(); ++ i) { @@ -188,10 +192,17 @@ public: for (std::size_t f = 0; f < m_features.size(); ++ f) ft.push_back(m_features[f]->value(i)); gt.push_back(g); +#ifdef CGAL_CLASSIFICATION_VERBOSE + count[std::size_t(g)] ++; +#endif } } - CGAL_CLASSIFICATION_CERR << "Using " << gt.size() << " inliers" << std::endl; + CGAL_CLASSIFICATION_CERR << "Using " << gt.size() << " inliers:" << std::endl; +#ifdef CGAL_CLASSIFICATION_VERBOSE + for (std::size_t i = 0; i < m_labels.size(); ++ i) + std::cerr << " * " << m_labels[i]->name() << ": " << count[i] << " inlier(s)" << std::endl; +#endif CGAL::internal::liblearning::DataView2D label_vector (&(gt[0]), gt.size(), 1); CGAL::internal::liblearning::DataView2D feature_vector(&(ft[0]), gt.size(), ft.size() / gt.size()); From 12427c498539ec4a8b8cebc0ef83d16e0a165e56 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 29 Jan 2019 16:35:43 +0100 Subject: [PATCH 09/79] Update plugin with latest modifications --- .../Classification/Classification_plugin.cpp | 38 +++++++--------- .../Classification/Cluster_classification.cpp | 20 +++++---- .../Classification/Item_classification_base.h | 44 +++++++++++++------ .../Point_set_item_classification.cpp | 37 +++++++++------- .../Surface_mesh_item_classification.cpp | 16 +++---- 5 files changed, 88 insertions(+), 67 deletions(-) diff --git a/Polyhedron/demo/Polyhedron/Plugins/Classification/Classification_plugin.cpp b/Polyhedron/demo/Polyhedron/Plugins/Classification/Classification_plugin.cpp index b9bf0568ab3..5d533511595 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Classification/Classification_plugin.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/Classification/Classification_plugin.cpp @@ -46,11 +46,6 @@ #include #include -#define CGAL_CLASSIFICATION_ETHZ_ID "Random Forest (ETHZ)" -#define CGAL_CLASSIFICATION_TENSORFLOW_ID "Neural Network (TensorFlow)" -#define CGAL_CLASSIFICATION_OPENCV_ID "Random Forest (OpenCV)" -#define CGAL_CLASSIFICATION_SOWF_ID "Sum of Weighted Features" - using namespace CGAL::Three; class Polyhedron_demo_classification_plugin : @@ -546,13 +541,13 @@ public Q_SLOTS: int get_classifier () { if (classifier->text() == QString(CGAL_CLASSIFICATION_ETHZ_ID)) - return 1; + return CGAL_CLASSIFICATION_ETHZ_NUMBER; if (classifier->text() == QString(CGAL_CLASSIFICATION_TENSORFLOW_ID)) - return 3; + return CGAL_CLASSIFICATION_TENSORFLOW_NUMBER; if (classifier->text() == QString(CGAL_CLASSIFICATION_OPENCV_ID)) - return 2; + return CGAL_CLASSIFICATION_OPENCV_NUMBER; if (classifier->text() == QString(CGAL_CLASSIFICATION_SOWF_ID)) - return 0; + return CGAL_CLASSIFICATION_SOWF_NUMBER; std::cerr << "Error: unknown classifier" << std::endl; return -1; @@ -650,18 +645,18 @@ public Q_SLOTS: QString filename; int classifier = get_classifier(); - if (classifier == 0) // Sum of Weighted Featuers + if (classifier == CGAL_CLASSIFICATION_SOWF_NUMBER) // Sum of Weighted Featuers filename = QFileDialog::getSaveFileName(mw, tr("Save classification configuration"), tr("%1 (CGAL classif config).xml").arg(classif->item()->name()), "CGAL classification configuration (*.xml);;"); - else if (classifier == 1) // Random Forest (ETHZ) + else if (classifier == CGAL_CLASSIFICATION_ETHZ_NUMBER) // Random Forest (ETHZ) filename = QFileDialog::getSaveFileName(mw, tr("Save classification configuration"), tr("%1 (ETHZ random forest config).bin").arg(classif->item()->name()), "ETHZ random forest configuration (*.bin);;"); #ifdef CGAL_LINKED_WITH_OPENCV - else if (classifier == 2) // Random Forest (OpenCV) + else if (classifier == CGAL_CLASSIFICATION_OPENCV_NUMBER) // Random Forest (OpenCV) filename = QFileDialog::getSaveFileName(mw, tr("Save classification configuration"), tr("%1 (OpenCV %2.%3 random forest config).xml") @@ -673,7 +668,7 @@ public Q_SLOTS: .arg(CV_MINOR_VERSION)); #endif #ifdef CGAL_LINKED_WITH_TENSORFLOW - else if (classifier == 3) // Neural Network (TensorFlow) + else if (classifier == CGAL_CLASSIFICATION_TENSORFLOW_NUMBER) // Neural Network (TensorFlow) filename = QFileDialog::getSaveFileName(mw, tr("Save classification configuration"), tr("%1 (CGAL Neural Network config).xml").arg(classif->item()->name()), @@ -704,18 +699,18 @@ public Q_SLOTS: QString filename; int classifier = get_classifier(); - if (classifier == 0) // SOWF + if (classifier == CGAL_CLASSIFICATION_SOWF_NUMBER) // Sum of Weighted Featuers filename = QFileDialog::getOpenFileName(mw, tr("Open CGAL classification configuration"), ".", "CGAL classification configuration (*.xml);;All Files (*)"); - else if (classifier == 1) // ETHZ + else if (classifier == CGAL_CLASSIFICATION_ETHZ_NUMBER) // Random Forest (ETHZ) filename = QFileDialog::getOpenFileName(mw, tr("Open ETHZ random forest configuration"), ".", "ETHZ random forest configuration (*.bin);Deprecated compressed ETHZ random forest configuration (*.gz);All Files (*)"); #ifdef CGAL_LINKED_WITH_OPENCV - else if (classifier == 2) // OpenCV + else if (classifier == CGAL_CLASSIFICATION_OPENCV_NUMBER) // Random Forest (OpenCV) filename = QFileDialog::getOpenFileName(mw, tr("Open OpenCV %2.%3 random forest configuration") .arg(CV_MAJOR_VERSION) @@ -726,7 +721,7 @@ public Q_SLOTS: .arg(CV_MINOR_VERSION)); #endif #ifdef CGAL_LINKED_WITH_TENSORFLOW - else if (classifier == 3) // TensorFlow + else if (classifier == CGAL_CLASSIFICATION_TENSORFLOW_NUMBER) // Neural Network (TensorFlow) filename = QFileDialog::getOpenFileName(mw, tr("Open CGAL Neural Network classification configuration"), ".", @@ -739,10 +734,8 @@ public Q_SLOTS: QApplication::setOverrideCursor(Qt::WaitCursor); classif->load_config (filename.toStdString().c_str(), classifier); - update_plugin_from_item(classif); run (classif, 0); - QApplication::restoreOverrideCursor(); item_changed(classif->item()); } @@ -1347,13 +1340,14 @@ public Q_SLOTS: QMultipleInputDialog dialog ("Train Classifier", mw); int classifier = get_classifier(); - if (classifier == 0) // SOWF + if (classifier == CGAL_CLASSIFICATION_SOWF_NUMBER) // Sum of Weighted Featuers { QSpinBox* trials = dialog.add ("Number of trials: ", "trials"); trials->setRange (1, 99999); trials->setValue (800); } - else if (classifier == 1 || classifier == 2) // random forest + else if (classifier == CGAL_CLASSIFICATION_ETHZ_NUMBER + || classifier == CGAL_CLASSIFICATION_OPENCV_NUMBER) // random forest { QSpinBox* trees = dialog.add ("Number of trees: ", "num_trees"); trees->setRange (1, 9999); @@ -1362,7 +1356,7 @@ public Q_SLOTS: depth->setRange (1, 9999); depth->setValue (20); } - else if (classifier == 3) // neural network + else if (classifier == CGAL_CLASSIFICATION_TENSORFLOW_NUMBER) // Neural Network (TensorFlow) { QSpinBox* trials = dialog.add ("Number of trials: ", "trials"); trials->setRange (1, 99999); diff --git a/Polyhedron/demo/Polyhedron/Plugins/Classification/Cluster_classification.cpp b/Polyhedron/demo/Polyhedron/Plugins/Classification/Cluster_classification.cpp index 2db5ec4b353..fa29d05a15c 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Classification/Cluster_classification.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/Classification/Cluster_classification.cpp @@ -240,6 +240,7 @@ Cluster_classification::Cluster_classification(Scene_points_with_normal_item* po #endif // Compute neighborhood +#if 0 typedef CGAL::Triangulation_vertex_base_with_info_3 Vb; typedef CGAL::Delaunay_triangulation_cell_base_3 Cb; typedef CGAL::Triangulation_data_structure_3 Tds; @@ -279,7 +280,7 @@ Cluster_classification::Cluster_classification(Scene_points_with_normal_item* po m_clusters[std::size_t(it->first)].neighbors->push_back (std::size_t(it->second)); m_clusters[std::size_t(it->second)].neighbors->push_back (std::size_t(it->first)); } - +#endif } @@ -780,6 +781,7 @@ void Cluster_classification::add_remaining_point_set_properties_as_features(Feat prop[i] == "training" || prop[i] == "label" || prop[i] == "classification" || + prop[i] == "scan_direction_flag" || prop[i] == "real_color" || prop[i] == "shape" || prop[i] == "red" || prop[i] == "green" || prop[i] == "blue" || @@ -841,14 +843,14 @@ void Cluster_classification::train(int classifier, const QMultipleInputDialog& d std::vector indices (m_clusters.size(), -1); - if (classifier == 0) + if (classifier == CGAL_CLASSIFICATION_SOWF_NUMBER) { m_sowf->train(training, dialog.get("trials")->value()); CGAL::Classification::classify (m_clusters, m_labels, *m_sowf, indices, m_label_probabilities); } - else if (classifier == 1) + else if (classifier == CGAL_CLASSIFICATION_ETHZ_NUMBER) { if (m_ethz != NULL) delete m_ethz; @@ -860,7 +862,7 @@ void Cluster_classification::train(int classifier, const QMultipleInputDialog& d m_labels, *m_ethz, indices, m_label_probabilities); } - else if (classifier == 2) + else if (classifier == CGAL_CLASSIFICATION_OPENCV_NUMBER) { #ifdef CGAL_LINKED_WITH_OPENCV if (m_random_forest != NULL) @@ -874,7 +876,7 @@ void Cluster_classification::train(int classifier, const QMultipleInputDialog& d indices, m_label_probabilities); #endif } - else if (classifier == 3) + else if (classifier == CGAL_CLASSIFICATION_TENSORFLOW_NUMBER) { #ifdef CGAL_LINKED_WITH_TENSORFLOW if (m_neural_network != NULL) @@ -938,9 +940,9 @@ bool Cluster_classification::run (int method, int classifier, } reset_indices(); - if (classifier == 0) + if (classifier == CGAL_CLASSIFICATION_SOWF_NUMBER) run (method, *m_sowf, subdivisions, smoothing); - else if (classifier == 1) + else if (classifier == CGAL_CLASSIFICATION_ETHZ_NUMBER) { if (m_ethz == NULL) { @@ -949,7 +951,7 @@ bool Cluster_classification::run (int method, int classifier, } run (method, *m_ethz, subdivisions, smoothing); } - else if (classifier == 2) + else if (classifier == CGAL_CLASSIFICATION_OPENCV_NUMBER) { #ifdef CGAL_LINKED_WITH_OPENCV if (m_random_forest == NULL) @@ -960,7 +962,7 @@ bool Cluster_classification::run (int method, int classifier, run (method, *m_random_forest, subdivisions, smoothing); #endif } - else if (classifier == 3) + else if (classifier == CGAL_CLASSIFICATION_TENSORFLOW_NUMBER) { #ifdef CGAL_LINKED_WITH_TENSORFLOW if (m_neural_network == NULL) diff --git a/Polyhedron/demo/Polyhedron/Plugins/Classification/Item_classification_base.h b/Polyhedron/demo/Polyhedron/Plugins/Classification/Item_classification_base.h index f3732a96382..bd72eb80df9 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Classification/Item_classification_base.h +++ b/Polyhedron/demo/Polyhedron/Plugins/Classification/Item_classification_base.h @@ -10,16 +10,32 @@ #include #include -#include + + #include +#include + +#ifdef CGAL_LINKED_WITH_TENSORFLOW +# include +#endif #ifdef CGAL_LINKED_WITH_OPENCV -#include -#endif -#ifdef CGAL_LINKED_WITH_TENSORFLOW -#include +# include #endif +#define CGAL_CLASSIFICATION_ETHZ_ID "Random Forest (ETHZ)" +#define CGAL_CLASSIFICATION_ETHZ_NUMBER 0 + +#define CGAL_CLASSIFICATION_TENSORFLOW_ID "Neural Network (TensorFlow)" +#define CGAL_CLASSIFICATION_TENSORFLOW_NUMBER 1 + +#define CGAL_CLASSIFICATION_OPENCV_ID "Random Forest (OpenCV)" +#define CGAL_CLASSIFICATION_OPENCV_NUMBER 2 + +#define CGAL_CLASSIFICATION_SOWF_ID "Sum of Weighted Features" +#define CGAL_CLASSIFICATION_SOWF_NUMBER 3 + + class Item_classification_base { public: @@ -172,23 +188,25 @@ public: return; } - if (classifier == 0) + if (classifier == CGAL_CLASSIFICATION_SOWF_NUMBER) { std::ofstream f (filename); m_sowf->save_configuration (f); } - else if (classifier == 1) + else if (classifier == CGAL_CLASSIFICATION_ETHZ_NUMBER) { + std::cerr << "D "; std::ofstream f (filename, std::ios_base::out | std::ios_base::binary); m_ethz->save_configuration (f); + std::cerr << "E "; } - else if (classifier == 2) + else if (classifier == CGAL_CLASSIFICATION_OPENCV_NUMBER) { #ifdef CGAL_LINKED_WITH_OPENCV m_random_forest->save_configuration (filename); #endif } - else if (classifier == 3) + else if (classifier == CGAL_CLASSIFICATION_TENSORFLOW_NUMBER) { #ifdef CGAL_LINKED_WITH_TENSORFLOW std::ofstream f (filename); @@ -204,12 +222,12 @@ public: return; } - if (classifier == 0) + if (classifier == CGAL_CLASSIFICATION_SOWF_NUMBER) { std::ifstream f (filename); m_sowf->load_configuration (f, true); } - else if (classifier == 1) + else if (classifier == CGAL_CLASSIFICATION_ETHZ_NUMBER) { if (m_ethz == NULL) m_ethz = new ETHZ_random_forest (m_labels, m_features); @@ -221,13 +239,13 @@ public: else m_ethz->load_configuration (f); } - else if (classifier == 2) + else if (classifier == CGAL_CLASSIFICATION_OPENCV_NUMBER) { #ifdef CGAL_LINKED_WITH_OPENCV m_random_forest->load_configuration (filename); #endif } - else if (classifier == 3) + else if (classifier == CGAL_CLASSIFICATION_TENSORFLOW_NUMBER) { #ifdef CGAL_LINKED_WITH_TENSORFLOW if (m_neural_network == NULL) diff --git a/Polyhedron/demo/Polyhedron/Plugins/Classification/Point_set_item_classification.cpp b/Polyhedron/demo/Polyhedron/Plugins/Classification/Point_set_item_classification.cpp index feb5779e208..fa6197b4c44 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Classification/Point_set_item_classification.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/Classification/Point_set_item_classification.cpp @@ -247,7 +247,13 @@ Point_set_item_classification::~Point_set_item_classification() if (m_points != NULL) { // For LAS saving, convert classification info in the LAS standard - if (m_input_is_las) + QString filename = m_points->property("source filename").toString(); + + if (m_input_is_las || + (!filename.isEmpty() && (filename.endsWith(QString(".las")) || + filename.endsWith(QString(".LAS")) || + filename.endsWith(QString(".laz")) || + filename.endsWith(QString(".LAZ"))))) { Point_set::Property_map las_classif = m_points->point_set()->add_property_map("classification", 0).first; @@ -670,6 +676,7 @@ void Point_set_item_classification::add_remaining_point_set_properties_as_featur prop[i] == "training" || prop[i] == "label" || prop[i] == "classification" || + prop[i] == "scan_direction_flag" || prop[i] == "real_color" || prop[i] == "shape" || prop[i] == "red" || prop[i] == "green" || prop[i] == "blue" || @@ -731,14 +738,14 @@ void Point_set_item_classification::train(int classifier, const QMultipleInputDi for (std::size_t i = 0; i < m_labels.size(); ++ i) std::cerr << " * " << m_labels[i]->name() << ": " << nb_label[i] << " point(s)" << std::endl; - if (classifier == 0) - { - m_sowf->train(training, dialog.get("trials")->value()); - CGAL::Classification::classify (*(m_points->point_set()), - m_labels, *m_sowf, - indices, m_label_probabilities); - } - else if (classifier == 1) + if (classifier == CGAL_CLASSIFICATION_SOWF_NUMBER) + { + m_sowf->train(training, dialog.get("trials")->value()); + CGAL::Classification::classify (*(m_points->point_set()), + m_labels, *m_sowf, + indices, m_label_probabilities); + } + else if (classifier == CGAL_CLASSIFICATION_ETHZ_NUMBER) { if (m_ethz != NULL) delete m_ethz; @@ -750,7 +757,7 @@ void Point_set_item_classification::train(int classifier, const QMultipleInputDi m_labels, *m_ethz, indices, m_label_probabilities); } - else if (classifier == 2) + else if (classifier == CGAL_CLASSIFICATION_OPENCV_NUMBER) { #ifdef CGAL_LINKED_WITH_OPENCV if (m_random_forest != NULL) @@ -764,7 +771,7 @@ void Point_set_item_classification::train(int classifier, const QMultipleInputDi indices, m_label_probabilities); #endif } - else if (classifier == 3) + else if (classifier == CGAL_CLASSIFICATION_TENSORFLOW_NUMBER) { #ifdef CGAL_LINKED_WITH_TENSORFLOW if (m_neural_network != NULL) @@ -829,9 +836,9 @@ bool Point_set_item_classification::run (int method, int classifier, } reset_indices(); - if (classifier == 0) + if (classifier == CGAL_CLASSIFICATION_SOWF_NUMBER) run (method, *m_sowf, subdivisions, smoothing); - else if (classifier == 1) + else if (classifier == CGAL_CLASSIFICATION_ETHZ_NUMBER) { if (m_ethz == NULL) { @@ -840,7 +847,7 @@ bool Point_set_item_classification::run (int method, int classifier, } run (method, *m_ethz, subdivisions, smoothing); } - else if (classifier == 2) + else if (classifier == CGAL_CLASSIFICATION_OPENCV_NUMBER) { #ifdef CGAL_LINKED_WITH_OPENCV if (m_random_forest == NULL) @@ -851,7 +858,7 @@ bool Point_set_item_classification::run (int method, int classifier, run (method, *m_random_forest, subdivisions, smoothing); #endif } - else if (classifier == 3) + else if (classifier == CGAL_CLASSIFICATION_TENSORFLOW_NUMBER) { #ifdef CGAL_LINKED_WITH_TENSORFLOW if (m_neural_network == NULL) diff --git a/Polyhedron/demo/Polyhedron/Plugins/Classification/Surface_mesh_item_classification.cpp b/Polyhedron/demo/Polyhedron/Plugins/Classification/Surface_mesh_item_classification.cpp index 2c032b631f9..89838e0c0de 100644 --- a/Polyhedron/demo/Polyhedron/Plugins/Classification/Surface_mesh_item_classification.cpp +++ b/Polyhedron/demo/Polyhedron/Plugins/Classification/Surface_mesh_item_classification.cpp @@ -296,14 +296,14 @@ void Surface_mesh_item_classification::train (int classifier, const QMultipleInp for (std::size_t i = 0; i < m_labels.size(); ++ i) std::cerr << " * " << m_labels[i]->name() << ": " << nb_label[i] << " face(s)" << std::endl; - if (classifier == 0) + if (classifier == CGAL_CLASSIFICATION_SOWF_NUMBER) { m_sowf->train(training, dialog.get("trials")->value()); CGAL::Classification::classify (m_mesh->polyhedron()->faces(), m_labels, *m_sowf, indices, m_label_probabilities); } - else if (classifier == 1) + else if (classifier == CGAL_CLASSIFICATION_ETHZ_NUMBER) { if (m_ethz != NULL) delete m_ethz; @@ -315,7 +315,7 @@ void Surface_mesh_item_classification::train (int classifier, const QMultipleInp m_labels, *m_ethz, indices, m_label_probabilities); } - else if (classifier == 2) + else if (classifier == CGAL_CLASSIFICATION_OPENCV_NUMBER) { #ifdef CGAL_LINKED_WITH_OPENCV if (m_random_forest != NULL) @@ -330,7 +330,7 @@ void Surface_mesh_item_classification::train (int classifier, const QMultipleInp indices, m_label_probabilities); #endif } - else if (classifier == 3) + else if (classifier == CGAL_CLASSIFICATION_TENSORFLOW_NUMBER) { #ifdef CGAL_LINKED_WITH_TENSORFLOW if (m_neural_network != NULL) @@ -392,9 +392,9 @@ bool Surface_mesh_item_classification::run (int method, int classifier, return false; } - if (classifier == 0) + if (classifier == CGAL_CLASSIFICATION_SOWF_NUMBER) run (method, *m_sowf, subdivisions, smoothing); - else if (classifier == 1) + else if (classifier == CGAL_CLASSIFICATION_ETHZ_NUMBER) { if (m_ethz == NULL) { @@ -403,7 +403,7 @@ bool Surface_mesh_item_classification::run (int method, int classifier, } run (method, *m_ethz, subdivisions, smoothing); } - else if (classifier == 2) + else if (classifier == CGAL_CLASSIFICATION_OPENCV_NUMBER) { #ifdef CGAL_LINKED_WITH_OPENCV if (m_random_forest == NULL) @@ -414,7 +414,7 @@ bool Surface_mesh_item_classification::run (int method, int classifier, run (method, *m_random_forest, subdivisions, smoothing); #endif } - else if (classifier == 3) + else if (classifier == CGAL_CLASSIFICATION_TENSORFLOW_NUMBER) { #ifdef CGAL_LINKED_WITH_TENSORFLOW if (m_neural_network == NULL) From 5b1334fd3d6f04165b247dded5e8d3ed015d2b71 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Tue, 25 Feb 2020 16:25:23 +0100 Subject: [PATCH 10/79] Improve API of label/feature sets --- .../include/CGAL/Classification/Feature_set.h | 26 +++++++++++++++---- .../include/CGAL/Classification/Label_set.h | 22 ++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/Classification/include/CGAL/Classification/Feature_set.h b/Classification/include/CGAL/Classification/Feature_set.h index e225649cf84..fcf5bb6fdae 100644 --- a/Classification/include/CGAL/Classification/Feature_set.h +++ b/Classification/include/CGAL/Classification/Feature_set.h @@ -59,6 +59,14 @@ class Feature_set public: +#ifdef DOXYGEN_RUNNING + typedef unspecified_type const_iterator; ///< A random access iterator with value type `Feature_handle`. + typedef unspecified_type iterator; ///< A random access iterator with value type `Feature_handle`. +#else + typedef std::vector::const_iterator const_iterator; + typedef std::vector::iterator iterator; +#endif + /// \name Constructor /// @{ @@ -190,8 +198,6 @@ public: /// @{ -#if defined(CGAL_LINKED_WITH_TBB) || defined(DOXYGEN_RUNNING) - /*! \brief Initializes structures to compute features in parallel. @@ -199,7 +205,8 @@ public: should be called before making several calls of `add()`. After the calls of `add()`, `end_parallel_additions()` should be called. - \note This function requires \ref thirdpartyTBB. + \note If \ref thirdpartyTBB is not available, this function does + nothing. \warning As arguments of `add()` are passed by reference and that new threads are started if `begin_parallel_additions()` is used, it is @@ -212,7 +219,9 @@ public: */ void begin_parallel_additions() { +#ifdef CGAL_LINKED_WITH_TBB m_tasks = new tbb::task_group; +#endif } /*! @@ -224,12 +233,14 @@ public: should be called after `begin_parallel_additions()` and several calls of `add()`. - \note This function requires \ref thirdpartyTBB. + \note If \ref thirdpartyTBB is not available, this function does + nothing. \sa `begin_parallel_additions()` */ void end_parallel_additions() { +#ifdef CGAL_LINKED_WITH_TBB m_tasks->wait(); delete m_tasks; m_tasks = nullptr; @@ -237,14 +248,19 @@ public: for (std::size_t i = 0; i < m_adders.size(); ++ i) delete m_adders[i]; m_adders.clear(); - } #endif + } /// @} /// \name Access /// @{ + + const_iterator begin() const { return m_features.begin(); } + iterator begin() { return m_features.begin(); } + const_iterator end() const { return m_features.end(); } + iterator end() { return m_features.end(); } /*! \brief Returns how many features are defined. diff --git a/Classification/include/CGAL/Classification/Label_set.h b/Classification/include/CGAL/Classification/Label_set.h index 0e0fe57882a..63df128aaa7 100644 --- a/Classification/include/CGAL/Classification/Label_set.h +++ b/Classification/include/CGAL/Classification/Label_set.h @@ -36,7 +36,24 @@ class Label_set public: +#ifdef DOXYGEN_RUNNING + typedef unspecified_type const_iterator; ///< A random access iterator with value type `Label_handle`. + typedef unspecified_type iterator; ///< A random access iterator with value type `Label_handle`. +#else + typedef std::vector::const_iterator const_iterator; + typedef std::vector::iterator iterator; +#endif + Label_set() { } + + /*! + \brief Constructs a label set from a set of label names. + */ + Label_set(std::initializer_list labels) + { + for (const char* l : labels) + m_labels.push_back (Label_handle(new Classification::Label(l))); + } /// \cond SKIP_IN_MANUAL virtual ~Label_set() { } @@ -83,6 +100,11 @@ public: return true; } + const_iterator begin() const { return m_labels.begin(); } + iterator begin() { return m_labels.begin(); } + const_iterator end() const { return m_labels.end(); } + iterator end() { return m_labels.end(); } + /*! \brief Returns how many labels are defined. */ From 92bba3c700aa6676ea4ca8b8cccf92c17774463f Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Thu, 2 Apr 2020 08:00:24 +0200 Subject: [PATCH 11/79] Make classifiers comply with Range concept --- .../ETHZ/Random_forest_classifier.h | 8 +++++--- .../OpenCV/Random_forest_classifier.h | 15 ++++++++++----- .../Sum_of_weighted_features_classifier.h | 10 +++++++--- .../TensorFlow/Neural_network_classifier.h | 6 ++++-- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h b/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h index d2f50acecba..b55d098367d 100644 --- a/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h +++ b/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h @@ -183,10 +183,11 @@ public: #ifdef CGAL_CLASSIFICATION_VERBOSE std::vector count (m_labels.size(), 0); #endif - - for (std::size_t i = 0; i < ground_truth.size(); ++ i) + + std::size_t i = 0; + for (const auto& gt_value : ground_truth) { - int g = int(ground_truth[i]); + int g = int(gt_value); if (g != -1) { for (std::size_t f = 0; f < m_features.size(); ++ f) @@ -196,6 +197,7 @@ public: count[std::size_t(g)] ++; #endif } + ++ i; } CGAL_CLASSIFICATION_CERR << "Using " << gt.size() << " inliers:" << std::endl; diff --git a/Classification/include/CGAL/Classification/OpenCV/Random_forest_classifier.h b/Classification/include/CGAL/Classification/OpenCV/Random_forest_classifier.h index 5de7855cf57..ec993dd39ce 100644 --- a/Classification/include/CGAL/Classification/OpenCV/Random_forest_classifier.h +++ b/Classification/include/CGAL/Classification/OpenCV/Random_forest_classifier.h @@ -160,21 +160,26 @@ public: #endif std::size_t nb_samples = 0; - for (std::size_t i = 0; i < ground_truth.size(); ++ i) - if (int(ground_truth[i]) != -1) + for (const auto& gt_value : ground_truth) + if (int(gt_value) != -1) ++ nb_samples; cv::Mat training_features (int(nb_samples), int(m_features.size()), CV_32FC1); cv::Mat training_labels (int(nb_samples), 1, CV_32FC1); - for (std::size_t i = 0, index = 0; i < ground_truth.size(); ++ i) - if (int(ground_truth[i]) != -1) + std::size_t i = 0, index = 0; + for (const auto& gt_value : ground_truth) + { + if (int(gt_value) != -1) { for (std::size_t f = 0; f < m_features.size(); ++ f) training_features.at(int(index), int(f)) = m_features[f]->value(i); - training_labels.at(int(index), 0) = static_cast(ground_truth[i]); + training_labels.at(int(index), 0) = static_cast(gt_value); ++ index; } + ++ i; + } + #if (CV_MAJOR_VERSION < 3) float* priors = new float[m_labels.size()]; diff --git a/Classification/include/CGAL/Classification/Sum_of_weighted_features_classifier.h b/Classification/include/CGAL/Classification/Sum_of_weighted_features_classifier.h index 4f17b7da445..fbba230ad37 100644 --- a/Classification/include/CGAL/Classification/Sum_of_weighted_features_classifier.h +++ b/Classification/include/CGAL/Classification/Sum_of_weighted_features_classifier.h @@ -300,12 +300,16 @@ public: { std::vector > training_sets (m_labels.size()); std::size_t nb_tot = 0; - for (std::size_t i = 0; i < ground_truth.size(); ++ i) - if (int(ground_truth[i]) != -1) + std::size_t i = 0; + for (const auto& gt_value : ground_truth) + { + if (int(gt_value) != -1) { - training_sets[std::size_t(ground_truth[i])].push_back (i); + training_sets[std::size_t(gt_value)].push_back (i); ++ nb_tot; } + ++ i; + } #ifdef CLASSIFICATION_TRAINING_QUICK_ESTIMATION for (std::size_t i = 0; i < m_labels.size(); ++ i) diff --git a/Classification/include/CGAL/Classification/TensorFlow/Neural_network_classifier.h b/Classification/include/CGAL/Classification/TensorFlow/Neural_network_classifier.h index db9487b49ab..f7f96d0d842 100644 --- a/Classification/include/CGAL/Classification/TensorFlow/Neural_network_classifier.h +++ b/Classification/include/CGAL/Classification/TensorFlow/Neural_network_classifier.h @@ -258,15 +258,17 @@ public: std::vector indices; std::vector raw_gt; - for (std::size_t i = 0; i < ground_truth.size(); ++ i) + std::size_t i = 0; + for (const auto& gt_value : ground_truth) { - int gc = int(ground_truth[i]); + int gc = int(gt_value); if (gc != -1) { indices.push_back (i); raw_gt.push_back (gc); random_indices[std::size_t(gc)].push_back (indices.size() - 1); } + ++ i; } if (!initialized()) From dfee57ab3ddc3aa22beee462780a5ae725ad3e24 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Thu, 2 Apr 2020 08:01:23 +0200 Subject: [PATCH 12/79] Make evaluation comply with Range concept --- .../include/CGAL/Classification/Evaluation.h | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Classification/include/CGAL/Classification/Evaluation.h b/Classification/include/CGAL/Classification/Evaluation.h index 4ce6d0b53ad..1b5294adc7a 100644 --- a/Classification/include/CGAL/Classification/Evaluation.h +++ b/Classification/include/CGAL/Classification/Evaluation.h @@ -16,6 +16,11 @@ #include #include + +#include + +#include + #include #include // for std::isnan @@ -115,10 +120,13 @@ public: void append (const GroundTruthIndexRange& ground_truth, const ResultIndexRange& result) { - for (std::size_t i = 0; i < ground_truth.size(); ++ i) + + for (const auto& p : CGAL::make_range + (boost::make_zip_iterator(boost::make_tuple(ground_truth.begin(), result.begin())), + boost::make_zip_iterator(boost::make_tuple(ground_truth.end(), result.end())))) { - int gt = static_cast(ground_truth[i]); - int res = static_cast(result[i]); + int gt = static_cast(get<0>(p)); + int res = static_cast(get<1>(p)); if (gt == -1 || res == -1) continue; From 1207cdb22f087270f9325f8d768eba4d08b5bb59 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Thu, 2 Apr 2020 08:01:30 +0200 Subject: [PATCH 13/79] Update/improve examples --- .../Classification/example_classification.cpp | 62 ++++++++-------- .../example_cluster_classification.cpp | 45 ++++------- .../example_ethz_random_forest.cpp | 74 ++++--------------- .../Classification/example_feature.cpp | 2 +- .../example_generation_and_training.cpp | 37 +++------- .../example_mesh_classification.cpp | 14 +--- .../example_opencv_random_forest.cpp | 32 +++----- .../example_tensorflow_neural_network.cpp | 31 +++----- 8 files changed, 99 insertions(+), 198 deletions(-) diff --git a/Classification/examples/Classification/example_classification.cpp b/Classification/examples/Classification/example_classification.cpp index 0475fdfcfb3..e3800008b5d 100644 --- a/Classification/examples/Classification/example_classification.cpp +++ b/Classification/examples/Classification/example_classification.cpp @@ -13,11 +13,10 @@ #include #include #include +#include #include -typedef CGAL::Parallel_if_available_tag Concurrency_tag; - typedef CGAL::Simple_cartesian Kernel; typedef Kernel::Point_3 Point; typedef Kernel::Iso_cuboid_3 Iso_cuboid_3; @@ -84,9 +83,7 @@ int main (int argc, char** argv) std::cerr << "Computing features" << std::endl; Feature_set features; -#ifdef CGAL_LINKED_WITH_TBB features.begin_parallel_additions(); -#endif Feature_handle distance_to_plane = features.add (pts, Pmap(), eigen); Feature_handle dispersion = features.add (pts, Pmap(), grid, @@ -94,9 +91,7 @@ int main (int argc, char** argv) Feature_handle elevation = features.add (pts, Pmap(), grid, radius_dtm); -#ifdef CGAL_LINKED_WITH_TBB features.end_parallel_additions(); -#endif //! [Features] /////////////////////////////////////////////////////////////////// @@ -146,7 +141,7 @@ int main (int argc, char** argv) CGAL::Real_timer t; t.start(); - Classification::classify (pts, labels, classifier, label_indices); + Classification::classify (pts, labels, classifier, label_indices); t.stop(); std::cerr << "Raw classification performed in " << t.time() << " second(s)" << std::endl; t.reset(); @@ -156,7 +151,7 @@ int main (int argc, char** argv) /////////////////////////////////////////////////////////////////// //! [Smoothing] t.start(); - Classification::classify_with_local_smoothing + Classification::classify_with_local_smoothing (pts, Pmap(), labels, classifier, neighborhood.sphere_neighbor_query(radius_neighbors), label_indices); @@ -169,7 +164,7 @@ int main (int argc, char** argv) /////////////////////////////////////////////////////////////////// //! [Graph_cut] t.start(); - Classification::classify_with_graphcut + Classification::classify_with_graphcut (pts, Pmap(), labels, classifier, neighborhood.k_neighbor_query(12), 0.2f, 4, label_indices); @@ -180,36 +175,43 @@ int main (int argc, char** argv) // Save the output in a colored PLY format - std::ofstream f ("classification.ply"); - f << "ply" << std::endl - << "format ascii 1.0" << std::endl - << "element vertex " << pts.size() << std::endl - << "property float x" << std::endl - << "property float y" << std::endl - << "property float z" << std::endl - << "property uchar red" << std::endl - << "property uchar green" << std::endl - << "property uchar blue" << std::endl - << "end_header" << std::endl; + std::vector red, green, blue; + red.reserve(pts.size()); + green.reserve(pts.size()); + blue.reserve(pts.size()); for (std::size_t i = 0; i < pts.size(); ++ i) { - f << pts[i] << " "; - Label_handle label = labels[std::size_t(label_indices[i])]; + unsigned r = 0, g = 0, b = 0; if (label == ground) - f << "245 180 0" << std::endl; - else if (label == vegetation) - f << "0 255 27" << std::endl; - else if (label == roof) - f << "255 0 170" << std::endl; - else { - f << "0 0 0" << std::endl; - std::cerr << "Error: unknown classification label" << std::endl; + r = 245; g = 180; b = 0; } + else if (label == vegetation) + { + r = 0; g = 255; b = 27; + } + else if (label == roof) + { + r = 255; g = 0; b = 170; + } + red.push_back(r); + green.push_back(g); + blue.push_back(b); } + std::ofstream f ("classification.ply"); + + CGAL::write_ply_points_with_properties + (f, CGAL::make_range (boost::counting_iterator(0), + boost::counting_iterator(pts.size())), + CGAL::make_ply_point_writer (CGAL::make_property_map(pts)), + std::make_pair(CGAL::make_property_map(red), CGAL::PLY_property("red")), + std::make_pair(CGAL::make_property_map(green), CGAL::PLY_property("green")), + std::make_pair(CGAL::make_property_map(blue), CGAL::PLY_property("blue"))); + + std::cerr << "All done" << std::endl; return EXIT_SUCCESS; } diff --git a/Classification/examples/Classification/example_cluster_classification.cpp b/Classification/examples/Classification/example_cluster_classification.cpp index 5d14cb8ccf6..e670b871935 100644 --- a/Classification/examples/Classification/example_cluster_classification.cpp +++ b/Classification/examples/Classification/example_cluster_classification.cpp @@ -18,8 +18,6 @@ #include #include -typedef CGAL::Parallel_if_available_tag Concurrency_tag; - typedef CGAL::Simple_cartesian Kernel; typedef Kernel::Point_3 Point; typedef Kernel::Iso_cuboid_3 Iso_cuboid_3; @@ -37,6 +35,7 @@ typedef CGAL::Shape_detection::Point_set::Least_squares_plane_fit_region Region_growing; namespace Classification = CGAL::Classification; +namespace Feature = CGAL::Classification::Feature; typedef Classification::Label_handle Label_handle; typedef Classification::Feature_handle Feature_handle; @@ -67,7 +66,7 @@ int main (int argc, char** argv) CGAL::Real_timer t; t.start(); pts.add_normal_map(); - CGAL::jet_estimate_normals (pts, 12); + CGAL::jet_estimate_normals (pts, 12); t.stop(); std::cerr << "Done in " << t.time() << " second(s)" << std::endl; t.reset(); @@ -151,50 +150,38 @@ int main (int argc, char** argv) Feature_set features; -#ifdef CGAL_LINKED_WITH_TBB - features.begin_parallel_additions(); -#endif - // First, compute means of features. - for (std::size_t i = 0; i < pointwise_features.size(); ++ i) - features.add (clusters, pointwise_features[i]); - -#ifdef CGAL_LINKED_WITH_TBB - features.end_parallel_additions(); features.begin_parallel_additions(); -#endif - + for (Feature_handle fh : pointwise_features) + features.add (clusters, fh); + features.end_parallel_additions(); + // Then, compute variances of features (and remaining cluster features). + features.begin_parallel_additions(); for (std::size_t i = 0; i < pointwise_features.size(); ++ i) - features.add (clusters, - pointwise_features[i], // i^th feature - features[i]); // mean of i^th feature + features.add (clusters, + pointwise_features[i], // i^th feature + features[i]); // mean of i^th feature - features.add (clusters); - features.add (clusters); + features.add (clusters); + features.add (clusters); for (std::size_t i = 0; i < 3; ++ i) - features.add (clusters, eigen, (unsigned int)(i)); + features.add (clusters, eigen, (unsigned int)(i)); -#ifdef CGAL_LINKED_WITH_TBB features.end_parallel_additions(); -#endif //! [Features] /////////////////////////////////////////////////////////////////// t.stop(); - // Add types. - Label_set labels; - Label_handle ground = labels.add ("ground"); - Label_handle vegetation = labels.add ("vegetation"); - Label_handle roof = labels.add ("roof"); + Label_set labels = { "ground", "vegetation", "roof" }; std::vector label_indices(clusters.size(), -1); std::cerr << "Using ETHZ Random Forest Classifier" << std::endl; - Classification::ETHZ_random_forest_classifier classifier (labels, features); + Classification::ETHZ::Random_forest_classifier classifier (labels, features); std::cerr << "Loading configuration" << std::endl; std::ifstream in_config (filename_config, std::ios_base::in | std::ios_base::binary); @@ -203,7 +190,7 @@ int main (int argc, char** argv) std::cerr << "Classifying" << std::endl; t.reset(); t.start(); - Classification::classify (clusters, labels, classifier, label_indices); + Classification::classify (clusters, labels, classifier, label_indices); t.stop(); std::cerr << "Classification done in " << t.time() << " second(s)" << std::endl; diff --git a/Classification/examples/Classification/example_ethz_random_forest.cpp b/Classification/examples/Classification/example_ethz_random_forest.cpp index 18403f25452..4e625e4381c 100644 --- a/Classification/examples/Classification/example_ethz_random_forest.cpp +++ b/Classification/examples/Classification/example_ethz_random_forest.cpp @@ -49,18 +49,13 @@ int main (int argc, char** argv) Imap label_map; bool lm_found = false; - boost::tie (label_map, lm_found) = pts.property_map ("label"); + std::tie (label_map, lm_found) = pts.property_map ("label"); if (!lm_found) { std::cerr << "Error: \"label\" property not found in input file." << std::endl; return EXIT_FAILURE; } - std::vector ground_truth; - ground_truth.reserve (pts.size()); - std::copy (pts.range(label_map).begin(), pts.range(label_map).end(), - std::back_inserter (ground_truth)); - Feature_set features; std::cerr << "Generating features" << std::endl; @@ -69,20 +64,14 @@ int main (int argc, char** argv) Feature_generator generator (pts, pts.point_map(), 5); // using 5 scales -#ifdef CGAL_LINKED_WITH_TBB features.begin_parallel_additions(); -#endif - generator.generate_point_based_features (features); - -#ifdef CGAL_LINKED_WITH_TBB features.end_parallel_additions(); -#endif t.stop(); std::cerr << "Done in " << t.time() << " second(s)" << std::endl; - // Add types + // Add labels Label_set labels; Label_handle ground = labels.add ("ground"); Label_handle vegetation = labels.add ("vegetation"); @@ -96,13 +85,13 @@ int main (int argc, char** argv) std::cerr << "Training" << std::endl; t.reset(); t.start(); - classifier.train (ground_truth); + classifier.train (pts.range(label_map)); t.stop(); std::cerr << "Done in " << t.time() << " second(s)" << std::endl; t.reset(); t.start(); - Classification::classify_with_graphcut + Classification::classify_with_graphcut (pts, pts.point_map(), labels, classifier, generator.neighborhood().k_neighbor_query(12), 0.2f, 1, label_indices); @@ -111,56 +100,21 @@ int main (int argc, char** argv) std::cerr << "Classification with graphcut done in " << t.time() << " second(s)" << std::endl; std::cerr << "Precision, recall, F1 scores and IoU:" << std::endl; - Classification::Evaluation evaluation (labels, ground_truth, label_indices); - - for (std::size_t i = 0; i < labels.size(); ++ i) + Classification::Evaluation evaluation (labels, pts.range(label_map), label_indices); + + for (Label_handle l : labels) { - std::cerr << " * " << labels[i]->name() << ": " - << evaluation.precision(labels[i]) << " ; " - << evaluation.recall(labels[i]) << " ; " - << evaluation.f1_score(labels[i]) << " ; " - << evaluation.intersection_over_union(labels[i]) << std::endl; + std::cerr << " * " << l->name() << ": " + << evaluation.precision(l) << " ; " + << evaluation.recall(l) << " ; " + << evaluation.f1_score(l) << " ; " + << evaluation.intersection_over_union(l) << std::endl; } std::cerr << "Accuracy = " << evaluation.accuracy() << std::endl << "Mean F1 score = " << evaluation.mean_f1_score() << std::endl << "Mean IoU = " << evaluation.mean_intersection_over_union() << std::endl; - { - std::ofstream out ("toto.bin", std::ios_base::binary); - classifier.save_configuration(out); - - Classification::ETHZ::Random_forest_classifier classifier_2 (labels, features); - std::ifstream in ("toto.bin", std::ios_base::binary); - classifier_2.load_configuration(in); - - t.reset(); - t.start(); - Classification::classify_with_graphcut - (pts, pts.point_map(), labels, classifier_2, - generator.neighborhood().k_neighbor_query(12), - 0.2f, 1, label_indices); - t.stop(); - - std::cerr << "Classification with graphcut done in " << t.time() << " second(s)" << std::endl; - - std::cerr << "Precision, recall, F1 scores and IoU:" << std::endl; - Classification::Evaluation evaluation (labels, ground_truth, label_indices); - - for (std::size_t i = 0; i < labels.size(); ++ i) - { - std::cerr << " * " << labels[i]->name() << ": " - << evaluation.precision(labels[i]) << " ; " - << evaluation.recall(labels[i]) << " ; " - << evaluation.f1_score(labels[i]) << " ; " - << evaluation.intersection_over_union(labels[i]) << std::endl; - } - - std::cerr << "Accuracy = " << evaluation.accuracy() << std::endl - << "Mean F1 score = " << evaluation.mean_f1_score() << std::endl - << "Mean IoU = " << evaluation.mean_intersection_over_union() << std::endl; - } - // Color point set according to class UCmap red = pts.add_property_map("red", 0).first; UCmap green = pts.add_property_map("green", 0).first; @@ -186,6 +140,10 @@ int main (int argc, char** argv) } } + // Save configuration for later use + std::ofstream fconfig ("ethz_random_forest.bin", std::ios_base::binary); + classifier.save_configuration(fconfig); + // Write result std::ofstream f ("classification.ply"); f.precision(18); diff --git a/Classification/examples/Classification/example_feature.cpp b/Classification/examples/Classification/example_feature.cpp index 4cce763be1a..b8b996e04a0 100644 --- a/Classification/examples/Classification/example_feature.cpp +++ b/Classification/examples/Classification/example_feature.cpp @@ -115,7 +115,7 @@ int main (int argc, char** argv) std::cerr << "Classifying" << std::endl; std::vector label_indices(pts.size(), -1); - Classification::classify_with_graphcut + Classification::classify_with_graphcut (pts, Pmap(), labels, classifier, neighborhood.k_neighbor_query(12), 0.5, 1, label_indices); diff --git a/Classification/examples/Classification/example_generation_and_training.cpp b/Classification/examples/Classification/example_generation_and_training.cpp index 1b9aa0138e9..69dd8627b64 100644 --- a/Classification/examples/Classification/example_generation_and_training.cpp +++ b/Classification/examples/Classification/example_generation_and_training.cpp @@ -46,18 +46,13 @@ int main (int argc, char** argv) Imap label_map; bool lm_found = false; - boost::tie (label_map, lm_found) = pts.property_map ("label"); + std::tie (label_map, lm_found) = pts.property_map ("label"); if (!lm_found) { std::cerr << "Error: \"label\" property not found in input file." << std::endl; return EXIT_FAILURE; } - std::vector ground_truth; - ground_truth.reserve (pts.size()); - std::copy (pts.range(label_map).begin(), pts.range(label_map).end(), - std::back_inserter (ground_truth)); - std::cerr << "Generating features" << std::endl; CGAL::Real_timer t; t.start(); @@ -70,15 +65,9 @@ int main (int argc, char** argv) std::size_t number_of_scales = 5; Feature_generator generator (pts, pts.point_map(), number_of_scales); -#ifdef CGAL_LINKED_WITH_TBB features.begin_parallel_additions(); -#endif - generator.generate_point_based_features (features); - -#ifdef CGAL_LINKED_WITH_TBB features.end_parallel_additions(); -#endif //! [Generator] /////////////////////////////////////////////////////////////////// @@ -86,25 +75,21 @@ int main (int argc, char** argv) t.stop(); std::cerr << features.size() << " feature(s) generated in " << t.time() << " second(s)" << std::endl; - // Add types - Label_set labels; - Label_handle ground = labels.add ("ground"); - Label_handle vegetation = labels.add ("vegetation"); - Label_handle roof = labels.add ("roof"); + Label_set labels = { "ground", "vegetation", "roof" }; Classifier classifier (labels, features); std::cerr << "Training" << std::endl; t.reset(); t.start(); - classifier.train (ground_truth, 800); + classifier.train (pts.range(label_map), 800); t.stop(); std::cerr << "Done in " << t.time() << " second(s)" << std::endl; t.reset(); t.start(); std::vector label_indices(pts.size(), -1); - Classification::classify_with_graphcut + Classification::classify_with_graphcut (pts, pts.point_map(), labels, classifier, generator.neighborhood().k_neighbor_query(12), 0.2f, 10, label_indices); @@ -112,15 +97,15 @@ int main (int argc, char** argv) std::cerr << "Classification with graphcut done in " << t.time() << " second(s)" << std::endl; std::cerr << "Precision, recall, F1 scores and IoU:" << std::endl; - Classification::Evaluation evaluation (labels, ground_truth, label_indices); + Classification::Evaluation evaluation (labels, pts.range(label_map), label_indices); - for (std::size_t i = 0; i < labels.size(); ++ i) + for (Label_handle l : labels) { - std::cerr << " * " << labels[i]->name() << ": " - << evaluation.precision(labels[i]) << " ; " - << evaluation.recall(labels[i]) << " ; " - << evaluation.f1_score(labels[i]) << " ; " - << evaluation.intersection_over_union(labels[i]) << std::endl; + std::cerr << " * " << l->name() << ": " + << evaluation.precision(l) << " ; " + << evaluation.recall(l) << " ; " + << evaluation.f1_score(l) << " ; " + << evaluation.intersection_over_union(l) << std::endl; } std::cerr << "Accuracy = " << evaluation.accuracy() << std::endl diff --git a/Classification/examples/Classification/example_mesh_classification.cpp b/Classification/examples/Classification/example_mesh_classification.cpp index c569ad0f03c..fcbb7d88baa 100644 --- a/Classification/examples/Classification/example_mesh_classification.cpp +++ b/Classification/examples/Classification/example_mesh_classification.cpp @@ -59,16 +59,10 @@ int main (int argc, char** argv) std::size_t number_of_scales = 5; Feature_generator generator (mesh, face_point_map, number_of_scales); -#ifdef CGAL_LINKED_WITH_TBB features.begin_parallel_additions(); -#endif - generator.generate_point_based_features (features); // Features that consider the mesh as a point set generator.generate_face_based_features (features); // Features computed directly on mesh faces - -#ifdef CGAL_LINKED_WITH_TBB features.end_parallel_additions(); -#endif //! [Generator] /////////////////////////////////////////////////////////////////// @@ -76,11 +70,7 @@ int main (int argc, char** argv) t.stop(); std::cerr << "Done in " << t.time() << " second(s)" << std::endl; - // Add types - Label_set labels; - Label_handle ground = labels.add ("ground"); - Label_handle vegetation = labels.add ("vegetation"); - Label_handle roof = labels.add ("roof"); + Label_set labels = { "ground", "vegetation", "roof" }; std::vector label_indices(mesh.number_of_faces(), -1); @@ -94,7 +84,7 @@ int main (int argc, char** argv) std::cerr << "Classifying with graphcut" << std::endl; t.reset(); t.start(); - Classification::classify_with_graphcut + Classification::classify_with_graphcut (mesh.faces(), Face_with_bbox_map(&mesh), labels, classifier, generator.neighborhood().n_ring_neighbor_query(2), 0.2f, 1, label_indices); diff --git a/Classification/examples/Classification/example_opencv_random_forest.cpp b/Classification/examples/Classification/example_opencv_random_forest.cpp index 4ada5c5182a..f368ebb9485 100644 --- a/Classification/examples/Classification/example_opencv_random_forest.cpp +++ b/Classification/examples/Classification/example_opencv_random_forest.cpp @@ -49,18 +49,13 @@ int main (int argc, char** argv) Imap label_map; bool lm_found = false; - boost::tie (label_map, lm_found) = pts.property_map ("label"); + std::tie (label_map, lm_found) = pts.property_map ("label"); if (!lm_found) { std::cerr << "Error: \"label\" property not found in input file." << std::endl; return EXIT_FAILURE; } - std::vector ground_truth; - ground_truth.reserve (pts.size()); - std::copy (pts.range(label_map).begin(), pts.range(label_map).end(), - std::back_inserter (ground_truth)); - Feature_set features; std::cerr << "Generating features" << std::endl; @@ -69,19 +64,14 @@ int main (int argc, char** argv) Feature_generator generator (pts, pts.point_map(), 5); // using 5 scales -#ifdef CGAL_LINKED_WITH_TBB features.begin_parallel_additions(); -#endif - generator.generate_point_based_features (features); -#ifdef CGAL_LINKED_WITH_TBB features.end_parallel_additions(); -#endif t.stop(); std::cerr << "Done in " << t.time() << " second(s)" << std::endl; - // Add types + // Add labels Label_set labels; Label_handle ground = labels.add ("ground"); Label_handle vegetation = labels.add ("vegetation"); @@ -95,13 +85,13 @@ int main (int argc, char** argv) std::cerr << "Training" << std::endl; t.reset(); t.start(); - classifier.train (ground_truth); + classifier.train (pts.range(label_map)); t.stop(); std::cerr << "Done in " << t.time() << " second(s)" << std::endl; t.reset(); t.start(); - Classification::classify_with_graphcut + Classification::classify_with_graphcut (pts, pts.point_map(), labels, classifier, generator.neighborhood().k_neighbor_query(12), 0.2f, 1, label_indices); @@ -110,15 +100,15 @@ int main (int argc, char** argv) std::cerr << "Classification with graphcut done in " << t.time() << " second(s)" << std::endl; std::cerr << "Precision, recall, F1 scores and IoU:" << std::endl; - Classification::Evaluation evaluation (labels, ground_truth, label_indices); + Classification::Evaluation evaluation (labels, pts.range(label_map), label_indices); - for (std::size_t i = 0; i < labels.size(); ++ i) + for (Label_handle l : labels) { - std::cerr << " * " << labels[i]->name() << ": " - << evaluation.precision(labels[i]) << " ; " - << evaluation.recall(labels[i]) << " ; " - << evaluation.f1_score(labels[i]) << " ; " - << evaluation.intersection_over_union(labels[i]) << std::endl; + std::cerr << " * " << l->name() << ": " + << evaluation.precision(l) << " ; " + << evaluation.recall(l) << " ; " + << evaluation.f1_score(l) << " ; " + << evaluation.intersection_over_union(l) << std::endl; } std::cerr << "Accuracy = " << evaluation.accuracy() << std::endl diff --git a/Classification/examples/Classification/example_tensorflow_neural_network.cpp b/Classification/examples/Classification/example_tensorflow_neural_network.cpp index d86650de182..9779f201d68 100644 --- a/Classification/examples/Classification/example_tensorflow_neural_network.cpp +++ b/Classification/examples/Classification/example_tensorflow_neural_network.cpp @@ -49,18 +49,13 @@ int main (int argc, char** argv) Imap label_map; bool lm_found = false; - boost::tie (label_map, lm_found) = pts.property_map ("label"); + std::tie (label_map, lm_found) = pts.property_map ("label"); if (!lm_found) { std::cerr << "Error: \"label\" property not found in input file." << std::endl; return EXIT_FAILURE; } - std::vector ground_truth; - ground_truth.reserve (pts.size()); - std::copy (pts.range(label_map).begin(), pts.range(label_map).end(), - std::back_inserter (ground_truth)); - Feature_set features; std::cerr << "Generating features" << std::endl; @@ -69,20 +64,14 @@ int main (int argc, char** argv) Feature_generator generator (pts, pts.point_map(), 5); // using 5 scales -#ifdef CGAL_LINKED_WITH_TBB features.begin_parallel_additions(); -#endif - generator.generate_point_based_features (features); - -#ifdef CGAL_LINKED_WITH_TBB features.end_parallel_additions(); -#endif t.stop(); std::cerr << "Done in " << t.time() << " second(s)" << std::endl; - // Add types + // Add labels Label_set labels; Label_handle ground = labels.add ("ground"); Label_handle vegetation = labels.add ("vegetation"); @@ -96,7 +85,7 @@ int main (int argc, char** argv) std::cerr << "Training" << std::endl; t.reset(); t.start(); - classifier.train (ground_truth, + classifier.train (pts.range(ground_truth), true, // restart from scratch 100); // 100 iterations t.stop(); @@ -113,15 +102,15 @@ int main (int argc, char** argv) std::cerr << "Classification with graphcut done in " << t.time() << " second(s)" << std::endl; std::cerr << "Precision, recall, F1 scores and IoU:" << std::endl; - Classification::Evaluation evaluation (labels, ground_truth, label_indices); + Classification::Evaluation evaluation (labels, pts.range(ground_truth), label_indices); - for (std::size_t i = 0; i < labels.size(); ++ i) + for (Label_handle l : labels) { - std::cerr << " * " << labels[i]->name() << ": " - << evaluation.precision(labels[i]) << " ; " - << evaluation.recall(labels[i]) << " ; " - << evaluation.f1_score(labels[i]) << " ; " - << evaluation.intersection_over_union(labels[i]) << std::endl; + std::cerr << " * " << l->name() << ": " + << evaluation.precision(l) << " ; " + << evaluation.recall(l) << " ; " + << evaluation.f1_score(l) << " ; " + << evaluation.intersection_over_union(l) << std::endl; } std::cerr << "Accuracy = " << evaluation.accuracy() << std::endl From f4e52069a5bb2a0238d0f8f1233f7b81fa8f66e3 Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Thu, 2 Apr 2020 08:30:13 +0200 Subject: [PATCH 14/79] Fix CMakeLists --- Classification/examples/Classification/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classification/examples/Classification/CMakeLists.txt b/Classification/examples/Classification/CMakeLists.txt index 699084be8f1..57a1d5d63c3 100644 --- a/Classification/examples/Classification/CMakeLists.txt +++ b/Classification/examples/Classification/CMakeLists.txt @@ -81,7 +81,7 @@ foreach(target example_mesh_classification example_cluster_classification example_opencv_random_forest - example_tensorflow_neural_network) + example_tensorflow_neural_network example_deprecated_conversion) if(TARGET ${target}) CGAL_target_use_Eigen(${target}) From 43014baa918702e52339361e8b1eed10e3f8d43f Mon Sep 17 00:00:00 2001 From: Simon Giraudot Date: Thu, 2 Apr 2020 10:28:50 +0200 Subject: [PATCH 15/79] Modernize/refresh code with C++11/14 --- .../Classification/example_classification.cpp | 26 +-- .../include/CGAL/Classification/Cluster.h | 12 +- .../ETHZ/Random_forest_classifier.h | 69 +++----- .../ETHZ/internal/random-forest/forest.hpp | 26 +-- .../Feature/Distance_to_plane.h | 3 +- .../Classification/Feature/Echo_scatter.h | 4 +- .../CGAL/Classification/Feature/Elevation.h | 8 +- .../Classification/Feature/Height_above.h | 6 +- .../Classification/Feature/Height_below.h | 2 - .../Feature/Vertical_dispersion.h | 4 +- .../Classification/Feature/Vertical_range.h | 6 +- .../CGAL/Classification/Feature_base.h | 33 ++-- .../include/CGAL/Classification/Feature_set.h | 115 ++++++------ .../include/CGAL/Classification/Image.h | 24 +-- .../include/CGAL/Classification/Label.h | 4 +- .../include/CGAL/Classification/Label_set.h | 36 ++-- .../Classification/Local_eigen_analysis.h | 62 +++---- .../Classification/Mesh_feature_generator.h | 122 ++++--------- .../CGAL/Classification/Mesh_neighborhood.h | 25 ++- .../CGAL/Classification/Planimetric_grid.h | 16 +- .../Point_set_feature_generator.h | 166 ++++-------------- .../Classification/Point_set_neighborhood.h | 84 ++++----- .../Sum_of_weighted_features_classifier.h | 84 ++++----- .../include/CGAL/Classification/classify.h | 16 +- 24 files changed, 370 insertions(+), 583 deletions(-) diff --git a/Classification/examples/Classification/example_classification.cpp b/Classification/examples/Classification/example_classification.cpp index e3800008b5d..07b1147b47d 100644 --- a/Classification/examples/Classification/example_classification.cpp +++ b/Classification/examples/Classification/example_classification.cpp @@ -73,7 +73,7 @@ int main (int argc, char** argv) //! [Analysis] /////////////////////////////////////////////////////////////////// - + /////////////////////////////////////////////////////////////////// //! [Features] @@ -84,7 +84,7 @@ int main (int argc, char** argv) Feature_set features; features.begin_parallel_additions(); - + Feature_handle distance_to_plane = features.add (pts, Pmap(), eigen); Feature_handle dispersion = features.add (pts, Pmap(), grid, radius_neighbors); @@ -92,7 +92,7 @@ int main (int argc, char** argv) radius_dtm); features.end_parallel_additions(); - + //! [Features] /////////////////////////////////////////////////////////////////// @@ -106,7 +106,7 @@ int main (int argc, char** argv) //! [Labels] /////////////////////////////////////////////////////////////////// - + /////////////////////////////////////////////////////////////////// //! [Weights] @@ -115,12 +115,12 @@ int main (int argc, char** argv) classifier.set_weight (distance_to_plane, 6.75e-2f); classifier.set_weight (dispersion, 5.45e-1f); classifier.set_weight (elevation, 1.47e1f); - + std::cerr << "Setting effects" << std::endl; classifier.set_effect (ground, distance_to_plane, Classifier::NEUTRAL); classifier.set_effect (ground, dispersion, Classifier::NEUTRAL); classifier.set_effect (ground, elevation, Classifier::PENALIZING); - + classifier.set_effect (vegetation, distance_to_plane, Classifier::FAVORING); classifier.set_effect (vegetation, dispersion, Classifier::FAVORING); classifier.set_effect (vegetation, elevation, Classifier::NEUTRAL); @@ -138,7 +138,7 @@ int main (int argc, char** argv) /////////////////////////////////////////////////////////////////// //! [Classify] std::vector label_indices (pts.size(), -1); - + CGAL::Real_timer t; t.start(); Classification::classify (pts, labels, classifier, label_indices); @@ -147,7 +147,7 @@ int main (int argc, char** argv) t.reset(); //! [Classify] /////////////////////////////////////////////////////////////////// - + /////////////////////////////////////////////////////////////////// //! [Smoothing] t.start(); @@ -172,14 +172,14 @@ int main (int argc, char** argv) std::cerr << "Classification with graphcut performed in " << t.time() << " second(s)" << std::endl; //! [Graph_cut] /////////////////////////////////////////////////////////////////// - + // Save the output in a colored PLY format std::vector red, green, blue; red.reserve(pts.size()); green.reserve(pts.size()); blue.reserve(pts.size()); - + for (std::size_t i = 0; i < pts.size(); ++ i) { Label_handle label = labels[std::size_t(label_indices[i])]; @@ -200,7 +200,7 @@ int main (int argc, char** argv) green.push_back(g); blue.push_back(b); } - + std::ofstream f ("classification.ply"); CGAL::write_ply_points_with_properties @@ -210,8 +210,8 @@ int main (int argc, char** argv) std::make_pair(CGAL::make_property_map(red), CGAL::PLY_property("red")), std::make_pair(CGAL::make_property_map(green), CGAL::PLY_property("green")), std::make_pair(CGAL::make_property_map(blue), CGAL::PLY_property("blue"))); - - + + std::cerr << "All done" << std::endl; return EXIT_SUCCESS; } diff --git a/Classification/include/CGAL/Classification/Cluster.h b/Classification/include/CGAL/Classification/Cluster.h index 4b8b002c3b5..a0d0f209602 100644 --- a/Classification/include/CGAL/Classification/Cluster.h +++ b/Classification/include/CGAL/Classification/Cluster.h @@ -46,7 +46,7 @@ class Cluster { public: - typedef typename ItemMap::value_type Item; + using Item = typename boost::property_traits::value_type; /// \cond SKIP_IN_MANUAL struct Neighbor_query @@ -64,9 +64,9 @@ public: class Point_idx_to_point_unary_function { public: - typedef std::size_t argument_type; - typedef typename ItemMap::reference result_type; - typedef boost::readable_property_map_tag category; + using argument_type = std::size_t; + using result_type = typename boost::property_traits::reference; + using category = boost::readable_property_map_tag; const ItemRange* m_range; ItemMap m_item_map; @@ -105,9 +105,9 @@ public: \param item_map property map to access the input items. */ Cluster (const ItemRange& range, ItemMap item_map) - : neighbors (new std::vector()) + : neighbors (std::make_shared >()) , m_range (&range), m_item_map (item_map) - , m_inliers (new std::vector()) + , m_inliers (std::make_shared >()) , m_training(-1), m_label(-1) { } diff --git a/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h b/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h index b55d098367d..e741a2cc694 100644 --- a/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h +++ b/Classification/include/CGAL/Classification/ETHZ/Random_forest_classifier.h @@ -68,25 +68,25 @@ class Random_forest_classifier typedef CGAL::internal::liblearning::RandomForest::RandomForest < CGAL::internal::liblearning::RandomForest::NodeGini < CGAL::internal::liblearning::RandomForest::AxisAlignedSplitter> > Forest; - + const Label_set& m_labels; const Feature_set& m_features; - Forest* m_rfc; + std::shared_ptr m_rfc; public: - + /// \name Constructor /// @{ - + /*! \brief Instantiates the classifier using the sets of `labels` and `features`. */ Random_forest_classifier (const Label_set& labels, const Feature_set& features) - : m_labels (labels), m_features (features), m_rfc (nullptr) + : m_labels (labels), m_features (features) { } - + /*! \brief Copies the `other` classifier's configuration using another set of `features`. @@ -100,21 +100,13 @@ public: */ Random_forest_classifier (const Random_forest_classifier& other, const Feature_set& features) - : m_labels (other.m_labels), m_features (features), m_rfc (nullptr) + : m_labels (other.m_labels), m_features (features) { std::stringstream stream; other.save_configuration(stream); this->load_configuration(stream); } - - /// \cond SKIP_IN_MANUAL - ~Random_forest_classifier () - { - if (m_rfc != nullptr) - delete m_rfc; - } - /// \endcond - + /// @} /// \name Training @@ -130,7 +122,7 @@ public: train(ground_truth, reset_trees, num_trees, max_depth); } /// \endcond - + /*! \brief Runs the training algorithm. @@ -206,20 +198,17 @@ public: std::cerr << " * " << m_labels[i]->name() << ": " << count[i] << " inlier(s)" << std::endl; #endif - CGAL::internal::liblearning::DataView2D label_vector (&(gt[0]), gt.size(), 1); + CGAL::internal::liblearning::DataView2D label_vector (&(gt[0]), gt.size(), 1); CGAL::internal::liblearning::DataView2D feature_vector(&(ft[0]), gt.size(), ft.size() / gt.size()); - if (m_rfc != nullptr && reset_trees) - { - delete m_rfc; - m_rfc = nullptr; - } - - if (m_rfc == nullptr) - m_rfc = new Forest (params); + if (m_rfc && reset_trees) + m_rfc.reset(); + + if (!m_rfc) + m_rfc = std::make_shared (params); CGAL::internal::liblearning::RandomForest::AxisAlignedRandomSplitGenerator generator; - + m_rfc->train (feature_vector, label_vector, CGAL::internal::liblearning::DataView2D(), generator, 0, reset_trees, m_labels.size()); } @@ -228,7 +217,7 @@ public: void operator() (std::size_t item_index, std::vector& out) const { out.resize (m_labels.size(), 0.); - + std::vector ft; ft.reserve (m_features.size()); for (std::size_t f = 0; f < m_features.size(); ++ f) @@ -237,18 +226,18 @@ public: std::vector prob (m_labels.size()); m_rfc->evaluate (ft.data(), prob.data()); - + for (std::size_t i = 0; i < out.size(); ++ i) out[i] = (std::min) (1.f, (std::max) (0.f, prob[i])); } /// \endcond - + /// @} /// \name Miscellaneous /// @{ - + /*! \brief Computes, for each feature, how many nodes in the forest uses it as a split criterion. @@ -278,12 +267,12 @@ public: count.resize(m_features.size(), 0); return m_rfc->get_feature_usage(count); } - + /// @} /// \name Input/Output /// @{ - + /*! \brief Saves the current configuration in the stream `output`. @@ -297,7 +286,7 @@ public: { m_rfc->write(output); } - + /*! \brief Loads a configuration from the stream `input`. @@ -316,18 +305,16 @@ public: void load_configuration (std::istream& input) { CGAL::internal::liblearning::RandomForest::ForestParams params; - if (m_rfc != nullptr) - delete m_rfc; - m_rfc = new Forest (params); + m_rfc = std::make_shared (params); m_rfc->read(input); } - + /// @} /// \name Deprecated Input/Output /// @{ - + /*! \brief Converts a deprecated GZ configuration to a new BIN configuration. @@ -351,9 +338,7 @@ public: void load_deprecated_configuration (std::istream& input) { CGAL::internal::liblearning::RandomForest::ForestParams params; - if (m_rfc != nullptr) - delete m_rfc; - m_rfc = new Forest (params); + m_rfc = std::make_shared (params); boost::iostreams::filtering_istream ins; ins.push(boost::iostreams::gzip_decompressor()); diff --git a/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/forest.hpp b/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/forest.hpp index 4f62e67a16b..baf9fa9f109 100644 --- a/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/forest.hpp +++ b/Classification/include/CGAL/Classification/ETHZ/internal/random-forest/forest.hpp @@ -55,7 +55,7 @@ class Tree_training_functor typedef typename NodeT::ParamType ParamType; typedef typename NodeT::FeatureType FeatureType; typedef Tree TreeType; - + std::size_t seed_start; const std::vector& sample_idxes; boost::ptr_vector >& trees; @@ -63,7 +63,7 @@ class Tree_training_functor DataView2D labels; std::size_t n_in_bag_samples; const SplitGenerator& split_generator; - + public: Tree_training_functor(std::size_t seed_start, @@ -81,7 +81,7 @@ public: , n_in_bag_samples(n_in_bag_samples) , split_generator(split_generator) { } - + #ifdef CGAL_LINKED_WITH_TBB void operator()(const tbb::blocked_range& r) const { @@ -89,7 +89,7 @@ public: apply(s); } #endif // CGAL_LINKED_WITH_TBB - + inline void apply (std::size_t i_tree) const { // initialize random generator with sequential seeds (one for each @@ -120,14 +120,14 @@ public: RandomForest(ParamType const& params) : params(params) {} template - void train(DataView2D samples, - DataView2D labels, - DataView2D train_sample_idxes, + void train(DataView2D samples, + DataView2D labels, + DataView2D train_sample_idxes, SplitGenerator const& split_generator, size_t seed_start = 1, bool reset_trees = true, std::size_t n_classes = std::size_t(-1) - ) + ) { if (reset_trees) trees.clear(); @@ -136,7 +136,7 @@ public: params.n_classes = *std::max_element(&labels(0,0), &labels(0,0)+labels.num_elements()) + 1; else params.n_classes = n_classes; - + params.n_features = samples.cols; params.n_samples = samples.rows; @@ -159,15 +159,15 @@ public: std::size_t nb_trees = trees.size(); for (std::size_t i_tree = nb_trees; i_tree < nb_trees + params.n_trees; ++ i_tree) trees.push_back (new TreeType(¶ms)); - + Tree_training_functor f (seed_start, sample_idxes, trees, samples, labels, params.n_in_bag_samples, split_generator); #ifndef CGAL_LINKED_WITH_TBB - CGAL_static_assertion_msg (!(boost::is_convertible::value), + CGAL_static_assertion_msg (!(std::is_convertible::value), "Parallel_tag is enabled but TBB is unavailable."); #else - if (boost::is_convertible::value) + if (std::is_convertible::value) { tbb::parallel_for(tbb::blocked_range(nb_trees, nb_trees + params.n_trees), f); } @@ -250,7 +250,7 @@ public: { trees.push_back (new TreeType(¶ms)); trees.back().read(is); - } + } } void get_feature_usage (std::vector& count) const diff --git a/Classification/include/CGAL/Classification/Feature/Distance_to_plane.h b/Classification/include/CGAL/Classification/Feature/Distance_to_plane.h index 19b6b83a959..7c78a116e8d 100644 --- a/Classification/include/CGAL/Classification/Feature/Distance_to_plane.h +++ b/Classification/include/CGAL/Classification/Feature/Distance_to_plane.h @@ -46,8 +46,7 @@ namespace Feature { template class Distance_to_plane : public Feature_base { - - typedef typename CGAL::Kernel_traits::Kernel Kernel; + using Kernel = typename CGAL::Kernel_traits::Kernel; #ifdef CGAL_CLASSIFICATION_PRECOMPUTE_FEATURES std::vector distance_to_plane_feature; diff --git a/Classification/include/CGAL/Classification/Feature/Echo_scatter.h b/Classification/include/CGAL/Classification/Feature/Echo_scatter.h index b2206910102..c0165abba33 100644 --- a/Classification/include/CGAL/Classification/Feature/Echo_scatter.h +++ b/Classification/include/CGAL/Classification/Feature/Echo_scatter.h @@ -52,9 +52,9 @@ template Grid; + using Grid = Classification::Planimetric_grid; private: - typedef Classification::Image Image_cfloat; + using Image_cfloat = Classification::Image; const Grid& grid; Image_cfloat Scatter; diff --git a/Classification/include/CGAL/Classification/Feature/Elevation.h b/Classification/include/CGAL/Classification/Feature/Elevation.h index e4158800ef2..0803409a8cc 100644 --- a/Classification/include/CGAL/Classification/Feature/Elevation.h +++ b/Classification/include/CGAL/Classification/Feature/Elevation.h @@ -50,11 +50,9 @@ namespace Feature { template class Elevation : public Feature_base { - typedef typename GeomTraits::Iso_cuboid_3 Iso_cuboid_3; - - typedef Image Image_float; - typedef Image Image_cfloat; - typedef Planimetric_grid Grid; + using Image_float = Image; + using Image_cfloat = Image; + using Grid = Planimetric_grid; const PointRange& input; PointMap point_map; diff --git a/Classification/include/CGAL/Classification/Feature/Height_above.h b/Classification/include/CGAL/Classification/Feature/Height_above.h index a4cec436241..5fafb70b682 100644 --- a/Classification/include/CGAL/Classification/Feature/Height_above.h +++ b/Classification/include/CGAL/Classification/Feature/Height_above.h @@ -49,10 +49,8 @@ namespace Feature { template class Height_above : public Feature_base { - typedef typename GeomTraits::Iso_cuboid_3 Iso_cuboid_3; - - typedef Image Image_float; - typedef Planimetric_grid Grid; + using Image_float = Image; + using Grid = Planimetric_grid; const PointRange& input; PointMap point_map; diff --git a/Classification/include/CGAL/Classification/Feature/Height_below.h b/Classification/include/CGAL/Classification/Feature/Height_below.h index 421b7622c79..4ffddef5af1 100644 --- a/Classification/include/CGAL/Classification/Feature/Height_below.h +++ b/Classification/include/CGAL/Classification/Feature/Height_below.h @@ -49,8 +49,6 @@ namespace Feature { template class Height_below : public Feature_base { - typedef typename GeomTraits::Iso_cuboid_3 Iso_cuboid_3; - typedef Image Image_float; typedef Planimetric_grid Grid; diff --git a/Classification/include/CGAL/Classification/Feature/Vertical_dispersion.h b/Classification/include/CGAL/Classification/Feature/Vertical_dispersion.h index c173f70acca..30557493827 100644 --- a/Classification/include/CGAL/Classification/Feature/Vertical_dispersion.h +++ b/Classification/include/CGAL/Classification/Feature/Vertical_dispersion.h @@ -56,8 +56,8 @@ namespace Feature { template class Vertical_dispersion : public Feature_base { - typedef Classification::Image Image_cfloat; - typedef Classification::Planimetric_grid Grid; + using Image_cfloat = Classification::Image; + using Grid = Classification::Planimetric_grid; const Grid& grid; Image_cfloat Dispersion; diff --git a/Classification/include/CGAL/Classification/Feature/Vertical_range.h b/Classification/include/CGAL/Classification/Feature/Vertical_range.h index 759fd4a274e..28f66c8e755 100644 --- a/Classification/include/CGAL/Classification/Feature/Vertical_range.h +++ b/Classification/include/CGAL/Classification/Feature/Vertical_range.h @@ -49,10 +49,8 @@ namespace Feature { template class Vertical_range : public Feature_base { - typedef typename GeomTraits::Iso_cuboid_3 Iso_cuboid_3; - - typedef Image Image_float; - typedef Planimetric_grid Grid; + using Image_float = Image; + using Grid = Planimetric_grid; const PointRange& input; PointMap point_map; diff --git a/Classification/include/CGAL/Classification/Feature_base.h b/Classification/include/CGAL/Classification/Feature_base.h index cd87f4a964b..8b243e63086 100644 --- a/Classification/include/CGAL/Classification/Feature_base.h +++ b/Classification/include/CGAL/Classification/Feature_base.h @@ -14,14 +14,14 @@ #include -#include +#include #include namespace CGAL { namespace Classification { - + /*! \ingroup PkgClassificationFeature @@ -33,7 +33,7 @@ namespace Classification { class Feature_base { std::string m_name; - + public: /// \cond SKIP_IN_MANUAL @@ -51,7 +51,7 @@ public: \brief Changes the name of the feature. */ void set_name (const std::string& name) { m_name = name; } - + /*! \brief Returns the value taken by the feature for at the item for the item at position `index`. This method must be implemented by @@ -72,27 +72,30 @@ public: */ class Feature_handle { }; #else -//typedef boost::shared_ptr Feature_handle; class Feature_set; - + class Feature_handle { friend Feature_set; - - boost::shared_ptr > m_base; - template - Feature_handle (Feature* f) : m_base (new boost::shared_ptr(f)) { } + using Feature_base_ptr = std::unique_ptr; + std::shared_ptr m_base; - template - void attach (Feature* f) const + template + Feature_handle (Feature_ptr f) + : m_base (std::make_shared(std::move(f))) { - *m_base = boost::shared_ptr(f); + } + + template + void attach (Feature_ptr f) + { + *m_base = std::move(f); } public: - Feature_handle() : m_base (new boost::shared_ptr()) { } + Feature_handle() : m_base (std::make_shared()) { } Feature_base& operator*() { return **m_base; } @@ -104,7 +107,7 @@ public: bool operator< (const Feature_handle& other) const { return *m_base < *(other.m_base); } bool operator== (const Feature_handle& other) const { return *m_base == *(other.m_base); } }; - + #endif template diff --git a/Classification/include/CGAL/Classification/Feature_set.h b/Classification/include/CGAL/Classification/Feature_set.h index fcf5bb6fdae..dfa58152a52 100644 --- a/Classification/include/CGAL/Classification/Feature_set.h +++ b/Classification/include/CGAL/Classification/Feature_set.h @@ -16,15 +16,13 @@ #include -#include - #ifdef CGAL_LINKED_WITH_TBB -#include #include #endif // CGAL_LINKED_WITH_TBB #include #include +#include namespace CGAL { @@ -40,7 +38,7 @@ the addition and the deletion of features. */ class Feature_set { - typedef std::vector Base; + using Base = std::vector; Base m_features; struct Compare_name @@ -52,19 +50,19 @@ class Feature_set return a->name() < b->name(); } }; - + #ifdef CGAL_LINKED_WITH_TBB - tbb::task_group* m_tasks; + std::unique_ptr m_tasks; #endif // CGAL_LINKED_WITH_TBB - + public: #ifdef DOXYGEN_RUNNING - typedef unspecified_type const_iterator; ///< A random access iterator with value type `Feature_handle`. - typedef unspecified_type iterator; ///< A random access iterator with value type `Feature_handle`. + using const_iterator = unspecified_type; ///< A random access iterator with value type `Feature_handle`. + using iterator = unspecified_type; ///< A random access iterator with value type `Feature_handle`. #else - typedef std::vector::const_iterator const_iterator; - typedef std::vector::iterator iterator; + using const_iterator = std::vector::const_iterator; + using iterator = std::vector::iterator; #endif /// \name Constructor @@ -74,28 +72,13 @@ public: \brief Creates an empty feature set. */ Feature_set() -#ifdef CGAL_LINKED_WITH_TBB - : m_tasks(nullptr) -#endif { } /// @} - - /// \cond SKIP_IN_MANUAL - virtual ~Feature_set() - { -#ifdef CGAL_LINKED_WITH_TBB - if (m_tasks != nullptr) - delete m_tasks; - for (std::size_t i = 0; i < m_adders.size(); ++ i) - delete m_adders[i]; -#endif - } - /// \endcond /// \name Modifications /// @{ - + /*! \brief Instantiates a new feature and adds it to the set. @@ -121,20 +104,21 @@ public: Feature_handle add (T&& ... t) { #ifdef CGAL_LINKED_WITH_TBB - if (m_tasks != nullptr) + if (m_tasks) { m_features.push_back (Feature_handle()); - - Parallel_feature_adder* adder - = new Parallel_feature_adder(m_features.back(), std::forward(t)...); - - m_adders.push_back (adder); + + Parallel_feature_adder_ptr adder + = std::make_unique > + (m_features.back(), std::forward(t)...); m_tasks->run (*adder); + + m_adders.emplace_back (std::move (adder)); } else #endif { - m_features.push_back (Feature_handle (new Feature(std::forward(t)...))); + m_features.push_back (Feature_handle (std::make_unique(std::forward(t)...))); } return m_features.back(); } @@ -144,27 +128,28 @@ public: Feature_handle add_with_scale_id (std::size_t i, T&& ... t) { #ifdef CGAL_LINKED_WITH_TBB - if (m_tasks != nullptr) + if (m_tasks) { m_features.push_back (Feature_handle()); - - Parallel_feature_adder* adder - = new Parallel_feature_adder(i, m_features.back(), std::forward(t)...); - - m_adders.push_back (adder); + + Parallel_feature_adder_ptr adder + = std::make_unique > + (i, m_features.back(), std::forward(t)...); m_tasks->run (*adder); + + m_adders.emplace_back (std::move (adder)); } else #endif { - m_features.push_back (Feature_handle (new Feature(std::forward(t)...))); + m_features.push_back (Feature_handle (std::make_unique(std::forward(t)...))); m_features.back()->set_name (m_features.back()->name() + "_" + std::to_string(i)); } return m_features.back(); } /// \endcond - + /*! \brief Removes a feature. @@ -172,7 +157,7 @@ public: \return `true` if the feature was correctly removed, `false` if its handle was not found. - */ + */ bool remove (Feature_handle feature) { for (std::size_t i = 0; i < m_features.size(); ++ i) @@ -196,7 +181,7 @@ public: /// \name Parallel Processing /// @{ - + /*! \brief Initializes structures to compute features in parallel. @@ -216,11 +201,11 @@ public: deleted before the thread has terminated. \sa `end_parallel_additions()` - */ + */ void begin_parallel_additions() { #ifdef CGAL_LINKED_WITH_TBB - m_tasks = new tbb::task_group; + m_tasks = std::make_unique(); #endif } @@ -237,16 +222,12 @@ public: nothing. \sa `begin_parallel_additions()` - */ + */ void end_parallel_additions() { #ifdef CGAL_LINKED_WITH_TBB m_tasks->wait(); - delete m_tasks; - m_tasks = nullptr; - - for (std::size_t i = 0; i < m_adders.size(); ++ i) - delete m_adders[i]; + m_tasks.release(); m_adders.clear(); #endif } @@ -261,10 +242,10 @@ public: iterator begin() { return m_features.begin(); } const_iterator end() const { return m_features.end(); } iterator end() { return m_features.end(); } - + /*! \brief Returns how many features are defined. - */ + */ std::size_t size() const { return m_features.size(); @@ -273,7 +254,7 @@ public: /*! \brief Returns the \f$i^{th}\f$ feature. - */ + */ Feature_handle operator[](std::size_t i) const { return m_features[i]; @@ -290,7 +271,7 @@ public: void sort_features_by_name() { std::sort (m_features.begin(), m_features.end(), - Compare_name()); + Compare_name()); } /// \endcond @@ -303,24 +284,24 @@ private: virtual ~Abstract_parallel_feature_adder() { } virtual void operator()() const = 0; }; - + template struct Parallel_feature_adder : Abstract_parallel_feature_adder { std::size_t scale; mutable Feature_handle fh; - boost::shared_ptr > args; - + std::shared_ptr > args; + Parallel_feature_adder (Feature_handle fh, T&& ... t) : scale (std::size_t(-1)), fh (fh) { - args = boost::make_shared >(std::forward(t)...); + args = std::make_shared >(std::forward(t)...); } - + Parallel_feature_adder (std::size_t scale, Feature_handle fh, T&& ... t) : scale(scale), fh (fh) { - args = boost::make_shared >(std::forward(t)...); + args = std::make_shared >(std::forward(t)...); } template @@ -337,11 +318,11 @@ private: template const Type& remove_ref_of_simple_type (const Type& t) const { return t; } - + template void add_feature (Tuple& t, seq) const { - fh.attach (new Feature (std::forward(std::get(t))...)); + fh.attach (std::make_unique (std::forward(std::get(t))...)); if (scale != std::size_t(-1)) fh->set_name (fh->name() + "_" + std::to_string(scale)); } @@ -353,7 +334,11 @@ private: }; - std::vector m_adders; + using Abstract_parallel_feature_adder_ptr = std::unique_ptr; + template + using Parallel_feature_adder_ptr = std::unique_ptr >; + + std::vector m_adders; /// \endcond }; diff --git a/Classification/include/CGAL/Classification/Image.h b/Classification/include/CGAL/Classification/Image.h index 45759ed219d..924e39bde24 100644 --- a/Classification/include/CGAL/Classification/Image.h +++ b/Classification/include/CGAL/Classification/Image.h @@ -14,8 +14,8 @@ #include -#include #include +#include #define CGAL_CLASSIFICATION_IMAGE_SIZE_LIMIT 100000000 @@ -27,15 +27,15 @@ namespace Classification { template class Image { - typedef std::vector Vector; - typedef std::map Map; + using Vector = std::vector; + using Map = std::map; std::size_t m_width; std::size_t m_height; std::size_t m_depth; - boost::shared_ptr m_raw; - boost::shared_ptr m_sparse; + std::shared_ptr m_raw; + std::shared_ptr m_sparse; Type m_default; // Forbid using copy constructor @@ -45,7 +45,7 @@ class Image public: - Image () : m_width(0), m_height(0), m_depth(0), m_raw (nullptr) + Image () : m_width(0), m_height(0), m_depth(0) { } @@ -57,9 +57,9 @@ public: if (m_width * m_height * m_depth > 0) { if (m_width * m_height * m_depth < CGAL_CLASSIFICATION_IMAGE_SIZE_LIMIT) - m_raw = boost::shared_ptr (new Vector(m_width * m_height * m_depth)); + m_raw = std::make_shared (m_width * m_height * m_depth); else - m_sparse = boost::shared_ptr (new Map()); + m_sparse = std::make_shared (); } } @@ -69,8 +69,8 @@ public: void free() { - m_raw = boost::shared_ptr(); - m_sparse = boost::shared_ptr(); + m_raw.reset(); + m_sparse.reset(); } Image& operator= (const Image& other) @@ -94,7 +94,7 @@ public: Type& operator() (const std::size_t& x, const std::size_t& y, const std::size_t& z = 0) { - if (m_raw == boost::shared_ptr()) // sparse case + if (!m_raw) // sparse case { typename Map::iterator inserted = m_sparse->insert (std::make_pair (coord(x,y,z), Type())).first; @@ -105,7 +105,7 @@ public: } const Type& operator() (const std::size_t& x, const std::size_t& y, const std::size_t& z = 0) const { - if (m_raw == boost::shared_ptr()) // sparse case + if (!m_raw) // sparse case { typename Map::iterator found = m_sparse->find (coord(x,y,z)); if (found != m_sparse->end()) diff --git a/Classification/include/CGAL/Classification/Label.h b/Classification/include/CGAL/Classification/Label.h index 45b2545803c..b0bf875d0a5 100644 --- a/Classification/include/CGAL/Classification/Label.h +++ b/Classification/include/CGAL/Classification/Label.h @@ -14,7 +14,7 @@ #include -#include +#include namespace CGAL { @@ -57,7 +57,7 @@ public: */ class Label_handle { }; #else -typedef boost::shared_ptr

This page was generated by the CGAL \ +Classification package.