From 947cea31a52c55e30af010f2e07563145706d068 Mon Sep 17 00:00:00 2001 From: Tim Devereux Date: Sat, 5 Sep 2026 03:12:58 +1000 Subject: [PATCH 1/4] added --segmented option to rayextract trees, for pre-labelled clouds When the input cloud is already segmented into individual trees, this takes the segmentation from the cloud and only estimates the tree models, one per label. Labels are read from the point colours, so it accepts the _segmented.ply files that rayextract trees generates (allowing a hand-corrected segmentation to be re-modelled) as well as clouds coloured one arbitrary colour per tree from any other segmentation method. Black points are unlabelled. The shortest path pass still runs, as the branch structure needs the path connectivity, but paths cannot cross between two labels, and unlabelled points adopt the label of the path reaching them so that they cannot bridge two trees. The grid based tree separation is replaced by grouping the ground mesh root points per label, each root point going to the label that most of its points belong to, and kept near the label's most voted root so that a single path down to a distant patch of ground cannot move the tree base. Labels with no root of their own use their entry points instead. Trunks are not split into several trees in this mode, as the labels are taken to be correct. --- README.md | 2 + raycloudtools/rayextract/rayextract.cpp | 32 ++++- raylib/extraction/raysegment.cpp | 183 +++++++++++++++++++++++- raylib/extraction/raysegment.h | 6 +- raylib/extraction/raytrees.cpp | 9 +- raylib/extraction/raytrees.h | 6 +- 6 files changed, 229 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7671bfb98..0f1f6068e 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,8 @@ This gives an example of how the command line tools could be sequenced to analys

+**rayextract trees forest_segmented.ply forest_mesh.ply --segmented**     as above, but for a cloud that is already segmented into individual trees. One tree is reconstructed per colour, so the segmentation is taken from the cloud rather than estimated. This accepts the _segmented.ply files that rayextract trees generates (so a hand-corrected segmentation can be re-modelled), and also clouds that are simply coloured one arbitrary colour per tree, from any other segmentation method. Black points are treated as unlabelled, and are attached to whichever tree's paths reach them. + *Optional build dependencies:* For rayimport/rayexport to work with .las and .laz files (LAS 1.0 through 1.4, including COPC): diff --git a/raycloudtools/rayextract/rayextract.cpp b/raycloudtools/rayextract/rayextract.cpp index bd5e3c4cc..e73d0da38 100644 --- a/raycloudtools/rayextract/rayextract.cpp +++ b/raycloudtools/rayextract/rayextract.cpp @@ -62,6 +62,7 @@ void usage(int exit_code = 1) std::cout << " --branch_segmentation- (-b) _segmented.ply is per branch segment" << std::endl; std::cout << " --grid_width 10 - (-w) crops results assuming cloud has been gridded with given width" << std::endl; std::cout << " --use_rays - (-u) use rays to reduce trunk radius overestimation in noisy cloud data" << std::endl; + std::cout << " --segmented - (-l) the cloud is already segmented into trees, one per colour (as in _segmented.ply). Only the tree models are estimated, black points are unlabelled" << std::endl; std::cout << " (for internal constants -c -g -s see source file rayextract)" << std::endl; std::cout << " note: branch radii rely on priors (Leonardo's rule at splits, self-similar taper) that can bias branch" << std::endl; std::cout << " measurements. Only use these results if the mechanism and its implications are understood." << std::endl; @@ -100,6 +101,7 @@ int rayExtract(int argc, char *argv[]) ray::DoubleArgument gradient(0.001, 1000.0, 1.0), global_taper(0.0, 1.0), global_taper_factor(0.0, 1.0); ray::OptionalKeyValueArgument gradient_option("gradient", 'g', &gradient); ray::OptionalFlagArgument exclude_rays("exclude_rays", 'e'), segment_branches("branch_segmentation", 'b'), stalks("stalks", 's'), use_rays("use_rays", 'u'); + ray::OptionalFlagArgument segmented("segmented", 'l'); ray::DoubleArgument width(0.01, 10.0, 0.25), drop(0.001, 1.0), max_gradient(0.01, 5.0), min_gradient(0.01, 5.0); ray::DoubleArgument max_diameter(0.01, 100.0), distance_limit(0.01, 10.0), height_min(0.01, 1000.0), @@ -140,7 +142,8 @@ int rayExtract(int argc, char *argv[]) argc, argv, { &trees, &cloud_file, &mesh_file }, { &max_diameter_option, &distance_limit_option, &height_min_option, &crop_length_option, &girth_height_ratio_option, &cylinder_length_to_width_option, &gap_ratio_option, &span_ratio_option, &gravity_factor_option, - &segment_branches, &grid_width_option, &global_taper_option, &global_taper_factor_option, &use_rays, &verbose, &view_flag }); + &segment_branches, &grid_width_option, &global_taper_option, &global_taper_factor_option, &use_rays, &segmented, + &verbose, &view_flag }); bool extract_leaves = ray::parseCommandLine(argc, argv, { &leaves, &cloud_file, &trees_file }, { &leaf_option, &leaf_area_option, &leaf_droop_option, &stalks, &view_flag }); if (!extract_trunks && !extract_forest && !extract_terrain && !extract_trees && !extract_leaves) @@ -233,6 +236,33 @@ int rayExtract(int argc, char *argv[]) params.use_rays = use_rays.isSet(); params.segment_branches = segment_branches.isSet(); + // for a pre-segmented cloud we take the tree label of each point from its colour, so the tree + // structures are estimated per label rather than segmenting the trees from the point data + std::vector point_labels; + if (segmented.isSet()) + { + point_labels.reserve(cloud.rayCount()); + size_t num_labelled = 0; + for (size_t i = 0; i < cloud.rayCount(); i++) + { + // this is the inverse of the colouring used when saving the _segmented.ply file, and as it is a + // one-to-one mapping it also works for clouds coloured one arbitrary colour per tree. + // black (unlabelled) points convert to -1 + point_labels.push_back(ray::convertColourToInt(cloud.colours[i])); + if (point_labels.back() >= 0) + { + num_labelled++; + } + } + if (num_labelled == 0) + { + std::cerr << "Error: --segmented expects one colour per tree in " << cloud_file.name() + << ", but every point is black (unlabelled)" << std::endl; + usage(true); + } + params.point_labels = &point_labels; + } + ray::Trees trees(cloud, offset, mesh, params, verbose.isSet()); // output the picewise cylindrical description of the trees diff --git a/raylib/extraction/raysegment.cpp b/raylib/extraction/raysegment.cpp index 2a8e75d3f..2528ab697 100644 --- a/raylib/extraction/raysegment.cpp +++ b/raylib/extraction/raysegment.cpp @@ -6,7 +6,9 @@ #include "raysegment.h" #include #include "rayterrain.h" +#include #include +#include namespace ray { @@ -40,10 +42,12 @@ class QueueNodeComparator /// @c distance_limit maximum distance between points that can be connected /// @c gravity_factor controls how far laterally the shortest paths can travel /// @c closest_node a priority queue +/// @c path_labels optional per-point tree labels (-1 for unlabelled). When supplied, paths are not allowed to +/// connect points of two different labels, and unlabelled points adopt the label of the path that reaches them void connectPointsShortestPath( std::vector &points, std::priority_queue, QueueNodeComparator> &closest_node, double distance_limit, - double gravity_factor) + double gravity_factor, std::vector *path_labels) { // 1. get nearest neighbours const int search_size = std::min(20, static_cast(points.size()) - 1); @@ -71,6 +75,16 @@ void connectPointsShortestPath( for (int i = 0; i < search_size && indices(i, node.id) != Nabo::NNSearchD::InvalidIndex; i++) { const int child = indices(i, node.id); + if (path_labels) + { + // paths are not allowed to cross from one labelled tree into another + const int node_label = (*path_labels)[node.id]; + const int child_label = (*path_labels)[child]; + if (node_label != -1 && child_label != -1 && node_label != child_label) + { + continue; + } + } const double dist2 = dists2(i, node.id); // square distance to neighbour const double dist = std::sqrt(dist2); double new_score = 0; @@ -117,6 +131,11 @@ void connectPointsShortestPath( points[child].distance_to_ground = node.distance_to_ground + dist; points[child].parent = node.id; points[child].root = node.root; + if (path_labels && (*path_labels)[child] == -1) + { + // an unlabelled point belongs to the tree that its path comes from + (*path_labels)[child] = (*path_labels)[node.id]; + } closest_node.push( QueueNode(points[child].distance_to_ground, points[child].score, node.radius, node.root, child)); } @@ -130,17 +149,55 @@ void connectPointsShortestPath( /// the returned vector of index sets provides the root points for each separated tree std::vector> getRootsAndSegment(std::vector &points, const Cloud &cloud, const Mesh &mesh, double max_diameter, double distance_limit, double height_min, - double gravity_factor) + double gravity_factor, const std::vector *point_labels) { + if (point_labels && point_labels->size() != cloud.ends.size()) + { + std::cerr << "Error: the number of point labels does not match the number of rays in the cloud" << std::endl; + return std::vector>(); + } // first fill in the basic attributes of the points structure points.reserve(cloud.ends.size()); + std::vector labels; // the supplied label per point, or -1 where unlabelled + int num_labels = 0; + if (point_labels) + { + labels.reserve(cloud.ends.size()); + } + std::unordered_map label_ids; // maps the supplied label values onto a contiguous set of tree indices for (unsigned int i = 0; i < cloud.ends.size(); i++) { if (cloud.rayBounded(i)) { points.push_back(Vertex(cloud.ends[i], cloud.starts[i])); + if (point_labels) + { + const int label = (*point_labels)[i]; + if (label < 0) // unlabelled, so it will be attached to whichever tree's path reaches it + { + labels.push_back(-1); + } + else + { + const auto &found = label_ids.find(label); + if (found == label_ids.end()) + { + label_ids.insert({ label, num_labels }); + labels.push_back(num_labels++); + } + else + { + labels.push_back(found->second); + } + } + } } } + const int num_cloud_points = static_cast(points.size()); + if (point_labels) + { + std::cout << "using " << num_labels << " pre-labelled trees from the input cloud" << std::endl; + } const double pixel_width = max_diameter; Eigen::Vector3d box_min, box_max; @@ -157,6 +214,13 @@ std::vector> getRootsAndSegment(std::vector &points, co points.push_back(Vertex(vert - Eigen::Vector3d(0,0,0.01), vert + Eigen::Vector3d(0,0,0.01))); // make small vertical ray } } + // the ground mesh vertices are unlabelled, they are the seeds that the labelled trees grow from + std::vector path_labels; + if (point_labels) + { + path_labels = labels; + path_labels.resize(points.size(), -1); + } // convert the ground mesh to an easy look-up height field Eigen::ArrayXXd lowfield; mesh.toHeightField(lowfield, box_min, box_max, pixel_width); @@ -190,7 +254,120 @@ std::vector> getRootsAndSegment(std::vector &points, co } // perform Djikstra's shortest path to ground algorithm to fill in the parent indices in 'points' - connectPointsShortestPath(points, closest_node, distance_limit, gravity_factor); + connectPointsShortestPath(points, closest_node, distance_limit, gravity_factor, + point_labels ? &path_labels : nullptr); + + // when labels have been supplied we don't need to segment the trees, we just group the root points + // according to the label of the points that grow from them, generating one tree per label + if (point_labels) + { + // each labelled point votes for its root point, so that a root point shared by several + // labels is given to the label that most of its points belong to + std::vector> votes(points.size() - roots_start); + std::vector max_heights(num_labels, 0.0); + for (int i = 0; i < num_cloud_points; i++) + { + const int label = labels[i]; + if (label == -1) + { + continue; + } + const Eigen::Vector3i index = ((points[i].pos - box_min) / pixel_width).cast(); + max_heights[label] = std::max(max_heights[label], points[i].pos[2] - lowfield(index[0], index[1])); + if (points[i].root >= roots_start) + { + votes[points[i].root - roots_start][label]++; + } + } + std::vector> roots_set(num_labels); + std::vector best_root(num_labels, -1); // the most voted for root point per label + std::vector best_root_votes(num_labels, 0); + for (int i = roots_start; i < static_cast(points.size()); i++) + { + int best_label = -1; + int most_votes = 0; + for (const auto &vote : votes[i - roots_start]) + { + if (vote.second > most_votes) + { + most_votes = vote.second; + best_label = vote.first; + } + } + if (best_label != -1) + { + roots_set[best_label].push_back(i); + if (most_votes > best_root_votes[best_label]) + { + best_root_votes[best_label] = most_votes; + best_root[best_label] = i; + } + } + } + // only keep the root points close to the most voted for one, so that a single path descending to a + // distant patch of ground cannot move the base of the tree + const double max_root_radius = 2.0 * pixel_width; + for (int label = 0; label < num_labels; label++) + { + if (best_root[label] == -1) + { + continue; + } + std::vector close_roots; + for (auto &root : roots_set[label]) + { + Eigen::Vector3d dif = points[root].pos - points[best_root[label]].pos; + dif[2] = 0.0; + if (dif.norm() <= max_root_radius) + { + close_roots.push_back(root); + } + } + roots_set[label] = close_roots; + } + // a label whose points all descend through another label's root points has no root of its own, + // so we use its entry points (the points where its paths cross into the label) instead + std::vector needs_entry_points(num_labels); + for (int label = 0; label < num_labels; label++) + { + needs_entry_points[label] = roots_set[label].empty(); + } + for (int i = 0; i < num_cloud_points; i++) + { + const int label = labels[i]; + if (label == -1 || !needs_entry_points[label]) + { + continue; + } + const int parent = points[i].parent; + if (parent == -1 || path_labels[parent] != label) + { + roots_set[label].push_back(i); + } + } + // lastly remove the labels that are too small to count as a tree + std::vector> result; + int num_short = 0; + for (int label = 0; label < num_labels; label++) + { + if (roots_set[label].empty()) + { + continue; + } + if (max_heights[label] < height_min) + { + num_short++; + continue; + } + result.push_back(roots_set[label]); + } + if (num_short > 0) + { + std::cout << "removed " << num_short << " labelled trees shorter than the " << height_min + << " m minimum height" << std::endl; + } + return result; + } // next we want to segment the paths into separate trees. To do this we find the number of points and // the maximum height of points that come from each cell index diff --git a/raylib/extraction/raysegment.h b/raylib/extraction/raysegment.h index 3477983a2..d03e0ed1d 100644 --- a/raylib/extraction/raysegment.h +++ b/raylib/extraction/raysegment.h @@ -42,9 +42,13 @@ struct RAYLIB_EXPORT Vertex /// @c max_diameter maximum diameter of a tree trunk /// @c distance_limit maximum distance between points that can be connected /// @c gravity_factor controls how far laterally the shortest paths can travel +/// @c point_labels optional tree label per ray in @c cloud. When supplied, one tree is +/// generated per distinct label, rather than segmenting the trees from the point distribution. Negative +/// labels mean unlabelled, these points are attached to whichever labelled tree reaches them std::vector> RAYLIB_EXPORT getRootsAndSegment(std::vector &points, const Cloud &cloud, const Mesh &mesh, double max_diameter, double distance_limit, double height_min, - double gravity_factor); + double gravity_factor, + const std::vector *point_labels = nullptr); } // namespace ray #endif // RAYLIB_RAYSEGMENT_H \ No newline at end of file diff --git a/raylib/extraction/raytrees.cpp b/raylib/extraction/raytrees.cpp index be368bcc7..04491b68b 100644 --- a/raylib/extraction/raytrees.cpp +++ b/raylib/extraction/raytrees.cpp @@ -23,6 +23,7 @@ TreesParams::TreesParams() , segment_branches(false) , global_taper(0.012) , global_taper_factor(0.3) + , point_labels(nullptr) {} /// The main reconstruction algorithm @@ -33,8 +34,9 @@ Trees::Trees(Cloud &cloud, const Eigen::Vector3d &offset, const Mesh &mesh, cons // firstly, get the full set of shortest paths from ground to tips, and the set of roots params_ = ¶ms; - std::vector> roots_list = getRootsAndSegment( - points_, cloud, mesh, params_->max_diameter, params_->distance_limit, params_->height_min, params_->gravity_factor); + std::vector> roots_list = + getRootsAndSegment(points_, cloud, mesh, params_->max_diameter, params_->distance_limit, params_->height_min, + params_->gravity_factor, params_->point_labels); // Now we want to convert these paths into a set of branch sections, from root to tips // splitting as we go up... @@ -145,7 +147,8 @@ Trees::Trees(Cloud &cloud, const Eigen::Vector3d &offset, const Mesh &mesh, cons sections_[sec_].tip = best_tip; sections_[sec_].ends = best_ends; nodes = best_nodes; - if (sections_[sec_].split_count < 2) + // when the trees are pre-labelled we trust the labels, so we don't split a trunk into several trees + if (!params_->point_labels && sections_[sec_].split_count < 2) { double thickness = best_dist; bool points_removed = false; diff --git a/raylib/extraction/raytrees.h b/raylib/extraction/raytrees.h index 523101e45..814d0e9dd 100644 --- a/raylib/extraction/raytrees.h +++ b/raylib/extraction/raytrees.h @@ -32,6 +32,8 @@ struct RAYLIB_EXPORT TreesParams double global_taper; // forced global taper, uses global_taper_factor to define how much it is applied double global_taper_factor; // 0 estimates per-tree tapering, 1 uses per-scan tapering, 0.5 is mid-way on mid-weight trees bool use_rays; // use the full rays in order to estimate a smaller radius when points are not all on the real branch + const std::vector *point_labels; // optional per-ray tree labels (-1 for unlabelled), one tree is generated + // per distinct label rather than segmenting the trees from the point data }; struct BranchSection; // forwards declaration @@ -44,7 +46,9 @@ class RAYLIB_EXPORT Trees { public: /// Constructs the piecewise cylindrical tree structures from the input ray cloud @c cloud - /// The ground @c mesh defines the ground and @params are used to control the reconstruction + /// The ground @c mesh defines the ground and @params are used to control the reconstruction. + /// When @c params.point_labels is supplied the cloud is treated as pre-segmented, so only the + /// tree models are estimated, one per label Trees(Cloud &cloud, const Eigen::Vector3d &offset, const Mesh &mesh, const TreesParams ¶ms, bool verbose); /// save the trees representation to a text file From ed72d8591e316aa0ec30c8e3925bb6aa2f4ca6ff Mon Sep 17 00:00:00 2001 From: Tim Devereux <25172134+tim-devereux@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:15:29 -0300 Subject: [PATCH 2/4] Update README --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 0f1f6068e..7671bfb98 100644 --- a/README.md +++ b/README.md @@ -235,8 +235,6 @@ This gives an example of how the command line tools could be sequenced to analys

-**rayextract trees forest_segmented.ply forest_mesh.ply --segmented**     as above, but for a cloud that is already segmented into individual trees. One tree is reconstructed per colour, so the segmentation is taken from the cloud rather than estimated. This accepts the _segmented.ply files that rayextract trees generates (so a hand-corrected segmentation can be re-modelled), and also clouds that are simply coloured one arbitrary colour per tree, from any other segmentation method. Black points are treated as unlabelled, and are attached to whichever tree's paths reach them. - *Optional build dependencies:* For rayimport/rayexport to work with .las and .laz files (LAS 1.0 through 1.4, including COPC): From 98d6040c40d05686ef58c26a7e8d4f8f7325f545 Mon Sep 17 00:00:00 2001 From: Tim Devereux Date: Wed, 9 Sep 2026 13:13:39 +1000 Subject: [PATCH 3/4] pass point labels to the Trees constructor instead of TreesParams --- raycloudtools/rayextract/rayextract.cpp | 4 ++-- raylib/extraction/raytrees.cpp | 9 +++++---- raylib/extraction/raytrees.h | 10 +++++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/raycloudtools/rayextract/rayextract.cpp b/raycloudtools/rayextract/rayextract.cpp index e73d0da38..d2408aa05 100644 --- a/raycloudtools/rayextract/rayextract.cpp +++ b/raycloudtools/rayextract/rayextract.cpp @@ -260,10 +260,10 @@ int rayExtract(int argc, char *argv[]) << ", but every point is black (unlabelled)" << std::endl; usage(true); } - params.point_labels = &point_labels; } - ray::Trees trees(cloud, offset, mesh, params, verbose.isSet()); + ray::Trees trees(cloud, offset, mesh, params, verbose.isSet(), + segmented.isSet() ? &point_labels : nullptr); // output the picewise cylindrical description of the trees trees.save(cloud_file.nameStub() + "_trees.txt", offset, verbose.isSet()); diff --git a/raylib/extraction/raytrees.cpp b/raylib/extraction/raytrees.cpp index 04491b68b..062994ee5 100644 --- a/raylib/extraction/raytrees.cpp +++ b/raylib/extraction/raytrees.cpp @@ -23,20 +23,21 @@ TreesParams::TreesParams() , segment_branches(false) , global_taper(0.012) , global_taper_factor(0.3) - , point_labels(nullptr) {} /// The main reconstruction algorithm /// It is based on finding the shortest paths using Djikstra's algorithm, followed /// by an agglomeration of paths, with repeated splitting from root to tips -Trees::Trees(Cloud &cloud, const Eigen::Vector3d &offset, const Mesh &mesh, const TreesParams ¶ms, bool verbose) +Trees::Trees(Cloud &cloud, const Eigen::Vector3d &offset, const Mesh &mesh, const TreesParams ¶ms, bool verbose, + const std::vector *point_labels) { // firstly, get the full set of shortest paths from ground to tips, and the set of roots params_ = ¶ms; + point_labels_ = point_labels; std::vector> roots_list = getRootsAndSegment(points_, cloud, mesh, params_->max_diameter, params_->distance_limit, params_->height_min, - params_->gravity_factor, params_->point_labels); + params_->gravity_factor, point_labels_); // Now we want to convert these paths into a set of branch sections, from root to tips // splitting as we go up... @@ -148,7 +149,7 @@ Trees::Trees(Cloud &cloud, const Eigen::Vector3d &offset, const Mesh &mesh, cons sections_[sec_].ends = best_ends; nodes = best_nodes; // when the trees are pre-labelled we trust the labels, so we don't split a trunk into several trees - if (!params_->point_labels && sections_[sec_].split_count < 2) + if (!point_labels_ && sections_[sec_].split_count < 2) { double thickness = best_dist; bool points_removed = false; diff --git a/raylib/extraction/raytrees.h b/raylib/extraction/raytrees.h index 814d0e9dd..670d2d607 100644 --- a/raylib/extraction/raytrees.h +++ b/raylib/extraction/raytrees.h @@ -32,8 +32,6 @@ struct RAYLIB_EXPORT TreesParams double global_taper; // forced global taper, uses global_taper_factor to define how much it is applied double global_taper_factor; // 0 estimates per-tree tapering, 1 uses per-scan tapering, 0.5 is mid-way on mid-weight trees bool use_rays; // use the full rays in order to estimate a smaller radius when points are not all on the real branch - const std::vector *point_labels; // optional per-ray tree labels (-1 for unlabelled), one tree is generated - // per distinct label rather than segmenting the trees from the point data }; struct BranchSection; // forwards declaration @@ -47,9 +45,10 @@ class RAYLIB_EXPORT Trees public: /// Constructs the piecewise cylindrical tree structures from the input ray cloud @c cloud /// The ground @c mesh defines the ground and @params are used to control the reconstruction. - /// When @c params.point_labels is supplied the cloud is treated as pre-segmented, so only the - /// tree models are estimated, one per label - Trees(Cloud &cloud, const Eigen::Vector3d &offset, const Mesh &mesh, const TreesParams ¶ms, bool verbose); + /// When @c point_labels is supplied (one tree label per ray in @c cloud, -1 for unlabelled) the + /// cloud is treated as pre-segmented, so only the tree models are estimated, one per label + Trees(Cloud &cloud, const Eigen::Vector3d &offset, const Mesh &mesh, const TreesParams ¶ms, bool verbose, + const std::vector *point_labels = nullptr); /// save the trees representation to a text file bool save(const std::string &filename, const Eigen::Vector3d &offset, bool verbose) const; @@ -107,6 +106,7 @@ class RAYLIB_EXPORT Trees // cached data that is used throughout the processing method int sec_; const TreesParams *params_; + const std::vector *point_labels_; // per-ray tree labels when the cloud is pre-segmented, otherwise null std::vector points_; double forest_taper_{0}; double forest_weight_{0}; From 14c557352b7e76213548819012fe2888fc97228f Mon Sep 17 00:00:00 2001 From: Tim Devereux Date: Fri, 11 Sep 2026 11:31:08 +1000 Subject: [PATCH 4/4] order pre-labelled trees by input label, so their IDs round trip --- raylib/extraction/raysegment.cpp | 38 +++++++++++++++++--------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/raylib/extraction/raysegment.cpp b/raylib/extraction/raysegment.cpp index 2528ab697..d2b30301d 100644 --- a/raylib/extraction/raysegment.cpp +++ b/raylib/extraction/raysegment.cpp @@ -7,6 +7,7 @@ #include #include "rayterrain.h" #include +#include #include #include @@ -164,7 +165,23 @@ std::vector> getRootsAndSegment(std::vector &points, co { labels.reserve(cloud.ends.size()); } - std::unordered_map label_ids; // maps the supplied label values onto a contiguous set of tree indices + // maps the supplied label values onto a contiguous set of tree indices, in ascending label order so that + // the output trees keep the order (and so the IDs) of the input labels + std::map label_ids; + if (point_labels) + { + for (unsigned int i = 0; i < cloud.ends.size(); i++) + { + if (cloud.rayBounded(i) && (*point_labels)[i] >= 0) + { + label_ids[(*point_labels)[i]] = 0; + } + } + for (auto &label_id : label_ids) + { + label_id.second = num_labels++; + } + } for (unsigned int i = 0; i < cloud.ends.size(); i++) { if (cloud.rayBounded(i)) @@ -172,24 +189,9 @@ std::vector> getRootsAndSegment(std::vector &points, co points.push_back(Vertex(cloud.ends[i], cloud.starts[i])); if (point_labels) { + // negative labels are unlabelled, so these points attach to whichever tree's path reaches them const int label = (*point_labels)[i]; - if (label < 0) // unlabelled, so it will be attached to whichever tree's path reaches it - { - labels.push_back(-1); - } - else - { - const auto &found = label_ids.find(label); - if (found == label_ids.end()) - { - label_ids.insert({ label, num_labels }); - labels.push_back(num_labels++); - } - else - { - labels.push_back(found->second); - } - } + labels.push_back(label < 0 ? -1 : label_ids[label]); } } }