From 5c31be12255399f4d8d73530901cbd5475516510 Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Fri, 24 Apr 2026 11:41:36 +1000 Subject: [PATCH 01/37] improved density_rgb to be less extreme in brightness --- raylib/rayrenderer.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/raylib/rayrenderer.cpp b/raylib/rayrenderer.cpp index e65a7e118..407fe5a6e 100644 --- a/raylib/rayrenderer.cpp +++ b/raylib/rayrenderer.cpp @@ -825,7 +825,10 @@ bool renderCloud(const std::string &cloud_file, const Cuboid &bounds, ViewDirect case RenderStyle::Density_rgb: { if (is_hdr) - col3d = colour[0] * redGreenBlueSpectrum(std::log10(std::max(1e-6, colour[0]))); + { + // brightness doubles and hue cycles every power of 10 + col3d = std::pow(colour[0], 0.3) * redGreenBlueSpectrum(std::log10(std::max(1e-6, colour[0]))); + } else { double shade = colour[0] / max_val; From f8cdd44658c5a8c74a6d30f1651a9a5cf0ffd347 Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Fri, 8 May 2026 10:36:49 +1000 Subject: [PATCH 02/37] attempt to make rayrender rgb_density comparible between scans --- raylib/rayrenderer.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/raylib/rayrenderer.cpp b/raylib/rayrenderer.cpp index 407fe5a6e..8ba4e1fdb 100644 --- a/raylib/rayrenderer.cpp +++ b/raylib/rayrenderer.cpp @@ -787,6 +787,7 @@ bool renderCloud(const std::string &cloud_file, const Cuboid &bounds, ViewDirect max_val = mean + 2.0 * standard_deviation; min_val = mean - 2.0 * standard_deviation; } + std::cout << "max val: " << max_val << std::endl; // The final pixel buffer std::vector pixel_colours; @@ -831,10 +832,12 @@ bool renderCloud(const std::string &cloud_file, const Cuboid &bounds, ViewDirect } else { - double shade = colour[0] / max_val; - col3d = redGreenBlueGradient(shade); - if (shade < 0.05) - col3d *= 20.0 * shade; // this blends the lowest densities down to black + col3d = 0.7 * std::pow(colour[0], 0.3) * redGreenBlueSpectrum(std::log10(std::max(1e-6, colour[0]))); + +// double shade = colour[0] / max_val; +// col3d = redGreenBlueGradient(shade); +// if (shade < 0.05) +// col3d *= 20.0 * shade; // this blends the lowest densities down to black } break; } From ab4d45a49e365d1f95def1ec3545e329a04aeffc Mon Sep 17 00:00:00 2001 From: Tom Lowe Date: Mon, 11 May 2026 09:24:56 +1000 Subject: [PATCH 03/37] support unsigned int ply properties --- raylib/rayply.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/raylib/rayply.cpp b/raylib/rayply.cpp index 4ca5cbce5..e997458e8 100644 --- a/raylib/rayply.cpp +++ b/raylib/rayply.cpp @@ -29,6 +29,7 @@ enum DataType kDTushort, kDTuchar, kDTint, + kDTuint, kDTnone }; } // namespace @@ -347,7 +348,7 @@ bool readPly(const std::string &file_name, bool is_ray_cloud, bool pos_is_float = false; bool normal_is_float = false; DataType intensity_type = kDTnone; - int rowsteps[] = { int(sizeof(float)), int(sizeof(double)), int(sizeof(unsigned short)), int(sizeof(unsigned char)), int(sizeof(int)), + int rowsteps[] = { int(sizeof(float)), int(sizeof(double)), int(sizeof(unsigned short)), int(sizeof(unsigned char)), int(sizeof(int)), int(sizeof(unsigned int)), 0 }; // to match each DataType enum while (line != "end_header\r" && line != "end_header") @@ -375,6 +376,8 @@ bool readPly(const std::string &file_name, bool is_ray_cloud, data_type = kDTushort; else if (line.find("property int") != std::string::npos) data_type = kDTint; + else if (line.find("property uint") != std::string::npos || line.find("property unsigned int") != std::string::npos) + data_type = kDTuint; if (line == "property float x" || line == "property double x") { From 25aa672524551ed44a21968e5d9f469fa5d074ca Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Tue, 26 May 2026 09:33:39 +1000 Subject: [PATCH 04/37] improved raydiff --- raycloudtools/raydiff/raydiff.cpp | 66 +++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/raycloudtools/raydiff/raydiff.cpp b/raycloudtools/raydiff/raydiff.cpp index 048af2f37..9dc94a506 100644 --- a/raycloudtools/raydiff/raydiff.cpp +++ b/raycloudtools/raydiff/raydiff.cpp @@ -28,6 +28,7 @@ void usage(int exit_code = 1) std::cout << "usage:" << std::endl; std::cout << "raydiff cloud1.ply cloud2.ply" << std::endl; std::cout << " --distance 0 - optional threshold in m for colouring differences. Default auto-detects distribution shoulder" << std::endl; + std::cout << " --unified - show as single cloud. Red for removed geometry, green for added." << std::endl; std::cout << " --visualise - open in the default visualisation tool" << std::endl; // clang-format on exit(exit_code); @@ -144,10 +145,10 @@ double getShoulder(double k, std::vector sorted_dists, double &min_error int rayDiff(int argc, char *argv[]) { ray::FileArgument cloud1_name, cloud2_name; - ray::OptionalFlagArgument visualise("visualise", 'v'); + ray::OptionalFlagArgument visualise("visualise", 'v'), unified("unified", 'u'); ray::DoubleArgument distance_threshold(0.0, 1000.0, 0.0); ray::OptionalKeyValueArgument distance_option("distance", 'd', &distance_threshold); - if (!ray::parseCommandLine(argc, argv, { &cloud1_name, &cloud2_name }, { &distance_option, &visualise })) + if (!ray::parseCommandLine(argc, argv, { &cloud1_name, &cloud2_name }, { &distance_option, &visualise, &unified })) { usage(); } @@ -261,29 +262,62 @@ int rayDiff(int argc, char *argv[]) j++; } } - cloud1.save(cloud1_name.nameStub() + "_diff.ply"); j = 0; - for (int i = 0; i<(int)cloud2.ends.size(); i++) + if (unified.isSet()) { - if (cloud2.rayBounded(i)) + Eigen::Vector3d diff2_col(0,255,0); + int imin = cloud1.ends.size(); + cloud1.starts.insert(cloud1.starts.end(), cloud2.starts.begin(), cloud2.starts.end()); + cloud1.ends.insert(cloud1.ends.end(), cloud2.ends.begin(), cloud2.ends.end()); + cloud1.times.insert(cloud1.times.end(), cloud2.times.begin(), cloud2.times.end()); + cloud1.colours.insert(cloud1.colours.end(), cloud2.colours.begin(), cloud2.colours.end()); + for (int i = 0; i<(int)cloud2.ends.size(); i++) { - if (dists_to_cloud2[j] > min_error_dist) + int I = i + imin; + if (cloud2.rayBounded(i)) { - double proximity = min_error_dist / dists_to_cloud2[j]; - Eigen::Vector3d col(cloud2.colours[i].red, cloud2.colours[i].green, cloud2.colours[i].blue); - Eigen::Vector3d new_col = diff_col + (col - diff_col) * proximity; - cloud2.colours[i] = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), cloud2.colours[i].alpha); + if (dists_to_cloud2[j] > min_error_dist) + { + double proximity = min_error_dist / dists_to_cloud2[j]; + Eigen::Vector3d col(cloud2.colours[i].red, cloud2.colours[i].green, cloud2.colours[i].blue); + Eigen::Vector3d new_col = diff2_col + (col - diff2_col) * proximity; + cloud1.colours[I] = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), cloud2.colours[i].alpha); + } + j++; } - j++; + } + cloud1.save(cloud1_name.nameStub() + "_diff.ply"); + if (visualise.isSet()) + { + std::string command = std::string(VISUALISE_TOOL) + std::string(" ") + cloud1_name.nameStub() + "_diff.ply"; + system(command.c_str()); } } - cloud2.save(cloud2_name.nameStub() + "_diff.ply"); - - if (visualise.isSet()) + else { - std::string command = std::string(VISUALISE_TOOL) + std::string(" ") + cloud1_name.nameStub() + "_diff.ply " + cloud2_name.nameStub() + "_diff.ply"; - system(command.c_str()); + cloud1.save(cloud1_name.nameStub() + "_diff.ply"); + for (int i = 0; i<(int)cloud2.ends.size(); i++) + { + if (cloud2.rayBounded(i)) + { + if (dists_to_cloud2[j] > min_error_dist) + { + double proximity = min_error_dist / dists_to_cloud2[j]; + Eigen::Vector3d col(cloud2.colours[i].red, cloud2.colours[i].green, cloud2.colours[i].blue); + Eigen::Vector3d new_col = diff_col + (col - diff_col) * proximity; + cloud2.colours[i] = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), cloud2.colours[i].alpha); + } + j++; + } + } + cloud2.save(cloud2_name.nameStub() + "_diff.ply"); + if (visualise.isSet()) + { + std::string command = std::string(VISUALISE_TOOL) + std::string(" ") + cloud1_name.nameStub() + "_diff.ply " + cloud2_name.nameStub() + "_diff.ply"; + system(command.c_str()); + } } + return (int)similarity; return 1; } From adeae59a1a104cde786f7951545bdb447d9307a3 Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Tue, 26 May 2026 09:36:04 +1000 Subject: [PATCH 05/37] improved raydiff --- raycloudtools/raydiff/raydiff.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/raycloudtools/raydiff/raydiff.cpp b/raycloudtools/raydiff/raydiff.cpp index 9dc94a506..3b9e76caa 100644 --- a/raycloudtools/raydiff/raydiff.cpp +++ b/raycloudtools/raydiff/raydiff.cpp @@ -319,7 +319,6 @@ int rayDiff(int argc, char *argv[]) } return (int)similarity; - return 1; } int main(int argc, char *argv[]) From 56e59beb71ecf86f507ca121e089a4b6f74695f5 Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Tue, 26 May 2026 10:49:46 +1000 Subject: [PATCH 06/37] seems to be working --- raycloudtools/raydiff/raydiff.cpp | 228 +++++++++++++++++++----------- 1 file changed, 149 insertions(+), 79 deletions(-) diff --git a/raycloudtools/raydiff/raydiff.cpp b/raycloudtools/raydiff/raydiff.cpp index 3b9e76caa..f09fcda03 100644 --- a/raycloudtools/raydiff/raydiff.cpp +++ b/raycloudtools/raydiff/raydiff.cpp @@ -9,11 +9,13 @@ #include #include #include +#include #include "raylib/raycloud.h" #include "raylib/rayparse.h" #include "raylib/raycuboid.h" #include "raylib/rayply.h" +#include "raylib/raycloudwriter.h" // we look for a uniform distribution of best fit given a correlation dimension k // if the points are in a line then k=1 // if the points are in a plane then k=2 @@ -34,70 +36,70 @@ void usage(int exit_code = 1) exit(exit_code); } -void calcNearestNeighbourDistances(const ray::Cloud &cloud1, const ray::Cloud &cloud2, std::vector &distances) +void calcNearestNeighbourDistances(const std::vector &cloud1, const std::vector &cloud2, std::vector &distances) { - Nabo::NNSearchD *nns; + Nabo::NNSearchF *nns; // Nabo::Parameters params("bucketSize", 8); int num_bounded = 0, num_bounded2 = 0; - for (unsigned int i = 0; i < cloud1.ends.size(); i++) - if (cloud1.rayBounded(i)) + for (unsigned int i = 0; i < cloud1.size(); i++) + if (cloud1[i][0] != std::numeric_limits::max()) num_bounded++; - for (unsigned int i = 0; i < cloud2.ends.size(); i++) - if (cloud2.rayBounded(i)) + for (unsigned int i = 0; i < cloud2.size(); i++) + if (cloud2[i][0] != std::numeric_limits::max()) num_bounded2++; - Eigen::MatrixXd points_p(3, num_bounded); + Eigen::MatrixXf points_p(3, num_bounded); int j = 0; - for (unsigned int i = 0; i < cloud1.ends.size(); i++) + for (unsigned int i = 0; i < cloud1.size(); i++) { - if (cloud1.rayBounded(i)) - points_p.col(j++) = cloud1.ends[i]; + if (cloud1[i][0] != std::numeric_limits::max()) + points_p.col(j++) = cloud1[i]; } - nns = Nabo::NNSearchD::createKDTreeLinearHeap(points_p, 3); + nns = Nabo::NNSearchF::createKDTreeLinearHeap(points_p, 3); - Eigen::MatrixXd points_q(3, num_bounded2); + Eigen::MatrixXf points_q(3, num_bounded2); j = 0; - for (unsigned int i = 0; i < cloud2.ends.size(); i++) + for (unsigned int i = 0; i < cloud2.size(); i++) { - if (cloud2.rayBounded(i)) - points_q.col(j++) = cloud2.ends[i]; + if (cloud2[i][0] != std::numeric_limits::max()) + points_q.col(j++) = cloud2[i]; } Eigen::MatrixXi indices; - Eigen::MatrixXd dists2; + Eigen::MatrixXf dists2; // Run the search const int search_size = 1; indices.resize(search_size, num_bounded2); dists2.resize(search_size, num_bounded2); - nns->knn(points_q, indices, dists2, search_size, ray::kNearestNeighbourEpsilon, Nabo::NNSearchD::SearchOptionFlags::SORT_RESULTS | Nabo::NNSearchD::SearchOptionFlags::ALLOW_SELF_MATCH); + nns->knn(points_q, indices, dists2, search_size, (float)ray::kNearestNeighbourEpsilon, Nabo::NNSearchF::SearchOptionFlags::SORT_RESULTS | Nabo::NNSearchF::SearchOptionFlags::ALLOW_SELF_MATCH); + delete nns; distances.resize(num_bounded2); for (int i = 0; i sorted_dists, double &min_error_dist, double &similarity) +double getShoulder(double k, std::vector sorted_dists, double &min_error_dist, double &similarity) { int num = (int)sorted_dists.size(); // 1. transform the result to make the distances uniform if their 3D points are uniformly distributed for (int i = 0; i outside_const(num+1, 0.0); - std::vector outside_linear(num+1, 0.0); - std::vector outside_square(num+1, 0.0); + std::vector outside_const(num+1, 0.0); + std::vector outside_linear(num+1, 0.0); + std::vector outside_square(num+1, 0.0); for (int i = num-1; i>=0; i--) { double d = sorted_dists.back() - sorted_dists[i]; double yN = (num-1); - double I = (double)i - yN; - outside_const[i] = outside_const[i+1] + I*I; - outside_linear[i] = outside_linear[i+1] + 2.0*I*d; - outside_square[i] = outside_square[i+1] + d*d; + double I = (float)i - yN; + outside_const[i] = (float)((double)outside_const[i+1] + I*I); + outside_linear[i] = (float)((double)outside_linear[i+1] + 2.0*I*d); + outside_square[i] =(float)((double)outside_square[i+1] + d*d); } // 3. accumulate linear term forwards, but store only best results @@ -153,28 +155,53 @@ int rayDiff(int argc, char *argv[]) usage(); } - ray::Cloud cloud1, cloud2; - if (!cloud1.load(cloud1_name.name()) || !cloud2.load(cloud2_name.name())) + // low memory raydiff requires offsets to support float vectors, and careful avoidance of duplicating large arrays + // step 1: get points only + std::vector points1, points2; + std::vector *points = &points1; + Eigen::Vector3d start_pos(0,0,0); + bool has_start_pos = false; + auto getPoints = [&](std::vector &, std::vector &ends, + std::vector &, std::vector &colours) { - usage(); - } + if (!has_start_pos && !ends.empty()) + { + start_pos = ends[0]; + has_start_pos = true; + } + for (size_t i = 0; ipush_back(Eigen::Vector3f(std::numeric_limits::max(), 0,0)); // flag as unbounded + else + points->push_back((ends[i] - start_pos).cast()); + } + }; + if (!ray::Cloud::read(cloud1_name.name(), getPoints)) + return false; + points = &points2; + if (!ray::Cloud::read(cloud2_name.name(), getPoints)) + return false; + // Primary goal - get an overall similarity percentage and return it as the return value, so it can be used in bash scripts // this needs to include colour and location, but be insensitive to changes in density. i.e. it needs to work usefully on a repeat scan. - std::vector dists_to_cloud2, dists_to_cloud1; + std::vector dists_to_cloud2, dists_to_cloud1; std::cout << "calculating cloud2 neighbours of cloud1..." << std::endl; - calcNearestNeighbourDistances(cloud1, cloud2, dists_to_cloud2); + calcNearestNeighbourDistances(points1, points2, dists_to_cloud2); std::cout << "calculating cloud1 neighbours of cloud2..." << std::endl; - calcNearestNeighbourDistances(cloud2, cloud1, dists_to_cloud1); + calcNearestNeighbourDistances(points2, points1, dists_to_cloud1); + points1.clear(); points1.shrink_to_fit(); + points2.clear(); points2.shrink_to_fit(); // 1. max distance - double max_dist = 0.0; + float max_dist = 0.0; for (int i = 0; i<(int)dists_to_cloud2.size(); i++) max_dist = std::max(max_dist, dists_to_cloud2[i]); for (int i = 0; i<(int)dists_to_cloud1.size(); i++) max_dist = std::max(max_dist, dists_to_cloud1[i]); - std::vector sorted_dists = dists_to_cloud2; + std::vector sorted_dists = dists_to_cloud2; sorted_dists.insert(sorted_dists.end(), dists_to_cloud1.begin(), dists_to_cloud1.end()); double min_error_dist = distance_threshold.value(); @@ -198,6 +225,20 @@ int rayDiff(int argc, char *argv[]) } else { + // prune out distance 0 + for (int i = sorted_dists.size()-1; i>=0; i--) + { + if (sorted_dists[i] <= 0.0) + { + sorted_dists[i] = sorted_dists.back(); + sorted_dists.pop_back(); + } + } + if (sorted_dists.empty()) + { + std::cerr << "No difference detect between files. Exiting" << std::endl; + return 0; + } std::cout << "sorting differences..." << std::endl; std::sort(sorted_dists.begin(), sorted_dists.end()); double min_error = 0; @@ -245,48 +286,85 @@ int rayDiff(int argc, char *argv[]) std::cout << std::endl; std::cout << "saving out coloured red for matches beyond shoulder difference:" << std::endl; + // now render visuals + ray::CloudWriter writer; + if (!writer.begin(cloud1_name.nameStub() + "_diff.ply")) + return false; + + // By maintaining these buffers below, we avoid almost all memory fragmentation + ray::Cloud chunk; + std::map voxel_map; + std::vector samples; + int j = 0; Eigen::Vector3d diff_col(255,0,0); - for (int i = 0; i<(int)cloud1.ends.size(); i++) + std::vector *dists = &dists_to_cloud1; + auto colour = [&](std::vector &starts, std::vector &ends, + std::vector ×, std::vector &colours) { - if (cloud1.rayBounded(i)) + // firstly we store a count per cell + chunk.starts = starts; + chunk.ends = ends; + chunk.times = times; + chunk.colours = colours; + for (size_t i = 0; i min_error_dist) + if (colours[i].alpha > 0) { - double proximity = min_error_dist / dists_to_cloud1[j]; - Eigen::Vector3d col(cloud1.colours[i].red, cloud1.colours[i].green, cloud1.colours[i].blue); - Eigen::Vector3d new_col = diff_col + (col - diff_col) * proximity; - cloud1.colours[i] = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), cloud1.colours[i].alpha); + if ((*dists)[j] > min_error_dist) + { + double proximity = min_error_dist / (*dists)[j]; + Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); + Eigen::Vector3d new_col = diff_col + (col - diff_col) * proximity; + chunk.colours[i] = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); + } + j++; } - j++; } - } + writer.writeChunk(chunk); + }; + + if (!ray::Cloud::read(cloud1_name.name(), colour)) + return false; j = 0; + dists_to_cloud1.clear(); + dists_to_cloud1.shrink_to_fit(); + Eigen::Vector3d diff2_col(0,255,0); + + dists = &dists_to_cloud2; if (unified.isSet()) { - Eigen::Vector3d diff2_col(0,255,0); - int imin = cloud1.ends.size(); - cloud1.starts.insert(cloud1.starts.end(), cloud2.starts.begin(), cloud2.starts.end()); - cloud1.ends.insert(cloud1.ends.end(), cloud2.ends.begin(), cloud2.ends.end()); - cloud1.times.insert(cloud1.times.end(), cloud2.times.begin(), cloud2.times.end()); - cloud1.colours.insert(cloud1.colours.end(), cloud2.colours.begin(), cloud2.colours.end()); - for (int i = 0; i<(int)cloud2.ends.size(); i++) + + auto colour2 = [&](std::vector &starts, std::vector &ends, + std::vector ×, std::vector &colours) { - int I = i + imin; - if (cloud2.rayBounded(i)) + // firstly we store a count per cell + chunk.starts = starts; + chunk.ends = ends; + chunk.times = times; + chunk.colours = colours; + for (size_t i = 0; i min_error_dist) + if (colours[i].alpha > 0) { - double proximity = min_error_dist / dists_to_cloud2[j]; - Eigen::Vector3d col(cloud2.colours[i].red, cloud2.colours[i].green, cloud2.colours[i].blue); - Eigen::Vector3d new_col = diff2_col + (col - diff2_col) * proximity; - cloud1.colours[I] = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), cloud2.colours[i].alpha); + if ((*dists)[j] > min_error_dist) + { + double proximity = min_error_dist / (*dists)[j]; + Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); + Eigen::Vector3d new_col = diff2_col + (col - diff2_col) * proximity; + chunk.colours[i] = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); + } + j++; } - j++; } - } - cloud1.save(cloud1_name.nameStub() + "_diff.ply"); + writer.writeChunk(chunk); + }; + + if (!ray::Cloud::read(cloud2_name.name(), colour2)) + return false; + writer.end(); + if (visualise.isSet()) { std::string command = std::string(VISUALISE_TOOL) + std::string(" ") + cloud1_name.nameStub() + "_diff.ply"; @@ -295,22 +373,14 @@ int rayDiff(int argc, char *argv[]) } else { - cloud1.save(cloud1_name.nameStub() + "_diff.ply"); - for (int i = 0; i<(int)cloud2.ends.size(); i++) - { - if (cloud2.rayBounded(i)) - { - if (dists_to_cloud2[j] > min_error_dist) - { - double proximity = min_error_dist / dists_to_cloud2[j]; - Eigen::Vector3d col(cloud2.colours[i].red, cloud2.colours[i].green, cloud2.colours[i].blue); - Eigen::Vector3d new_col = diff_col + (col - diff_col) * proximity; - cloud2.colours[i] = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), cloud2.colours[i].alpha); - } - j++; - } - } - cloud2.save(cloud2_name.nameStub() + "_diff.ply"); + writer.end(); + diff_col = diff2_col; + if (!writer.begin(cloud2_name.nameStub() + "_diff.ply")) + return false; + + if (!ray::Cloud::read(cloud2_name.name(), colour)) + return false; + writer.end(); if (visualise.isSet()) { std::string command = std::string(VISUALISE_TOOL) + std::string(" ") + cloud1_name.nameStub() + "_diff.ply " + cloud2_name.nameStub() + "_diff.ply"; From fd029de57fdd6bf59a96043a405f2055335088ad Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Wed, 27 May 2026 09:05:36 +1000 Subject: [PATCH 07/37] tweaked raydiff to produce a combined cloud by default, and exclude identical points --- raycloudtools/raydiff/raydiff.cpp | 56 +++++++++++++++++-------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/raycloudtools/raydiff/raydiff.cpp b/raycloudtools/raydiff/raydiff.cpp index f09fcda03..b31b99561 100644 --- a/raycloudtools/raydiff/raydiff.cpp +++ b/raycloudtools/raydiff/raydiff.cpp @@ -26,11 +26,11 @@ void usage(int exit_code = 1) { // clang-format off - std::cout << "Difference between two ray clouds, differences coloured red, and similarity printed to screen. Optional visualisation." << std::endl; + std::cout << "Difference between two ray clouds output a coloured cloud, cloud1 differences in red, cloud2 differences in green, and similarity printed to screen." << std::endl; std::cout << "usage:" << std::endl; std::cout << "raydiff cloud1.ply cloud2.ply" << std::endl; std::cout << " --distance 0 - optional threshold in m for colouring differences. Default auto-detects distribution shoulder" << std::endl; - std::cout << " --unified - show as single cloud. Red for removed geometry, green for added." << std::endl; + std::cout << " --individual - output as two clouds, to show differences on each." << std::endl; std::cout << " --visualise - open in the default visualisation tool" << std::endl; // clang-format on exit(exit_code); @@ -147,10 +147,10 @@ double getShoulder(double k, std::vector sorted_dists, double &min_error_ int rayDiff(int argc, char *argv[]) { ray::FileArgument cloud1_name, cloud2_name; - ray::OptionalFlagArgument visualise("visualise", 'v'), unified("unified", 'u'); + ray::OptionalFlagArgument visualise("visualise", 'v'), individual("individual", 'i'); ray::DoubleArgument distance_threshold(0.0, 1000.0, 0.0); ray::OptionalKeyValueArgument distance_option("distance", 'd', &distance_threshold); - if (!ray::parseCommandLine(argc, argv, { &cloud1_name, &cloud2_name }, { &distance_option, &visualise, &unified })) + if (!ray::parseCommandLine(argc, argv, { &cloud1_name, &cloud2_name }, { &distance_option, &visualise, &individual })) { usage(); } @@ -226,7 +226,7 @@ int rayDiff(int argc, char *argv[]) else { // prune out distance 0 - for (int i = sorted_dists.size()-1; i>=0; i--) + for (int i = (int)sorted_dists.size()-1; i>=0; i--) { if (sorted_dists[i] <= 0.0) { @@ -300,27 +300,31 @@ int rayDiff(int argc, char *argv[]) int j = 0; Eigen::Vector3d diff_col(255,0,0); std::vector *dists = &dists_to_cloud1; + const float eps = 1e-8f; // in case there is inaccuracy in the KNN distance estimation for co-located point pairs auto colour = [&](std::vector &starts, std::vector &ends, std::vector ×, std::vector &colours) { // firstly we store a count per cell - chunk.starts = starts; - chunk.ends = ends; - chunk.times = times; - chunk.colours = colours; + chunk.clear(); for (size_t i = 0; i 0) { - if ((*dists)[j] > min_error_dist) + if ((*dists)[j] > eps) { - double proximity = min_error_dist / (*dists)[j]; - Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); - Eigen::Vector3d new_col = diff_col + (col - diff_col) * proximity; - chunk.colours[i] = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); + chunk.addRay(starts[i], ends[i], times[i], colours[i]); + if ((*dists)[j] > min_error_dist) + { + double proximity = min_error_dist / (*dists)[j]; + Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); + Eigen::Vector3d new_col = diff_col + (col - diff_col) * proximity; + chunk.colours.back() = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); + } } j++; } + else + chunk.addRay(starts[i], ends[i], times[i], colours[i]); } writer.writeChunk(chunk); }; @@ -333,30 +337,32 @@ int rayDiff(int argc, char *argv[]) Eigen::Vector3d diff2_col(0,255,0); dists = &dists_to_cloud2; - if (unified.isSet()) + if (!individual.isSet()) { - auto colour2 = [&](std::vector &starts, std::vector &ends, std::vector ×, std::vector &colours) { // firstly we store a count per cell - chunk.starts = starts; - chunk.ends = ends; - chunk.times = times; - chunk.colours = colours; + chunk.clear(); for (size_t i = 0; i 0) { - if ((*dists)[j] > min_error_dist) + if ((*dists)[j] > eps) { - double proximity = min_error_dist / (*dists)[j]; - Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); - Eigen::Vector3d new_col = diff2_col + (col - diff2_col) * proximity; - chunk.colours[i] = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); + chunk.addRay(starts[i], ends[i], times[i], colours[i]); + if ((*dists)[j] > min_error_dist) + { + double proximity = min_error_dist / (*dists)[j]; + Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); + Eigen::Vector3d new_col = diff2_col + (col - diff2_col) * proximity; + chunk.colours[i] = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); + } } j++; } + else + chunk.addRay(starts[i], ends[i], times[i], colours[i]); } writer.writeChunk(chunk); }; From 42979585412f41d146c70bb9e867dcb2e0bf4111 Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Wed, 27 May 2026 09:45:57 +1000 Subject: [PATCH 08/37] stage 1 complete --- raycloudtools/raydiff/raydiff.cpp | 225 ++--------------------------- raylib/CMakeLists.txt | 2 + raylib/raydiffer.cpp | 226 ++++++++++++++++++++++++++++++ raylib/raydiffer.h | 25 ++++ 4 files changed, 266 insertions(+), 212 deletions(-) create mode 100644 raylib/raydiffer.cpp create mode 100644 raylib/raydiffer.h diff --git a/raycloudtools/raydiff/raydiff.cpp b/raycloudtools/raydiff/raydiff.cpp index b31b99561..589a21542 100644 --- a/raycloudtools/raydiff/raydiff.cpp +++ b/raycloudtools/raydiff/raydiff.cpp @@ -16,12 +16,7 @@ #include "raylib/raycuboid.h" #include "raylib/rayply.h" #include "raylib/raycloudwriter.h" -// we look for a uniform distribution of best fit given a correlation dimension k -// if the points are in a line then k=1 -// if the points are in a plane then k=2 -// if the points are in a volume then k=3 -// this macro finds the k that best fit the uniform distribution. Otherwise we default to k=2 -#define FIND_CORRELATION_DIMENSION +#include "raylib/raydiffer.h" void usage(int exit_code = 1) { @@ -36,114 +31,6 @@ void usage(int exit_code = 1) exit(exit_code); } -void calcNearestNeighbourDistances(const std::vector &cloud1, const std::vector &cloud2, std::vector &distances) -{ - Nabo::NNSearchF *nns; - // Nabo::Parameters params("bucketSize", 8); - int num_bounded = 0, num_bounded2 = 0; - for (unsigned int i = 0; i < cloud1.size(); i++) - if (cloud1[i][0] != std::numeric_limits::max()) - num_bounded++; - for (unsigned int i = 0; i < cloud2.size(); i++) - if (cloud2[i][0] != std::numeric_limits::max()) - num_bounded2++; - Eigen::MatrixXf points_p(3, num_bounded); - int j = 0; - for (unsigned int i = 0; i < cloud1.size(); i++) - { - if (cloud1[i][0] != std::numeric_limits::max()) - points_p.col(j++) = cloud1[i]; - } - nns = Nabo::NNSearchF::createKDTreeLinearHeap(points_p, 3); - - Eigen::MatrixXf points_q(3, num_bounded2); - j = 0; - for (unsigned int i = 0; i < cloud2.size(); i++) - { - if (cloud2[i][0] != std::numeric_limits::max()) - points_q.col(j++) = cloud2[i]; - } - Eigen::MatrixXi indices; - Eigen::MatrixXf dists2; - // Run the search - const int search_size = 1; - indices.resize(search_size, num_bounded2); - dists2.resize(search_size, num_bounded2); - nns->knn(points_q, indices, dists2, search_size, (float)ray::kNearestNeighbourEpsilon, Nabo::NNSearchF::SearchOptionFlags::SORT_RESULTS | Nabo::NNSearchF::SearchOptionFlags::ALLOW_SELF_MATCH); - delete nns; - distances.resize(num_bounded2); - for (int i = 0; i sorted_dists, double &min_error_dist, double &similarity) -{ - int num = (int)sorted_dists.size(); - - // 1. transform the result to make the distances uniform if their 3D points are uniformly distributed - for (int i = 0; i outside_const(num+1, 0.0); - std::vector outside_linear(num+1, 0.0); - std::vector outside_square(num+1, 0.0); - for (int i = num-1; i>=0; i--) - { - double d = sorted_dists.back() - sorted_dists[i]; - double yN = (num-1); - double I = (float)i - yN; - outside_const[i] = (float)((double)outside_const[i+1] + I*I); - outside_linear[i] = (float)((double)outside_linear[i+1] + 2.0*I*d); - outside_square[i] =(float)((double)outside_square[i+1] + d*d); - } - - // 3. accumulate linear term forwards, but store only best results - double inside_const = 0.0; - double inside_linear = 0.0; - double inside_square = 0.0; - double min_error_sqr = 0.0; - double min_error_i = 0.0; - min_error_dist = 0.0; - for (int i = 0; i points1, points2; @@ -184,106 +75,16 @@ int rayDiff(int argc, char *argv[]) if (!ray::Cloud::read(cloud2_name.name(), getPoints)) return false; + // Primary goal - get an overall similarity percentage and return it as the return value, so it can be used in bash scripts // this needs to include colour and location, but be insensitive to changes in density. i.e. it needs to work usefully on a repeat scan. std::vector dists_to_cloud2, dists_to_cloud1; - std::cout << "calculating cloud2 neighbours of cloud1..." << std::endl; - calcNearestNeighbourDistances(points1, points2, dists_to_cloud2); - std::cout << "calculating cloud1 neighbours of cloud2..." << std::endl; - calcNearestNeighbourDistances(points2, points1, dists_to_cloud1); + ray::getDistancesBetweenPoints(points1, points2, dists_to_cloud1, dists_to_cloud2); points1.clear(); points1.shrink_to_fit(); points2.clear(); points2.shrink_to_fit(); - // 1. max distance - float max_dist = 0.0; - for (int i = 0; i<(int)dists_to_cloud2.size(); i++) - max_dist = std::max(max_dist, dists_to_cloud2[i]); - for (int i = 0; i<(int)dists_to_cloud1.size(); i++) - max_dist = std::max(max_dist, dists_to_cloud1[i]); - - std::vector sorted_dists = dists_to_cloud2; - sorted_dists.insert(sorted_dists.end(), dists_to_cloud1.begin(), dists_to_cloud1.end()); - - double min_error_dist = distance_threshold.value(); - double similarity = 0; - - if (distance_option.isSet()) - { - std::cout << std::endl; - std::cout << "Differences:" << std::setprecision(3) << std::fixed << std::endl; - std::cout << std::endl; - int inside_count = 0; - for (int i = 0; i<(int)sorted_dists.size(); i++) - { - if (sorted_dists[i] <= min_error_dist) - { - inside_count++; - } - } - similarity = 100.0*(double)inside_count / (int)sorted_dists.size(); - std::cout << " specified difference: " << min_error_dist << " m,\t" << similarity << "% inside" << std::endl; - } - else - { - // prune out distance 0 - for (int i = (int)sorted_dists.size()-1; i>=0; i--) - { - if (sorted_dists[i] <= 0.0) - { - sorted_dists[i] = sorted_dists.back(); - sorted_dists.pop_back(); - } - } - if (sorted_dists.empty()) - { - std::cerr << "No difference detect between files. Exiting" << std::endl; - return 0; - } - std::cout << "sorting differences..." << std::endl; - std::sort(sorted_dists.begin(), sorted_dists.end()); - double min_error = 0; - int min_i = 0; - double k = 2.0; // good default as point clouds are typically surfaces - #if defined FIND_CORRELATION_DIMENSION - double errors[5]; - std::cout << "(uniform deviations: "; - for (int i = 0; i<5; i++) - { - double k = 1.0 + (double)i/2.0; - errors[i] = getShoulder(k, sorted_dists, min_error_dist, similarity); - std::cout << "k " << k << ": " << errors[i]; - if (errors[i] < min_error || i==0) - { - min_error = errors[i]; - min_i = i; - } - } - std::cout << ")" << std::endl; - k = 1.0 + min_i/2.0; - min_i = std::max(1, std::min(min_i, 3)); // so we can interpolate - - double y0 = errors[min_i-1]; - double y1 = errors[min_i]; - double y2 = errors[min_i+1]; - double den = y2 - 2.0*y1 + y0; - if (den > 0.0) - { - double xmin = (y2 - 4.0*y1 + 3.0*y0)/(2.0*den); - xmin = std::max(0.0, std::min(xmin, 2.0)); // clamp - double I = (double)min_i - 1.0 + xmin; - k = 1.0 + I/2.0; - } - #endif - - std::cout << std::endl; - std::cout << "Differences:" << std::setprecision(3) << std::fixed << std::endl; - std::cout << std::endl; - min_error = getShoulder(k, sorted_dists, min_error_dist, similarity); - std::cout << " shoulder difference: " << min_error_dist << " m,\t" << similarity << "% inside, correlation dimension: " << k << std::endl; - } - std::cout << " median difference: " << sorted_dists[sorted_dists.size()/2] << " m" << std::endl; - std::cout << " max difference: " << max_dist << " m" << std::endl; - std::cout << std::endl; + double dist_threshold = distance_threshold.value(); + double similarity = ray::printDistanceStatistics(dists_to_cloud1, dists_to_cloud2, dist_threshold); std::cout << "saving out coloured red for matches beyond shoulder difference:" << std::endl; @@ -313,9 +114,9 @@ int rayDiff(int argc, char *argv[]) if ((*dists)[j] > eps) { chunk.addRay(starts[i], ends[i], times[i], colours[i]); - if ((*dists)[j] > min_error_dist) + if ((*dists)[j] > dist_threshold) { - double proximity = min_error_dist / (*dists)[j]; + double proximity = dist_threshold / (*dists)[j]; Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); Eigen::Vector3d new_col = diff_col + (col - diff_col) * proximity; chunk.colours.back() = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); @@ -351,9 +152,9 @@ int rayDiff(int argc, char *argv[]) if ((*dists)[j] > eps) { chunk.addRay(starts[i], ends[i], times[i], colours[i]); - if ((*dists)[j] > min_error_dist) + if ((*dists)[j] > dist_threshold) { - double proximity = min_error_dist / (*dists)[j]; + double proximity = dist_threshold / (*dists)[j]; Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); Eigen::Vector3d new_col = diff2_col + (col - diff2_col) * proximity; chunk.colours[i] = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); diff --git a/raylib/CMakeLists.txt b/raylib/CMakeLists.txt index 22738f83a..dd30cf9b0 100644 --- a/raylib/CMakeLists.txt +++ b/raylib/CMakeLists.txt @@ -20,6 +20,7 @@ set(PUBLIC_HEADERS rayconcavehull.h rayconvexhull.h raydecimation.h + raydiffer.h rayellipsoid.h rayfinealignment.h rayforestgen.h @@ -73,6 +74,7 @@ set(SOURCES rayconcavehull.cpp rayconvexhull.cpp raydecimation.cpp + raydiffer.cpp rayellipsoid.cpp rayfinealignment.cpp rayforestgen.cpp diff --git a/raylib/raydiffer.cpp b/raylib/raydiffer.cpp new file mode 100644 index 000000000..d663e212e --- /dev/null +++ b/raylib/raydiffer.cpp @@ -0,0 +1,226 @@ +// Copyright (c) 2026 +// Commonwealth Scientific and Industrial Research Organisation (CSIRO) +// ABN 41 687 119 230 +// +// Author: Tom Lowe +#include +#include "raydiffer.h" +// we look for a uniform distribution of best fit given a correlation dimension k +// if the points are in a line then k=1 +// if the points are in a plane then k=2 +// if the points are in a volume then k=3 +// this macro finds the k that best fit the uniform distribution. Otherwise we default to k=2 +#define FIND_CORRELATION_DIMENSION + +namespace ray +{ +void calcNearestNeighbourDistances(const std::vector &cloud1, const std::vector &cloud2, std::vector &distances) +{ + Nabo::NNSearchF *nns; + // Nabo::Parameters params("bucketSize", 8); + int num_bounded = 0, num_bounded2 = 0; + for (unsigned int i = 0; i < cloud1.size(); i++) + if (cloud1[i][0] != std::numeric_limits::max()) + num_bounded++; + for (unsigned int i = 0; i < cloud2.size(); i++) + if (cloud2[i][0] != std::numeric_limits::max()) + num_bounded2++; + Eigen::MatrixXf points_p(3, num_bounded); + int j = 0; + for (unsigned int i = 0; i < cloud1.size(); i++) + { + if (cloud1[i][0] != std::numeric_limits::max()) + points_p.col(j++) = cloud1[i]; + } + nns = Nabo::NNSearchF::createKDTreeLinearHeap(points_p, 3); + + Eigen::MatrixXf points_q(3, num_bounded2); + j = 0; + for (unsigned int i = 0; i < cloud2.size(); i++) + { + if (cloud2[i][0] != std::numeric_limits::max()) + points_q.col(j++) = cloud2[i]; + } + Eigen::MatrixXi indices; + Eigen::MatrixXf dists2; + // Run the search + const int search_size = 1; + indices.resize(search_size, num_bounded2); + dists2.resize(search_size, num_bounded2); + nns->knn(points_q, indices, dists2, search_size, (float)ray::kNearestNeighbourEpsilon, Nabo::NNSearchF::SearchOptionFlags::SORT_RESULTS | Nabo::NNSearchF::SearchOptionFlags::ALLOW_SELF_MATCH); + delete nns; + distances.resize(num_bounded2); + for (int i = 0; i &points1, const std::vector &points2, std::vector &dists_to_cloud1, std::vector &dists_to_cloud2) +{ + std::cout << "calculating cloud2 neighbours of cloud1..." << std::endl; + calcNearestNeighbourDistances(points1, points2, dists_to_cloud2); + std::cout << "calculating cloud1 neighbours of cloud2..." << std::endl; + calcNearestNeighbourDistances(points2, points1, dists_to_cloud1); +} + +double getShoulder(double k, std::vector sorted_dists, double &min_error_dist, double &similarity) +{ + int num = (int)sorted_dists.size(); + + // 1. transform the result to make the distances uniform if their 3D points are uniformly distributed + for (int i = 0; i outside_const(num+1, 0.0); + std::vector outside_linear(num+1, 0.0); + std::vector outside_square(num+1, 0.0); + for (int i = num-1; i>=0; i--) + { + double d = sorted_dists.back() - sorted_dists[i]; + double yN = (num-1); + double I = (float)i - yN; + outside_const[i] = (float)((double)outside_const[i+1] + I*I); + outside_linear[i] = (float)((double)outside_linear[i+1] + 2.0*I*d); + outside_square[i] =(float)((double)outside_square[i+1] + d*d); + } + + // 3. accumulate linear term forwards, but store only best results + double inside_const = 0.0; + double inside_linear = 0.0; + double inside_square = 0.0; + double min_error_sqr = 0.0; + double min_error_i = 0.0; + min_error_dist = 0.0; + for (int i = 0; i &dists_to_cloud1, const std::vector &dists_to_cloud2, double &distance_threshold) +{ + // 1. max distance + float max_dist = 0.0; + for (int i = 0; i<(int)dists_to_cloud2.size(); i++) + max_dist = std::max(max_dist, dists_to_cloud2[i]); + for (int i = 0; i<(int)dists_to_cloud1.size(); i++) + max_dist = std::max(max_dist, dists_to_cloud1[i]); + + std::vector sorted_dists = dists_to_cloud2; + sorted_dists.insert(sorted_dists.end(), dists_to_cloud1.begin(), dists_to_cloud1.end()); + + double similarity = 0; + + if (distance_threshold > 0.0) + { + std::cout << std::endl; + std::cout << "Differences:" << std::setprecision(3) << std::fixed << std::endl; + std::cout << std::endl; + int inside_count = 0; + for (int i = 0; i<(int)sorted_dists.size(); i++) + { + if (sorted_dists[i] <= distance_threshold) + { + inside_count++; + } + } + similarity = 100.0*(double)inside_count / (int)sorted_dists.size(); + std::cout << " specified difference: " << distance_threshold << " m,\t" << similarity << "% inside" << std::endl; + } + else + { + // prune out distance 0 + for (int i = (int)sorted_dists.size()-1; i>=0; i--) + { + if (sorted_dists[i] <= 0.0) + { + sorted_dists[i] = sorted_dists.back(); + sorted_dists.pop_back(); + } + } + if (sorted_dists.empty()) + { + std::cerr << "No difference detect between files. Exiting" << std::endl; + return 100.0; + } + std::cout << "sorting differences..." << std::endl; + std::sort(sorted_dists.begin(), sorted_dists.end()); + double min_error = 0; + int min_i = 0; + double k = 2.0; // good default as point clouds are typically surfaces + #if defined FIND_CORRELATION_DIMENSION + double errors[5]; + std::cout << "(uniform deviations: "; + for (int i = 0; i<5; i++) + { + double k = 1.0 + (double)i/2.0; + errors[i] = getShoulder(k, sorted_dists, distance_threshold, similarity); + std::cout << "k " << k << ": " << errors[i]; + if (errors[i] < min_error || i==0) + { + min_error = errors[i]; + min_i = i; + } + } + std::cout << ")" << std::endl; + k = 1.0 + min_i/2.0; + min_i = std::max(1, std::min(min_i, 3)); // so we can interpolate + + double y0 = errors[min_i-1]; + double y1 = errors[min_i]; + double y2 = errors[min_i+1]; + double den = y2 - 2.0*y1 + y0; + if (den > 0.0) + { + double xmin = (y2 - 4.0*y1 + 3.0*y0)/(2.0*den); + xmin = std::max(0.0, std::min(xmin, 2.0)); // clamp + double I = (double)min_i - 1.0 + xmin; + k = 1.0 + I/2.0; + } + #endif + + std::cout << std::endl; + std::cout << "Differences:" << std::setprecision(3) << std::fixed << std::endl; + std::cout << std::endl; + min_error = getShoulder(k, sorted_dists, distance_threshold, similarity); + std::cout << " shoulder difference: " << distance_threshold << " m,\t" << similarity << "% inside, correlation dimension: " << k << std::endl; + } + std::cout << " median difference: " << sorted_dists[sorted_dists.size()/2] << " m" << std::endl; + std::cout << " max difference: " << max_dist << " m" << std::endl; + std::cout << std::endl; + return similarity; +} +} // namespace ray diff --git a/raylib/raydiffer.h b/raylib/raydiffer.h new file mode 100644 index 000000000..851179663 --- /dev/null +++ b/raylib/raydiffer.h @@ -0,0 +1,25 @@ +// Copyright (c) 2026 +// Commonwealth Scientific and Industrial Research Organisation (CSIRO) +// ABN 41 687 119 230 +// +// Author: Tom Lowe +#ifndef RAYDIFFER_H +#define RAYDIFFER_H + +#include "raylib/raylibconfig.h" +#include "raylib/rayutils.h" + +namespace ray +{ + +/// Get vector of closest distances between the point pairs +/// And point marked with float_max in their x component are excluded +void RAYLIB_EXPORT getDistancesBetweenPoints(const std::vector &points1, const std::vector &points2, std::vector &dists_to_cloud1, std::vector &dists_to_cloud2); + +/// Print statistics on the distances, and adjust the distance_threshold if it is 0 to be at the statistical shoulder (bi-uniform fit) +/// returns the similarity (percentage under the distance threshold) +double RAYLIB_EXPORT printDistanceStatistics(const std::vector &dists_to_cloud1, const std::vector &dists_to_cloud2, double &distance_threshold); + +} // namespace ray + +#endif // RAYDIFFER_H From 09fd8854496685c6ef2249d1aeaacebb43955b91 Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Wed, 27 May 2026 10:17:50 +1000 Subject: [PATCH 09/37] good --- raycloudtools/raydiff/raydiff.cpp | 112 +------------------------- raylib/raydiffer.cpp | 128 ++++++++++++++++++++++++++++-- raylib/raydiffer.h | 7 ++ 3 files changed, 132 insertions(+), 115 deletions(-) diff --git a/raycloudtools/raydiff/raydiff.cpp b/raycloudtools/raydiff/raydiff.cpp index 589a21542..64908f70d 100644 --- a/raycloudtools/raydiff/raydiff.cpp +++ b/raycloudtools/raydiff/raydiff.cpp @@ -8,14 +8,10 @@ #include #include #include -#include #include #include "raylib/raycloud.h" #include "raylib/rayparse.h" -#include "raylib/raycuboid.h" -#include "raylib/rayply.h" -#include "raylib/raycloudwriter.h" #include "raylib/raydiffer.h" void usage(int exit_code = 1) @@ -85,114 +81,10 @@ int rayDiff(int argc, char *argv[]) double dist_threshold = distance_threshold.value(); double similarity = ray::printDistanceStatistics(dists_to_cloud1, dists_to_cloud2, dist_threshold); - - std::cout << "saving out coloured red for matches beyond shoulder difference:" << std::endl; - // now render visuals - ray::CloudWriter writer; - if (!writer.begin(cloud1_name.nameStub() + "_diff.ply")) - return false; - - // By maintaining these buffers below, we avoid almost all memory fragmentation - ray::Cloud chunk; - std::map voxel_map; - std::vector samples; - - int j = 0; - Eigen::Vector3d diff_col(255,0,0); - std::vector *dists = &dists_to_cloud1; - const float eps = 1e-8f; // in case there is inaccuracy in the KNN distance estimation for co-located point pairs - auto colour = [&](std::vector &starts, std::vector &ends, - std::vector ×, std::vector &colours) - { - // firstly we store a count per cell - chunk.clear(); - for (size_t i = 0; i 0) - { - if ((*dists)[j] > eps) - { - chunk.addRay(starts[i], ends[i], times[i], colours[i]); - if ((*dists)[j] > dist_threshold) - { - double proximity = dist_threshold / (*dists)[j]; - Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); - Eigen::Vector3d new_col = diff_col + (col - diff_col) * proximity; - chunk.colours.back() = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); - } - } - j++; - } - else - chunk.addRay(starts[i], ends[i], times[i], colours[i]); - } - writer.writeChunk(chunk); - }; - - if (!ray::Cloud::read(cloud1_name.name(), colour)) - return false; - j = 0; - dists_to_cloud1.clear(); - dists_to_cloud1.shrink_to_fit(); - Eigen::Vector3d diff2_col(0,255,0); - - dists = &dists_to_cloud2; - if (!individual.isSet()) + if (!ray::writeDifferencesToRayClouds(cloud1_name.nameStub(), cloud2_name.nameStub(), dists_to_cloud1, dists_to_cloud2, dist_threshold, individual.isSet(), visualise.isSet())) { - auto colour2 = [&](std::vector &starts, std::vector &ends, - std::vector ×, std::vector &colours) - { - // firstly we store a count per cell - chunk.clear(); - for (size_t i = 0; i 0) - { - if ((*dists)[j] > eps) - { - chunk.addRay(starts[i], ends[i], times[i], colours[i]); - if ((*dists)[j] > dist_threshold) - { - double proximity = dist_threshold / (*dists)[j]; - Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); - Eigen::Vector3d new_col = diff2_col + (col - diff2_col) * proximity; - chunk.colours[i] = ray::RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); - } - } - j++; - } - else - chunk.addRay(starts[i], ends[i], times[i], colours[i]); - } - writer.writeChunk(chunk); - }; - - if (!ray::Cloud::read(cloud2_name.name(), colour2)) - return false; - writer.end(); - - if (visualise.isSet()) - { - std::string command = std::string(VISUALISE_TOOL) + std::string(" ") + cloud1_name.nameStub() + "_diff.ply"; - system(command.c_str()); - } - } - else - { - writer.end(); - diff_col = diff2_col; - if (!writer.begin(cloud2_name.nameStub() + "_diff.ply")) - return false; - - if (!ray::Cloud::read(cloud2_name.name(), colour)) - return false; - writer.end(); - if (visualise.isSet()) - { - std::string command = std::string(VISUALISE_TOOL) + std::string(" ") + cloud1_name.nameStub() + "_diff.ply " + cloud2_name.nameStub() + "_diff.ply"; - system(command.c_str()); - } + return 1; } return (int)similarity; diff --git a/raylib/raydiffer.cpp b/raylib/raydiffer.cpp index d663e212e..e6d17b6e1 100644 --- a/raylib/raydiffer.cpp +++ b/raylib/raydiffer.cpp @@ -5,6 +5,9 @@ // Author: Tom Lowe #include #include "raydiffer.h" +#include "raylib/raycloud.h" +#include "raylib/rayply.h" +#include "raylib/raycloudwriter.h" // we look for a uniform distribution of best fit given a correlation dimension k // if the points are in a line then k=1 // if the points are in a plane then k=2 @@ -47,7 +50,7 @@ void calcNearestNeighbourDistances(const std::vector &cloud1, c const int search_size = 1; indices.resize(search_size, num_bounded2); dists2.resize(search_size, num_bounded2); - nns->knn(points_q, indices, dists2, search_size, (float)ray::kNearestNeighbourEpsilon, Nabo::NNSearchF::SearchOptionFlags::SORT_RESULTS | Nabo::NNSearchF::SearchOptionFlags::ALLOW_SELF_MATCH); + nns->knn(points_q, indices, dists2, search_size, (float)kNearestNeighbourEpsilon, Nabo::NNSearchF::SearchOptionFlags::SORT_RESULTS | Nabo::NNSearchF::SearchOptionFlags::ALLOW_SELF_MATCH); delete nns; distances.resize(num_bounded2); for (int i = 0; i sorted_dists, double &min_error_ min_error_dist = 0.0; for (int i = 0; i &dists_to_cloud1, const std::cout << std::endl; return similarity; } + +bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std::string &cloud2_namestub, std::vector &dists_to_cloud1, + std::vector &dists_to_cloud2, double dist_threshold, bool individual_files, bool visualise) +{ + std::cout << "saving out differences, coloured red for differences to " << cloud1_namestub << ".ply and green for differences in " << cloud2_namestub << ".ply" << std::endl; + + // now render visuals + CloudWriter writer; + if (!writer.begin(cloud1_namestub + "_diff.ply")) + return false; + + // By maintaining these buffers below, we avoid almost all memory fragmentation + Cloud chunk; + std::map voxel_map; + std::vector samples; + + int j = 0; + Eigen::Vector3d diff_col(255,0,0); + std::vector *dists = &dists_to_cloud1; + const float eps = 1e-8f; // in case there is inaccuracy in the KNN distance estimation for co-located point pairs + auto colour = [&](std::vector &starts, std::vector &ends, + std::vector ×, std::vector &colours) + { + // firstly we store a count per cell + chunk.clear(); + for (size_t i = 0; i 0) + { + if ((*dists)[j] > eps) + { + chunk.addRay(starts[i], ends[i], times[i], colours[i]); + if ((*dists)[j] > dist_threshold) + { + double proximity = dist_threshold / (*dists)[j]; + Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); + Eigen::Vector3d new_col = diff_col + (col - diff_col) * proximity; + chunk.colours.back() = RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); + } + } + j++; + } + else + chunk.addRay(starts[i], ends[i], times[i], colours[i]); + } + writer.writeChunk(chunk); + }; + + if (!Cloud::read(cloud1_namestub + ".ply", colour)) + return false; + j = 0; + dists_to_cloud1.clear(); + dists_to_cloud1.shrink_to_fit(); + Eigen::Vector3d diff2_col(0,255,0); + + dists = &dists_to_cloud2; + if (!individual_files) + { + auto colour2 = [&](std::vector &starts, std::vector &ends, + std::vector ×, std::vector &colours) + { + // firstly we store a count per cell + chunk.clear(); + for (size_t i = 0; i 0) + { + if ((*dists)[j] > eps) + { + chunk.addRay(starts[i], ends[i], times[i], colours[i]); + if ((*dists)[j] > dist_threshold) + { + double proximity = dist_threshold / (*dists)[j]; + Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); + Eigen::Vector3d new_col = diff2_col + (col - diff2_col) * proximity; + chunk.colours[i] = RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); + } + } + j++; + } + else + chunk.addRay(starts[i], ends[i], times[i], colours[i]); + } + writer.writeChunk(chunk); + }; + + if (!Cloud::read(cloud2_namestub + ".ply", colour2)) + return false; + writer.end(); + + if (visualise) + { + std::string command = std::string(VISUALISE_TOOL) + std::string(" ") + cloud1_namestub + "_diff.ply"; + system(command.c_str()); + } + } + else + { + writer.end(); + diff_col = diff2_col; + if (!writer.begin(cloud2_namestub + "_diff.ply")) + return false; + + if (!Cloud::read(cloud2_namestub + ".ply", colour)) + return false; + writer.end(); + if (visualise) + { + std::string command = std::string(VISUALISE_TOOL) + std::string(" ") + cloud1_namestub + "_diff.ply " + cloud2_namestub + "_diff.ply"; + system(command.c_str()); + } + } + return true; +} + } // namespace ray diff --git a/raylib/raydiffer.h b/raylib/raydiffer.h index 851179663..123755263 100644 --- a/raylib/raydiffer.h +++ b/raylib/raydiffer.h @@ -20,6 +20,13 @@ void RAYLIB_EXPORT getDistancesBetweenPoints(const std::vector /// returns the similarity (percentage under the distance threshold) double RAYLIB_EXPORT printDistanceStatistics(const std::vector &dists_to_cloud1, const std::vector &dists_to_cloud2, double &distance_threshold); +/// output differences as colours on the ray cloud: +/// 1. distances more than dist_threshold from cloud1 are red +/// 2. distances more than dist_threshold from cloud2 are green +/// 3. distances less than a tiny epsilon are not shown. So if you merge onto a large base map and raydiff with that map, it only shows where the differences are +bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std::string &cloud2_namestub, std::vector &dists_to_cloud1, + std::vector &dists_to_cloud2, double distance_threshold, bool individual_files, bool visualise); + } // namespace ray #endif // RAYDIFFER_H From 631d63c45ca0d79ece9017b356423156679d54d6 Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Wed, 27 May 2026 11:05:11 +1000 Subject: [PATCH 10/37] working nicely as a proper library function now --- raylib/raydiffer.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/raylib/raydiffer.cpp b/raylib/raydiffer.cpp index e6d17b6e1..358850c35 100644 --- a/raylib/raydiffer.cpp +++ b/raylib/raydiffer.cpp @@ -268,8 +268,6 @@ bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std:: } j++; } - else - chunk.addRay(starts[i], ends[i], times[i], colours[i]); } writer.writeChunk(chunk); }; @@ -301,13 +299,11 @@ bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std:: double proximity = dist_threshold / (*dists)[j]; Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); Eigen::Vector3d new_col = diff2_col + (col - diff2_col) * proximity; - chunk.colours[i] = RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); + chunk.colours.back() = RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); } } j++; } - else - chunk.addRay(starts[i], ends[i], times[i], colours[i]); } writer.writeChunk(chunk); }; From a557b579b981141229bfb7eec293767bb12cd62b Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Wed, 27 May 2026 14:54:44 +1000 Subject: [PATCH 11/37] minor improvements to raydiff --- raycloudtools/raydiff/raydiff.cpp | 4 ++-- raycloudtools/raysplit/CMakeLists.txt | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/raycloudtools/raydiff/raydiff.cpp b/raycloudtools/raydiff/raydiff.cpp index 64908f70d..f6157cca7 100644 --- a/raycloudtools/raydiff/raydiff.cpp +++ b/raycloudtools/raydiff/raydiff.cpp @@ -22,7 +22,7 @@ void usage(int exit_code = 1) std::cout << "raydiff cloud1.ply cloud2.ply" << std::endl; std::cout << " --distance 0 - optional threshold in m for colouring differences. Default auto-detects distribution shoulder" << std::endl; std::cout << " --individual - output as two clouds, to show differences on each." << std::endl; - std::cout << " --visualise - open in the default visualisation tool" << std::endl; + std::cout << " --view - open in the default visualisation tool" << std::endl; // clang-format on exit(exit_code); } @@ -30,7 +30,7 @@ void usage(int exit_code = 1) int rayDiff(int argc, char *argv[]) { ray::FileArgument cloud1_name, cloud2_name; - ray::OptionalFlagArgument visualise("visualise", 'v'), individual("individual", 'i'); + ray::OptionalFlagArgument visualise("view", 'v'), individual("individual", 'i'); ray::DoubleArgument distance_threshold(0.0, 1000.0, 0.0); ray::OptionalKeyValueArgument distance_option("distance", 'd', &distance_threshold); if (!ray::parseCommandLine(argc, argv, { &cloud1_name, &cloud2_name }, { &distance_option, &visualise, &individual })) diff --git a/raycloudtools/raysplit/CMakeLists.txt b/raycloudtools/raysplit/CMakeLists.txt index 1512b1e04..3b5bbacbd 100644 --- a/raycloudtools/raysplit/CMakeLists.txt +++ b/raycloudtools/raysplit/CMakeLists.txt @@ -1,7 +1,6 @@ set(SOURCES raysplit.cpp ) -add_compile_options(-ggdb -O0) ras_add_executable(raysplit LIBS raylib SOURCES ${SOURCES} From d23ad31d0c1cf2965b48dcc6d95b14168a1f5b0b Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Wed, 27 May 2026 14:59:54 +1000 Subject: [PATCH 12/37] one other --- raycloudtools/raydecimate/CMakeLists.txt | 1 - raycloudtools/rayextract/CMakeLists.txt | 1 - 2 files changed, 2 deletions(-) diff --git a/raycloudtools/raydecimate/CMakeLists.txt b/raycloudtools/raydecimate/CMakeLists.txt index 5a60e4528..d6365fa0c 100644 --- a/raycloudtools/raydecimate/CMakeLists.txt +++ b/raycloudtools/raydecimate/CMakeLists.txt @@ -1,7 +1,6 @@ set(SOURCES raydecimate.cpp ) -add_compile_options(-ggdb -O0) ras_add_executable(raydecimate LIBS raylib SOURCES ${SOURCES} diff --git a/raycloudtools/rayextract/CMakeLists.txt b/raycloudtools/rayextract/CMakeLists.txt index d576e8213..26cc5eabf 100644 --- a/raycloudtools/rayextract/CMakeLists.txt +++ b/raycloudtools/rayextract/CMakeLists.txt @@ -1,7 +1,6 @@ set(SOURCES rayextract.cpp ) -add_compile_options(-ggdb -O0) ras_add_executable(rayextract LIBS raylib From 7706cf3c8c497c15d3ab2332f30cc6566f4fe212 Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Thu, 28 May 2026 09:09:54 +1000 Subject: [PATCH 13/37] slight colour change to raydiff --- raylib/raydiffer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raylib/raydiffer.cpp b/raylib/raydiffer.cpp index 358850c35..53f0587b1 100644 --- a/raylib/raydiffer.cpp +++ b/raylib/raydiffer.cpp @@ -260,7 +260,7 @@ bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std:: chunk.addRay(starts[i], ends[i], times[i], colours[i]); if ((*dists)[j] > dist_threshold) { - double proximity = dist_threshold / (*dists)[j]; + double proximity = 0.25 + 0.75 * dist_threshold / (*dists)[j]; // go to only 75% so you can still see what it is Eigen::Vector3d col(colours[i].red, colours[i].green, colours[i].blue); Eigen::Vector3d new_col = diff_col + (col - diff_col) * proximity; chunk.colours.back() = RGBA((uint8_t)(new_col[0]+0.5), (uint8_t)(new_col[1]+0.5), (uint8_t)(new_col[2]+0.5), colours[i].alpha); From 539d1e8b98daccd3232ac3fc4741e2447377f31a Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Fri, 5 Jun 2026 13:33:53 +1000 Subject: [PATCH 14/37] made an optional flag across all commands to view the output immediately. This saves having to find what the output file is called and load it into a viewer --- CMakeLists.txt | 4 +++ raycloudtools/rayalign/rayalign.cpp | 13 ++++---- raycloudtools/raycolour/raycolour.cpp | 18 +++++++---- raycloudtools/raycombine/raycombine.cpp | 17 ++++++---- raycloudtools/raycreate/raycreate.cpp | 11 ++++--- raycloudtools/raydecimate/raydecimate.cpp | 10 +++--- raycloudtools/raydenoise/raydenoise.cpp | 9 ++++-- raycloudtools/raydiff/raydiff.cpp | 3 +- raycloudtools/rayextract/rayextract.cpp | 31 +++++++++++++------ raycloudtools/rayimport/rayimport.cpp | 15 +++++---- raycloudtools/rayrender/rayrender.cpp | 11 ++++--- raycloudtools/rayrotate/rayrotate.cpp | 7 +++-- raycloudtools/raysmooth/raysmooth.cpp | 11 ++++--- raycloudtools/raysplit/raysplit.cpp | 29 +++++++++-------- raycloudtools/raytransients/raytransients.cpp | 11 ++++--- raycloudtools/raytranslate/raytranslate.cpp | 14 +++++---- raycloudtools/raywrap/raywrap.cpp | 15 +++++---- raylib/rayutils.h | 13 ++++++++ 18 files changed, 154 insertions(+), 88 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 520f2138f..f7b5f83a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,10 @@ option(RAYCLOUD_BUILD_DOXYGEN "Build doxgen documentation?" OFF) option(RAYCLOUD_BUILD_TESTS "Build unit tests?" OFF) # Setup LeakTrack option(RAYCLOUD_LEAK_TRACK "Enable memory leak tracking?" OFF) +set(RAY_VISTOOL meshlab CACHE STRING "command to use to visualise point cloud / mesh files") +set(RAY_IMAGETOOL kolourpaint CACHE STRING "command to use to visualise image files") +add_compile_definitions(R_VISTOOL="${RAY_VISTOOL}") +add_compile_definitions(R_IMAGETOOL="${RAY_IMAGETOOL}") # WITH_ build options. option(WITH_LAS "With liblas for las file support?" OFF) diff --git a/raycloudtools/rayalign/rayalign.cpp b/raycloudtools/rayalign/rayalign.cpp index 2bf92cd60..645c93ce1 100644 --- a/raycloudtools/rayalign/rayalign.cpp +++ b/raycloudtools/rayalign/rayalign.cpp @@ -13,9 +13,6 @@ #include -#include -#include -#include #include #include @@ -24,10 +21,10 @@ void usage(int exit_code = 1) // clang-format off std::cout << "Align raycloudA onto raycloudB, rigidly. Outputs the transformed version of raycloudA." << std::endl; std::cout << "This method is for when there is more than approximately 30% overlap between clouds." << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "rayalign raycloudA raycloudB" << std::endl; std::cout << " --nonrigid - nonrigid (quadratic) alignment" << std::endl; - std::cout << " --verbose - outputs FFT images and the coarse alignment cloud" << std::endl; + std::cout << " --debug - outputs FFT images and the coarse alignment cloud" << std::endl; std::cout << " --local - fine alignment only, assumes clouds are already approximately aligned" << std::endl; std::cout << "rayalign raycloud - axis aligns to the walls, placing the major walls at (0,0,0), biggest along y." << std::endl; // clang-format on @@ -37,8 +34,8 @@ void usage(int exit_code = 1) int rayAlign(int argc, char *argv[]) { ray::FileArgument cloud_a, cloud_b; - ray::OptionalFlagArgument nonrigid("nonrigid", 'n'), is_verbose("verbose", 'v'), local("local", 'l'); - bool cross_align = ray::parseCommandLine(argc, argv, { &cloud_a, &cloud_b }, { &nonrigid, &is_verbose, &local }); + ray::OptionalFlagArgument nonrigid("nonrigid", 'n'), is_verbose("debug", 'd'), local("local", 'l'), view_flag("view", 'v'); + bool cross_align = ray::parseCommandLine(argc, argv, { &cloud_a, &cloud_b }, { &nonrigid, &is_verbose, &local, &view_flag }); bool self_align = ray::parseCommandLine(argc, argv, { &cloud_a }); if (!cross_align && !self_align) usage(); @@ -103,6 +100,8 @@ int rayAlign(int argc, char *argv[]) clouds[0].save(aligned_name); } + if (view_flag.isSet()) + ray::viewFile(aligned_name); return 0; } diff --git a/raycloudtools/raycolour/raycolour.cpp b/raycloudtools/raycolour/raycolour.cpp index e084b9027..d08bc9215 100644 --- a/raycloudtools/raycolour/raycolour.cpp +++ b/raycloudtools/raycolour/raycolour.cpp @@ -21,7 +21,7 @@ void usage(int exit_code = 1) { // clang-format off std::cout << "Colour the ray cloud, and/or shade it" << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "raycolour raycloud time - colour by time (optional on all types)" << std::endl; std::cout << " height - colour by height" << std::endl; std::cout << " shape - colour by geometry shape (r,g,b: spherical, cylinderical, planar)" << std::endl; @@ -100,14 +100,14 @@ int rayColour(int argc, char *argv[]) { ray::FileArgument cloud_file, image_file; ray::KeyChoice colour_type({ "time", "height", "shape", "normal", "alpha", "branches" }); - ray::OptionalFlagArgument lit("lit", 'l'); + ray::OptionalFlagArgument lit("lit", 'l'), view_flag("view", 'v'); ray::Vector3dArgument col(0.0, 1.0); ray::DoubleArgument alpha(0.0, 1.0); ray::TextArgument alpha_text("alpha"), image_text("image"); - const bool standard_format = ray::parseCommandLine(argc, argv, { &cloud_file, &colour_type }, { &lit }); - const bool flat_colour = ray::parseCommandLine(argc, argv, { &cloud_file, &col }, { &lit }); - const bool flat_alpha = ray::parseCommandLine(argc, argv, { &cloud_file, &alpha_text, &alpha }, { &lit }); - const bool image_format = ray::parseCommandLine(argc, argv, { &cloud_file, &image_text, &image_file }, { &lit }); + const bool standard_format = ray::parseCommandLine(argc, argv, { &cloud_file, &colour_type }, { &lit, &view_flag }); + const bool flat_colour = ray::parseCommandLine(argc, argv, { &cloud_file, &col }, { &lit, &view_flag }); + const bool flat_alpha = ray::parseCommandLine(argc, argv, { &cloud_file, &alpha_text, &alpha }, { &lit, &view_flag }); + const bool image_format = ray::parseCommandLine(argc, argv, { &cloud_file, &image_text, &image_file }, { &lit, &view_flag }); if (!standard_format && !flat_colour && !flat_alpha && !image_format) usage(); @@ -185,7 +185,11 @@ int rayColour(int argc, char *argv[]) } writer.end(); if (!lit.isSet()) + { + if (view_flag.isSet()) + ray::viewFile(out_file); return 0; + } in_file = out_file; // when lit we have to load again, from the saved output file std::cout << "reopening file for lighting..." << std::endl; } @@ -365,6 +369,8 @@ int rayColour(int argc, char *argv[]) } cloud.save(out_file); + if (view_flag.isSet()) + ray::viewFile(out_file); return 0; } diff --git a/raycloudtools/raycombine/raycombine.cpp b/raycloudtools/raycombine/raycombine.cpp index ddc8f97d5..884a12213 100644 --- a/raycloudtools/raycombine/raycombine.cpp +++ b/raycloudtools/raycombine/raycombine.cpp @@ -22,7 +22,7 @@ void usage(int exit_code = 1) // clang-format off std::cout << "Combines multiple ray clouds. Clouds are not moved but rays are omitted in the combined cloud according to the merge type specified." << std::endl; std::cout << "Outputs the combined cloud and the residual cloud of differences." << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "raycombine all raycloud1 raycloud2 ... raycloudN - concatenate all the rays in the _combined.ply cloud ('all' is optional)" << std::endl; std::cout << " min raycloud1 ... raycloudN 20 rays - combines into one cloud with minimal objects at differences" << std::endl; std::cout << " 20 is the number of pass through rays to define " << std::endl; @@ -44,21 +44,22 @@ int rayCombine(int argc, char *argv[]) ray::FileArgumentList cloud_files(2); ray::DoubleArgument num_rays(0.0, 100.0); ray::TextArgument rays_text("rays"), all_text("all"); + ray::OptionalFlagArgument view_flag("view", 'v'); // Below: false = allow unusual file extensions, for auto-merging, which occurs on non-standard temporary file names ray::FileArgument base_cloud(false), cloud_1(false), cloud_2(false), output_file(false); ray::OptionalKeyValueArgument output("output", 'o', &output_file); // three-way merge option - bool standard_format = ray::parseCommandLine(argc, argv, { &merge_type, &cloud_files, &num_rays, &rays_text }, { &output }); - bool concatenate_all = ray::parseCommandLine(argc, argv, { &all_text, &cloud_files }, { &output }); + bool standard_format = ray::parseCommandLine(argc, argv, { &merge_type, &cloud_files, &num_rays, &rays_text }, { &output, &view_flag }); + bool concatenate_all = ray::parseCommandLine(argc, argv, { &all_text, &cloud_files }, { &output, &view_flag }); bool threeway = ray::parseCommandLine( - argc, argv, { &base_cloud, &merge_type, &cloud_1, &cloud_2, &num_rays, &rays_text }, { &output }); + argc, argv, { &base_cloud, &merge_type, &cloud_1, &cloud_2, &num_rays, &rays_text }, { &output, &view_flag }); bool threeway_concatenate = - ray::parseCommandLine(argc, argv, { &base_cloud, &all_text, &cloud_1, &cloud_2 }, { &output }); + ray::parseCommandLine(argc, argv, { &base_cloud, &all_text, &cloud_1, &cloud_2 }, { &output, &view_flag }); if (!standard_format && !concatenate_all && !threeway && !threeway_concatenate) { - concatenate_all = ray::parseCommandLine(argc, argv, { &cloud_files }, { &output }); // a bit more ambiguous, so only try if the other formats failed + concatenate_all = ray::parseCommandLine(argc, argv, { &cloud_files }, { &output, &view_flag }); // a bit more ambiguous, so only try if the other formats failed if (!concatenate_all) { usage(); @@ -140,6 +141,8 @@ int rayCombine(int argc, char *argv[]) usage(); } writer.end(); + if (view_flag.isSet()) + ray::viewFile(combined_file); return 0; } @@ -166,6 +169,8 @@ int rayCombine(int argc, char *argv[]) progress_thread.join(); fixed_cloud->save(combined_file); + if (view_flag.isSet()) + ray::viewFile(combined_file); return 0; } diff --git a/raycloudtools/raycreate/raycreate.cpp b/raycloudtools/raycreate/raycreate.cpp index 96c348e47..2c84e054f 100644 --- a/raycloudtools/raycreate/raycreate.cpp +++ b/raycloudtools/raycreate/raycreate.cpp @@ -20,7 +20,7 @@ void usage(int exit_code = 1) { // clang-format off std::cout << "Generates simple example ray clouds" << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "raycreate room 3 - generates a room using the seed 3. Also:" << std::endl; std::cout << " building" << std::endl; std::cout << " tree" << std::endl; @@ -29,7 +29,7 @@ void usage(int exit_code = 1) std::cout << " field" << std::endl; std::cout << std::endl; std::cout << " forest trees.txt - generate from a comma-separated list of x,y,z,radius trees" << std::endl; - std::cout << " terrain mesh.ply - generate from a ground mesh" << std::endl; + std::cout << " terrain mesh.ply - generate from a ground mesh" << std::endl; // clang-format on exit(exit_code); } @@ -40,9 +40,10 @@ int rayCreate(int argc, char *argv[]) { ray::KeyChoice cloud_type({ "room", "building", "tree", "forest", "terrain", "field" }); ray::IntArgument seed(1, 1000000); + ray::OptionalFlagArgument view_flag("view", 'v'); ray::FileArgument input_file; - bool from_seed = ray::parseCommandLine(argc, argv, { &cloud_type, &seed }); - bool from_file = ray::parseCommandLine(argc, argv, { &cloud_type, &input_file }); + bool from_seed = ray::parseCommandLine(argc, argv, { &cloud_type, &seed }, { &view_flag }); + bool from_file = ray::parseCommandLine(argc, argv, { &cloud_type, &input_file }, { &view_flag }); if (!from_seed && !from_file) usage(); @@ -241,6 +242,8 @@ int rayCreate(int argc, char *argv[]) else usage(); cloud.save(type + ".ply"); + if (view_flag.isSet()) + ray::viewFile(type + ".ply"); return 0; } diff --git a/raycloudtools/raydecimate/raydecimate.cpp b/raycloudtools/raydecimate/raydecimate.cpp index 48ef5f695..740ecda77 100644 --- a/raycloudtools/raydecimate/raydecimate.cpp +++ b/raycloudtools/raydecimate/raydecimate.cpp @@ -19,7 +19,7 @@ void usage(int exit_code = 1) { // clang-format off std::cout << "Decimate a ray cloud spatially or temporally" << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "raydecimate raycloud 3 cm - reduces to one end point every 3 cm. A spatially even subsampling" << std::endl; std::cout << "raydecimate raycloud 4 rays - reduces to every fourth ray. A temporally even subsampling (if rays are chronological)" << std::endl; std::cout << "advanced methods not supported in rayrestore:" << std::endl; @@ -53,10 +53,11 @@ int rayDecimate(int argc, char *argv[]) ray::IntArgument width_for_ray(1, 10000); ray::DoubleArgument vox_width(0.01, 100.0); ray::DoubleArgument radius_per_length(0.01, 100.0); + ray::OptionalFlagArgument view_flag("view", 'v'); ray::ValueKeyChoice quantity({ &vox_width, &num_rays, &radius_per_length, &width_for_ray }, { "cm", "rays", "cm/m", "cm/ray" }); ray::TextArgument cm("cm"), points("points"); - bool standard_format = ray::parseCommandLine(argc, argv, { &cloud_file, &quantity }); - bool double_format_points = ray::parseCommandLine(argc, argv, { &cloud_file, &vox_width, &cm, &num_rays, &points }); + bool standard_format = ray::parseCommandLine(argc, argv, { &cloud_file, &quantity }, { &view_flag }); + bool double_format_points = ray::parseCommandLine(argc, argv, { &cloud_file, &vox_width, &cm, &num_rays, &points }, { &view_flag }); if (!standard_format && !double_format_points) usage(); @@ -83,7 +84,8 @@ int rayDecimate(int argc, char *argv[]) } if (!res) usage(); - + if (view_flag.isSet()) + ray::viewFile(cloud_file.nameStub() + "_decimated.ply"); return 0; } diff --git a/raycloudtools/raydenoise/raydenoise.cpp b/raycloudtools/raydenoise/raydenoise.cpp index 1b2b8d9a6..96fb034a5 100644 --- a/raycloudtools/raydenoise/raydenoise.cpp +++ b/raycloudtools/raydenoise/raydenoise.cpp @@ -16,7 +16,7 @@ void usage(int exit_code = 1) { // clang-format off std::cout << "Remove noise from ray clouds. In particular edge noise and isolated point noise." << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "raydenoise raycloud 4 cm - removes rays that contact more than 4 cm from any other," << std::endl; std::cout << "raydenoise raycloud 3 sigmas - removes points more than 3 sigmas from nearest points" << std::endl; std::cout << " range 4 cm - remove mixed-signal noise that occurs at a range gap." << std::endl; @@ -32,10 +32,11 @@ int rayDenoise(int argc, char *argv[]) ray::TextArgument range_text("range"); ray::DoubleArgument range(1.0, 1000.0); ray::TextArgument cm_text("cm"); + ray::OptionalFlagArgument view_flag("view", 'v'); ray::ValueKeyChoice quantity({ &vox_width, &sigmas, &range }, { "cm", "sigmas" }); - bool standard_format = ray::parseCommandLine(argc, argv, { &cloud_file, &quantity }); - bool range_noise = ray::parseCommandLine(argc, argv, { &cloud_file, &range_text, &range, &cm_text }); + bool standard_format = ray::parseCommandLine(argc, argv, { &cloud_file, &quantity }, { &view_flag }); + bool range_noise = ray::parseCommandLine(argc, argv, { &cloud_file, &range_text, &range, &cm_text }, { &view_flag }); if (!standard_format && !range_noise) usage(); @@ -150,6 +151,8 @@ int rayDenoise(int argc, char *argv[]) } new_cloud.save(cloud_file.nameStub() + "_denoised.ply"); + if (view_flag.isSet()) + ray::viewFile(cloud_file.nameStub() + "_denoised.ply"); return 0; } diff --git a/raycloudtools/raydiff/raydiff.cpp b/raycloudtools/raydiff/raydiff.cpp index f6157cca7..ee60d38c6 100644 --- a/raycloudtools/raydiff/raydiff.cpp +++ b/raycloudtools/raydiff/raydiff.cpp @@ -18,11 +18,10 @@ void usage(int exit_code = 1) { // clang-format off std::cout << "Difference between two ray clouds output a coloured cloud, cloud1 differences in red, cloud2 differences in green, and similarity printed to screen." << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "raydiff cloud1.ply cloud2.ply" << std::endl; std::cout << " --distance 0 - optional threshold in m for colouring differences. Default auto-detects distribution shoulder" << std::endl; std::cout << " --individual - output as two clouds, to show differences on each." << std::endl; - std::cout << " --view - open in the default visualisation tool" << std::endl; // clang-format on exit(exit_code); } diff --git a/raycloudtools/rayextract/rayextract.cpp b/raycloudtools/rayextract/rayextract.cpp index 5f0d25cdb..749181289 100644 --- a/raycloudtools/rayextract/rayextract.cpp +++ b/raycloudtools/rayextract/rayextract.cpp @@ -28,7 +28,7 @@ void usage(int exit_code = 1) const bool none = extract_type != "terrain" && extract_type != "trunks" && extract_type != "forest" && extract_type != "trees" && extract_type != "leaves"; // clang-format off std::cout << "Extract natural features into a text file or mesh file" << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; if (extract_type == "terrain" || none) { std::cout << "rayextract terrain cloud.ply - extract terrain undersurface to mesh. Slow, so consider decimating first." << std::endl; @@ -76,7 +76,7 @@ void usage(int exit_code = 1) std::cout << " --leaf_area 0.002 - area for each leaf." << std::endl; std::cout << " --leaf_droop 0.1 - drop per square horizontal distance." << std::endl; std::cout << " --stalks - include stalks to closest branch." << std::endl; - std::cout << " --verbose - extra debug output." << std::endl; + std::cout << " --debug - (-u) extra debug output." << std::endl; } // clang-format on exit(exit_code); @@ -91,6 +91,7 @@ int rayExtract(int argc, char *argv[]) extract_type = std::string(argv[1]); } ray::FileArgument cloud_file, mesh_file, trunks_file, trees_file, leaf_file; + ray::OptionalFlagArgument view_flag("view", 'v'); ray::TextArgument forest("forest"), trees("trees"), trunks("trunks"), terrain("terrain"), leaves("leaves"); ray::OptionalKeyValueArgument groundmesh_option("ground", 'g', &mesh_file); ray::OptionalKeyValueArgument trunks_option("trunks", 't', &trunks_file); @@ -126,20 +127,19 @@ int rayExtract(int argc, char *argv[]) ray::OptionalKeyValueArgument width_option("width", 'w', &width), smooth_option("smooth", 's', &smooth), drop_option("drop_ratio", 'd', &drop); - ray::OptionalFlagArgument verbose("verbose", 'v'); + ray::OptionalFlagArgument verbose("debug", 'u'); - bool extract_terrain = ray::parseCommandLine(argc, argv, { &terrain, &cloud_file }, { &gradient_option, &verbose }); - bool extract_trunks = ray::parseCommandLine(argc, argv, { &trunks, &cloud_file }, { &exclude_rays, &verbose }); + bool extract_terrain = ray::parseCommandLine(argc, argv, { &terrain, &cloud_file }, { &gradient_option, &verbose, &view_flag }); + bool extract_trunks = ray::parseCommandLine(argc, argv, { &trunks, &cloud_file }, { &exclude_rays, &verbose, &view_flag }); bool extract_forest = ray::parseCommandLine( argc, argv, { &forest, &cloud_file }, - { &groundmesh_option, &trunks_option, &width_option, &smooth_option, &drop_option, &verbose }); + { &groundmesh_option, &trunks_option, &width_option, &smooth_option, &drop_option, &verbose, &view_flag }); bool extract_trees = ray::parseCommandLine( 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 }); - bool extract_leaves = ray::parseCommandLine(argc, argv, { &leaves, &cloud_file, &trees_file }, { &leaf_option, &leaf_area_option, &leaf_droop_option, &stalks }); - + &segment_branches, &grid_width_option, &global_taper_option, &global_taper_factor_option, &use_rays, &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) { @@ -147,6 +147,7 @@ int rayExtract(int argc, char *argv[]) } // finds cylindrical trunks in the data and saves them to an _trunks.txt file + std::string output_file = ""; if (extract_trunks) { ray::Cloud cloud; @@ -247,7 +248,8 @@ int rayExtract(int argc, char *argv[]) } ray::Mesh tree_mesh; forest.generateSmoothMesh(tree_mesh, -1, 1, 1, 1); - ray::writePlyMesh(cloud_file.nameStub() + "_trees_mesh.ply", tree_mesh, true); + output_file = cloud_file.nameStub() + "_trees_mesh.ply"; + ray::writePlyMesh(output_file, tree_mesh, true); } // extract the tree locations from a larger, aerial view of a forest else if (extract_forest) @@ -302,16 +304,25 @@ int rayExtract(int argc, char *argv[]) ray::Terrain terrain; terrain.extract(cloud, offset, cloud_file.nameStub(), gradient.value(), verbose.isSet()); + output_file = cloud_file.nameStub() + "_mesh.ply"; } else if (extract_leaves) { ray::generateLeaves(cloud_file.nameStub(), trees_file.name(), leaf_file.name(), leaf_area.value(), leaf_droop.value(), stalks.isSet()); + output_file = cloud_file.nameStub() + "_leaves.ply"; } else { usage(true); } + if (view_flag.isSet()) + { + if (output_file == "") + std::cout << "Warning: cannot view the output file type" << std::endl; + else + ray::viewFile(output_file); + } return 0; } diff --git a/raycloudtools/rayimport/rayimport.cpp b/raycloudtools/rayimport/rayimport.cpp index e921c5aba..7cb9bcf33 100644 --- a/raycloudtools/rayimport/rayimport.cpp +++ b/raycloudtools/rayimport/rayimport.cpp @@ -18,7 +18,7 @@ void usage(int exit_code = 1) { // clang-format off std::cout << "Import a point cloud and trajectory file into a ray cloud" << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "rayimport pointcloudfile trajectoryfile - pointcloudfile can be a .laz, .las or .ply file" << std::endl; std::cout << " trajectoryfile is a text file using 'time x y z' format per line" << std::endl; std::cout << "rayimport pointcloudfile 0,0,0 - use 0,0,0 as the sensor location" << std::endl; @@ -37,14 +37,14 @@ int rayImport(int argc, char *argv[]) ray::Vector3dArgument position, ray_vec; ray::TextArgument ray_text("ray"); ray::OptionalKeyValueArgument max_intensity_option("max_intensity", 'm', &max_intensity); - ray::OptionalFlagArgument remove("remove_start_pos", 'r'); + ray::OptionalFlagArgument remove("remove_start_pos", 'r'), view_flag("view", 'v'); ray::FileArgument cloud_file, trajectory_file; bool standard_format = - ray::parseCommandLine(argc, argv, { &cloud_file, &trajectory_file }, { &max_intensity_option, &remove }); + ray::parseCommandLine(argc, argv, { &cloud_file, &trajectory_file }, { &max_intensity_option, &remove, &view_flag }); bool position_format = - ray::parseCommandLine(argc, argv, { &cloud_file, &position }, { &max_intensity_option, &remove }); + ray::parseCommandLine(argc, argv, { &cloud_file, &position }, { &max_intensity_option, &remove, &view_flag }); bool ray_format = - ray::parseCommandLine(argc, argv, { &cloud_file, &ray_text, &ray_vec }, { &max_intensity_option, &remove }); + ray::parseCommandLine(argc, argv, { &cloud_file, &ray_text, &ray_vec }, { &max_intensity_option, &remove, &view_flag }); if (!standard_format && !position_format && !ray_format) usage(); @@ -90,10 +90,11 @@ int rayImport(int argc, char *argv[]) std::string save_file = cloud_file.nameStub(); if (cloud_file.nameExt() == "ply") save_file += "_raycloud"; + save_file += ".ply"; size_t num_bounded; std::ofstream ofs; ray::RayPlyBuffer buffer; - if (!ray::writeRayCloudChunkStart(save_file + ".ply", ofs)) + if (!ray::writeRayCloudChunkStart(save_file, ofs)) usage(); Eigen::Vector3d start_pos(0, 0, 0); bool has_warned = false; @@ -225,6 +226,8 @@ int rayImport(int argc, char *argv[]) // std::cout.precision(10); std::cout << "start position: " << std::setprecision(5) << std::fixed << start_pos.transpose() << " removed from all points" << std::endl; } + if (view_flag.isSet()) + ray::viewFile(save_file); return 0; } diff --git a/raycloudtools/rayrender/rayrender.cpp b/raycloudtools/rayrender/rayrender.cpp index edad1f511..f974b328f 100644 --- a/raycloudtools/rayrender/rayrender.cpp +++ b/raycloudtools/rayrender/rayrender.cpp @@ -17,7 +17,7 @@ void usage(int exit_code = 1) { // clang-format off std::cout << "Render a ray cloud as an image, from a specified viewpoint" << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "rayrender raycloudfile.ply top ends - render from the top (plan view) the end points" << std::endl; std::cout << " left - facing negative x axis" << std::endl; std::cout << " right - facing positive x axis" << std::endl; @@ -53,7 +53,7 @@ int rayRender(int argc, char *argv[]) ray::DoubleArgument pixel_width(0.0001, 1000.0), grid_width(0.01, 1000000.0); ray::IntArgument resolution(1,20000, 512); ray::FileArgument cloud_file, image_file, transform_file, projection_file(false); - ray::OptionalFlagArgument mark_origin("mark_origin", 'm'); + ray::OptionalFlagArgument mark_origin("mark_origin", 'm'), view_flag("view", 'v');; ray::OptionalKeyValueArgument resolution_option("resolution", 'r', &resolution); ray::OptionalKeyValueArgument pixel_width_option("pixel_width", 'p', &pixel_width); ray::OptionalKeyValueArgument grid_width_option("grid_width", 'g', &grid_width); @@ -62,7 +62,7 @@ int rayRender(int argc, char *argv[]) ray::OptionalKeyValueArgument transform_file_option("output_transform", 't', &transform_file); if (!ray::parseCommandLine( argc, argv, { &cloud_file, &viewpoint, &style }, - { &resolution_option, &pixel_width_option, &output_file_option, &mark_origin, &transform_file_option, &grid_width_option, &projection_file_option })) + { &resolution_option, &pixel_width_option, &output_file_option, &view_flag, &mark_origin, &transform_file_option, &grid_width_option, &projection_file_option })) { usage(); } @@ -142,11 +142,12 @@ int rayRender(int argc, char *argv[]) { usage(); } - + if (view_flag.isSet()) + std::system((std::string(R_IMAGETOOL) + " " + image_file.name()).c_str()); return 0; } int main(int argc, char *argv[]) { return ray::runWithMemoryCheck(rayRender, argc, argv); -} \ No newline at end of file +} diff --git a/raycloudtools/rayrotate/rayrotate.cpp b/raycloudtools/rayrotate/rayrotate.cpp index c5d541ef2..c726c42ec 100644 --- a/raycloudtools/rayrotate/rayrotate.cpp +++ b/raycloudtools/rayrotate/rayrotate.cpp @@ -16,7 +16,7 @@ void usage(int exit_code = 1) { // clang-format off std::cout << "Rotate a raycloud about the origin" << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "rayrotate raycloud 30,0,0 - rotation (rx,ry,rz) is a rotation vector in degrees:" << std::endl; std::cout << " so this example rotates the cloud by 30 degrees in the x axis." << std::endl; // clang-format on @@ -27,7 +27,8 @@ int rayRotate(int argc, char *argv[]) { ray::FileArgument cloud_file; ray::Vector3dArgument rotation_arg(-360, 360); - if (!ray::parseCommandLine(argc, argv, { &cloud_file, &rotation_arg })) + ray::OptionalFlagArgument view_flag("view", 'v'); + if (!ray::parseCommandLine(argc, argv, { &cloud_file, &rotation_arg }, { &view_flag })) usage(); Eigen::Vector3d rot = rotation_arg.value(); @@ -45,6 +46,8 @@ int rayRotate(int argc, char *argv[]) usage(); std::rename(temp_name.c_str(), cloud_file.name().c_str()); + if (view_flag.isSet()) + ray::viewFile(cloud_file.name()); return 0; } diff --git a/raycloudtools/raysmooth/raysmooth.cpp b/raycloudtools/raysmooth/raysmooth.cpp index 07930c2fa..085f23dd4 100644 --- a/raycloudtools/raysmooth/raysmooth.cpp +++ b/raycloudtools/raysmooth/raysmooth.cpp @@ -17,7 +17,7 @@ void usage(int exit_code = 1) { // clang-format off std::cout << "Smooth a ray cloud. Nearby off-surface points are moved onto the nearest surface." << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "raysmooth raycloud" << std::endl; // clang-format on exit(exit_code); @@ -26,7 +26,8 @@ void usage(int exit_code = 1) int raySmooth(int argc, char *argv[]) { ray::FileArgument cloud_file; - if (!ray::parseCommandLine(argc, argv, { &cloud_file })) + ray::OptionalFlagArgument view_flag("view", 'v'); + if (!ray::parseCommandLine(argc, argv, { &cloud_file }, { &view_flag })) usage(); ray::Cloud cloud; @@ -65,8 +66,10 @@ int raySmooth(int argc, char *argv[]) cloud.ends[i] += normals[i] * (centroids[i] - cloud.ends[i]).dot(normals[i]); } - cloud.save(cloud_file.nameStub() + "_smooth.ply"); - + std::string output_file = cloud_file.nameStub() + "_smooth.ply"; + cloud.save(output_file); + if (view_flag.isSet()) + ray::viewFile(output_file); return 0; } diff --git a/raycloudtools/raysplit/raysplit.cpp b/raycloudtools/raysplit/raysplit.cpp index 1bce24b28..d48a90309 100644 --- a/raycloudtools/raysplit/raysplit.cpp +++ b/raycloudtools/raysplit/raysplit.cpp @@ -20,7 +20,7 @@ void usage(int exit_code = 1) { // clang-format off std::cout << "Split a ray cloud relative to the supplied triangle mesh, generating two cropped ray clouds" << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "raysplit raycloud plane 10,0,0 - splits around plane at 10 m along x axis" << std::endl; std::cout << " colour - splits by colour, one cloud per colour" << std::endl; std::cout << " colour 0.5,0,0 - splits by colour, around half red component" << std::endl; @@ -36,8 +36,8 @@ void usage(int exit_code = 1) std::cout << " grid wx,wy,wz - splits into a 0,0,0 centred grid of files, cell width wx,wy,wz. 0 for unused axes." << std::endl; std::cout << " grid wx,wy,wz 1 - same as above, but with a 1 metre overlap between cells." << std::endl; std::cout << " grid wx,wy,wz,wt - splits into a grid of files, cell width wx,wy,wz and period wt. 0 for unused axes." << std::endl; - std::cout << " capsule 1,2,3 10,11,12 5 - splits within a capsule using start, end and radius" << std::endl; - // clang-format on + std::cout << " capsule 1,2,3 5,8,12 7 - splits within a capsule using start, end and radius" << std::endl; +// clang-format on exit(exit_code); } @@ -45,6 +45,7 @@ void usage(int exit_code = 1) int raySplit(int argc, char *argv[]) { ray::FileArgument cloud_file; + ray::OptionalFlagArgument view_flag("view", 'v'); double max_val = std::numeric_limits::max(); ray::Vector3dArgument plane, colour(0.0, 1.0), single_colour(0.0, 255.0), raydir(-1.0, 1.0), box_centre, box_radius(0.0, max_val), cell_width(0.0, max_val), capsule_start, capsule_end; @@ -57,17 +58,17 @@ int raySplit(int argc, char *argv[]) ray::TextArgument distance_text("distance"), time_text("time"), percent_text("%"); ray::TextArgument box_text("box"), grid_text("grid"), colour_text("colour"), seg_colour_text("seg_colour"), capsule_text("capsule"); ray::DoubleArgument mesh_offset; - bool standard_format = ray::parseCommandLine(argc, argv, { &cloud_file, &choice }); - bool colour_format = ray::parseCommandLine(argc, argv, { &cloud_file, &colour_text }); - bool seg_colour_format = ray::parseCommandLine(argc, argv, { &cloud_file, &seg_colour_text }); - bool time_percent = ray::parseCommandLine(argc, argv, { &cloud_file, &time_text, &time, &percent_text }); - bool box_format = ray::parseCommandLine(argc, argv, { &cloud_file, &box_text, &box_centre, &box_radius }); - bool grid_format = ray::parseCommandLine(argc, argv, { &cloud_file, &grid_text, &cell_width }); - bool grid_format2 = ray::parseCommandLine(argc, argv, { &cloud_file, &grid_text, &cell_width2 }); - bool grid_format3 = ray::parseCommandLine(argc, argv, { &cloud_file, &grid_text, &cell_width, &overlap }); - bool mesh_split = ray::parseCommandLine(argc, argv, { &cloud_file, &mesh_file, &distance_text, &mesh_offset }); + bool standard_format = ray::parseCommandLine(argc, argv, { &cloud_file, &choice }, { &view_flag }); + bool colour_format = ray::parseCommandLine(argc, argv, { &cloud_file, &colour_text }, { &view_flag }); + bool seg_colour_format = ray::parseCommandLine(argc, argv, { &cloud_file, &seg_colour_text }, { &view_flag }); + bool time_percent = ray::parseCommandLine(argc, argv, { &cloud_file, &time_text, &time, &percent_text }, { &view_flag }); + bool box_format = ray::parseCommandLine(argc, argv, { &cloud_file, &box_text, &box_centre, &box_radius }, { &view_flag }); + bool grid_format = ray::parseCommandLine(argc, argv, { &cloud_file, &grid_text, &cell_width }, { &view_flag }); + bool grid_format2 = ray::parseCommandLine(argc, argv, { &cloud_file, &grid_text, &cell_width2 }, { &view_flag }); + bool grid_format3 = ray::parseCommandLine(argc, argv, { &cloud_file, &grid_text, &cell_width, &overlap }, { &view_flag }); + bool mesh_split = ray::parseCommandLine(argc, argv, { &cloud_file, &mesh_file, &distance_text, &mesh_offset }, { &view_flag }); bool capsule_split = - ray::parseCommandLine(argc, argv, { &cloud_file, &capsule_text, &capsule_start, &capsule_end, &capsule_radius }); + ray::parseCommandLine(argc, argv, { &cloud_file, &capsule_text, &capsule_start, &capsule_end, &capsule_radius }, { &view_flag }); if (!standard_format && !colour_format && !seg_colour_format && !box_format && !grid_format && !grid_format2 && !grid_format3 && !mesh_split && !time_percent && !capsule_split) { @@ -280,6 +281,8 @@ int raySplit(int argc, char *argv[]) } if (!res) usage(); + if (view_flag.isSet()) + ray::viewFile(in_name, out_name); return 0; } diff --git a/raycloudtools/raytransients/raytransients.cpp b/raycloudtools/raytransients/raytransients.cpp index eed297470..778eb34a6 100644 --- a/raycloudtools/raytransients/raytransients.cpp +++ b/raycloudtools/raytransients/raytransients.cpp @@ -23,7 +23,7 @@ void usage(int exit_code = 1) { // clang-format off std::cout << "Splits a raycloud into the transient rays and the fixed part" << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "raytransients min raycloud 20 rays - splits out positive transients (objects that have since moved)." << std::endl; std::cout << " 20 is number of pass through rays to classify as transient." << std::endl; std::cout << " max - finds negative transients, such as a hallway exposed when a door opens." << std::endl; @@ -40,8 +40,8 @@ int rayTransients(int argc, char *argv[]) ray::FileArgument cloud_file; ray::DoubleArgument num_rays(0.1, 100.0); ray::TextArgument text("rays"); - ray::OptionalFlagArgument colour("colour", 'c'); - if (!ray::parseCommandLine(argc, argv, { &merge_type, &cloud_file, &num_rays, &text }, { &colour })) + ray::OptionalFlagArgument colour("colour", 'c'), view_flag("view", 'v'); + if (!ray::parseCommandLine(argc, argv, { &merge_type, &cloud_file, &num_rays, &text }, { &colour, &view_flag })) usage(); ray::Cloud cloud; @@ -85,8 +85,11 @@ int rayTransients(int argc, char *argv[]) const ray::Cloud &transient = filter.differenceCloud(); const ray::Cloud &fixed = filter.fixedCloud(); - transient.save(cloud_file.nameStub() + "_transient.ply"); + std::string transient_file = cloud_file.nameStub() + "_transient.ply"; + transient.save(transient_file); fixed.save(cloud_file.nameStub() + "_fixed.ply"); + if (view_flag.isSet()) + ray::viewFile(transient_file); return 0; } diff --git a/raycloudtools/raytranslate/raytranslate.cpp b/raycloudtools/raytranslate/raytranslate.cpp index f85db8a9a..17b5ca7d9 100644 --- a/raycloudtools/raytranslate/raytranslate.cpp +++ b/raycloudtools/raytranslate/raytranslate.cpp @@ -18,7 +18,7 @@ void usage(int exit_code = 1) { // clang-format off std::cout << "Translate a raycloud" << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "raytranslate raycloud 0,0,1 - translation (x,y,z) in metres" << std::endl; std::cout << " 0,0,1,24.3 - optional 4th component translates time" << std::endl; std::cout << " subtract ground_mesh.ply - translate vertically to remove ground_mesh heights" << std::endl; @@ -31,13 +31,14 @@ int rayTranslate(int argc, char *argv[]) { ray::FileArgument cloud_file, ground_file; ray::TextArgument subtract("subtract"), add("add"); + ray::OptionalFlagArgument view_flag("view", 'v'); ray::Vector3dArgument translation3; ray::Vector4dArgument translation4; - bool vec3_format = ray::parseCommandLine(argc, argv, { &cloud_file, &translation3 }); - bool vec4_format = ray::parseCommandLine(argc, argv, { &cloud_file, &translation4 }); - bool ground_subtract_format = ray::parseCommandLine(argc, argv, { &cloud_file, &subtract, &ground_file }); - bool ground_add_format = ray::parseCommandLine(argc, argv, { &cloud_file, &add, &ground_file }); + bool vec3_format = ray::parseCommandLine(argc, argv, { &cloud_file, &translation3 }, { &view_flag }); + bool vec4_format = ray::parseCommandLine(argc, argv, { &cloud_file, &translation4 }, { &view_flag }); + bool ground_subtract_format = ray::parseCommandLine(argc, argv, { &cloud_file, &subtract, &ground_file }, { &view_flag }); + bool ground_add_format = ray::parseCommandLine(argc, argv, { &cloud_file, &add, &ground_file }, { &view_flag }); if (!vec3_format && !vec4_format && !ground_subtract_format && !ground_add_format) usage(); @@ -202,7 +203,8 @@ int rayTranslate(int argc, char *argv[]) std::cout << "Warning: " << num_totally_missed << " points have no laterally overlapping triangles, so the ground_file is not a full coverage. Point translation ignored" << std::endl; } std::rename(temp_name.c_str(), cloud_file.name().c_str()); - + if (view_flag.isSet()) + ray::viewFile(cloud_file.name()); return 0; } diff --git a/raycloudtools/raywrap/raywrap.cpp b/raycloudtools/raywrap/raywrap.cpp index 085c1e63f..27a1e0ed6 100644 --- a/raycloudtools/raywrap/raywrap.cpp +++ b/raycloudtools/raywrap/raywrap.cpp @@ -20,10 +20,10 @@ void usage(int exit_code = 1) { // clang-format off std::cout << "Extracts the ground surface as a mesh." << std::endl; - std::cout << "usage:" << std::endl; + std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "raywrap raycloud upwards 1.0 - wraps raycloud from the bottom upwards, or: downwards, inwards, outwards" << std::endl; std::cout << " the 1.0 is the maximum curvature to bend to" << std::endl; - std::cout << "--full - the full (slower) method accounts for overhangs." << std::endl; + std::cout << " --full - the full (slower) method accounts for overhangs." << std::endl; // clang-format on exit(exit_code); } @@ -33,8 +33,8 @@ int rayWrap(int argc, char *argv[]) ray::FileArgument cloud_file; ray::KeyChoice direction({ "upwards", "downwards", "inwards", "outwards" }); ray::DoubleArgument curvature; - ray::OptionalFlagArgument full("full", 'f'); - if (!ray::parseCommandLine(argc, argv, { &cloud_file, &direction, &curvature }, { &full })) + ray::OptionalFlagArgument full("full", 'f'), view_flag("view", 'v'); + if (!ray::parseCommandLine(argc, argv, { &cloud_file, &direction, &curvature }, { &full, &view_flag })) usage(); ray::Cloud cloud; @@ -43,6 +43,7 @@ int rayWrap(int argc, char *argv[]) cloud.removeUnboundedRays(); Eigen::Vector3d offset = cloud.removeStartPos(); + std::string output_file = cloud_file.nameStub() + "_mesh.ply"; if (full.isSet()) { ray::ConcaveHull concave_hull(cloud.ends); @@ -58,7 +59,7 @@ int rayWrap(int argc, char *argv[]) usage(); concave_hull.mesh().translate(offset); - writePlyMesh(cloud_file.nameStub() + "_mesh.ply", concave_hull.mesh(), true); + writePlyMesh(output_file, concave_hull.mesh(), true); } else { @@ -76,10 +77,12 @@ int rayWrap(int argc, char *argv[]) convex_hull.mesh().reduce(); convex_hull.mesh().translate(offset); - writePlyMesh(cloud_file.nameStub() + "_mesh.ply", convex_hull.mesh(), true); + writePlyMesh(output_file, convex_hull.mesh(), true); } std::cout << "Completed, output: " << cloud_file.nameStub() << "_mesh.ply" << std::endl; + if (view_flag.isSet()) + ray::viewFile(output_file); return 0; } diff --git a/raylib/rayutils.h b/raylib/rayutils.h index d257dc17b..c3da9fb1d 100644 --- a/raylib/rayutils.h +++ b/raylib/rayutils.h @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include #include #include @@ -22,6 +24,7 @@ #include #include #include + #define VISUALISE_TOOL "QT_QPA_PLATFORM=xcb meshlab" // the first term fixed opening problems on some platforms namespace ray @@ -407,6 +410,16 @@ void walkGrid(const Eigen::Vector3d &start, const Eigen::Vector3d &end, T &objec } } } + +inline int viewFile(const std::string &file_name, const std::string &file2_name = "") +{ + // Force Qt to use X11 platform (not Wayland). Can remove the first string if wish to use default render platform + std::string command = "QT_QPA_PLATFORM=xcb " + std::string(R_VISTOOL) + " " + file_name; + if (file2_name != "") + command += " " + file2_name; + return system(command.c_str()); +} + } // namespace ray #endif // RAYLIB_RAYUTILS_H From cfe625bc3e1d7ccfa09d172f541dcbe5a475ecba Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Fri, 5 Jun 2026 13:45:00 +1000 Subject: [PATCH 15/37] improved readme file --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index e6f47e80a..b84904eef 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,13 @@ If not there already, add to your ~/.bashrc: export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib ''' +## Visualising Ray Clouds: +free software such as Meshlab and CloudCompare can view ray clouds. + +The build variables RAY_VISTOOL and RAY_IMAGETOOL allow the --view (-v) flag to view command-line results immediately. +They default to meshlab and kolourpaint respectively but can be changed with ccmake or with the -D cmake argument. +You will need to install these programs if you want the --view flag to work. + ## File format: Raycloud files are loaded and saved in binary .ply format, the header section is text and follows this format: ```console From c2b8bc99f594543952f9f4d3e53b7ef1a1609f23 Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Fri, 5 Jun 2026 14:54:17 +1000 Subject: [PATCH 16/37] fixed --view not opening on rayimport --- raycloudtools/rayimport/rayimport.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/raycloudtools/rayimport/rayimport.cpp b/raycloudtools/rayimport/rayimport.cpp index 7cb9bcf33..a93a6dae9 100644 --- a/raycloudtools/rayimport/rayimport.cpp +++ b/raycloudtools/rayimport/rayimport.cpp @@ -31,6 +31,8 @@ void usage(int exit_code = 1) exit(exit_code); } +static std::string save_file = ""; // this is used to move the view tool outside of the memory check + int rayImport(int argc, char *argv[]) { ray::DoubleArgument max_intensity(0.0, 1e8, 100.0); @@ -53,7 +55,6 @@ int rayImport(int argc, char *argv[]) std::cerr << "Error: some ray cloud functions require rays to have a length. Please enter a non-zero vector for ray argument" << std::endl; usage(); } - ray::Cloud cloud; const std::string &traj_file = trajectory_file.name(); // Sensors we use have 0 to 100 for normal output, and to 255 for special reflective surfaces double maximum_intensity = max_intensity.value(); @@ -87,7 +88,7 @@ int rayImport(int argc, char *argv[]) usage(); } - std::string save_file = cloud_file.nameStub(); + save_file = cloud_file.nameStub(); if (cloud_file.nameExt() == "ply") save_file += "_raycloud"; save_file += ".ply"; @@ -226,12 +227,17 @@ int rayImport(int argc, char *argv[]) // std::cout.precision(10); std::cout << "start position: " << std::setprecision(5) << std::fixed << start_pos.transpose() << " removed from all points" << std::endl; } - if (view_flag.isSet()) - ray::viewFile(save_file); + if (!view_flag.isSet()) + save_file = ""; return 0; } int main(int argc, char *argv[]) { - return ray::runWithMemoryCheck(rayImport, argc, argv); + int res = ray::runWithMemoryCheck(rayImport, argc, argv); + // For some reason viewFile won't open the file if inside the rayImport function. Even when the .ply file is tiny. + // So it is going here instead. + if (save_file != "") + ray::viewFile(save_file); + return res; } \ No newline at end of file From a470a085c0541d15a0375715dbb438b0ad5fc736 Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Mon, 8 Jun 2026 09:06:31 +1000 Subject: [PATCH 17/37] fixed build option bug --- CMakeLists.txt | 2 -- raylib/raylibconfig.in.h | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f7b5f83a1..d4234bcaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,8 +30,6 @@ option(RAYCLOUD_BUILD_TESTS "Build unit tests?" OFF) option(RAYCLOUD_LEAK_TRACK "Enable memory leak tracking?" OFF) set(RAY_VISTOOL meshlab CACHE STRING "command to use to visualise point cloud / mesh files") set(RAY_IMAGETOOL kolourpaint CACHE STRING "command to use to visualise image files") -add_compile_definitions(R_VISTOOL="${RAY_VISTOOL}") -add_compile_definitions(R_IMAGETOOL="${RAY_IMAGETOOL}") # WITH_ build options. option(WITH_LAS "With liblas for las file support?" OFF) diff --git a/raylib/raylibconfig.in.h b/raylib/raylibconfig.in.h index 7a7a314cb..f8678f91f 100644 --- a/raylib/raylibconfig.in.h +++ b/raylib/raylibconfig.in.h @@ -64,4 +64,10 @@ #define RAYLIB_WITH_NORMAL_FIELD @WITH_NORMAL_FIELD@ #define RAYLIB_DOUBLE_RAYS @DOUBLE_RAYS@ +// String-configured helper tools (configured in top-level CMakeLists.txt). +// Exposed here so downstream users including raylib headers (e.g. rayutils.h) +// can resolve these macros from this generated config header alone. +#define R_VISTOOL "@RAY_VISTOOL@" +#define R_IMAGETOOL "@RAY_IMAGETOOL@" + #endif // RAYLIB_CONFIG_H From c40b75345f85490eef67551fd99b53ee9a6ae07f Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Mon, 8 Jun 2026 12:55:12 +1000 Subject: [PATCH 18/37] fixed vis tool bug --- raylib/raydiffer.cpp | 10 ++-------- raylib/rayutils.h | 2 -- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/raylib/raydiffer.cpp b/raylib/raydiffer.cpp index 53f0587b1..c2b17ee14 100644 --- a/raylib/raydiffer.cpp +++ b/raylib/raydiffer.cpp @@ -313,10 +313,7 @@ bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std:: writer.end(); if (visualise) - { - std::string command = std::string(VISUALISE_TOOL) + std::string(" ") + cloud1_namestub + "_diff.ply"; - system(command.c_str()); - } + viewFile(cloud1_namestub + "_diff.ply"); } else { @@ -329,10 +326,7 @@ bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std:: return false; writer.end(); if (visualise) - { - std::string command = std::string(VISUALISE_TOOL) + std::string(" ") + cloud1_namestub + "_diff.ply " + cloud2_namestub + "_diff.ply"; - system(command.c_str()); - } + viewFile(cloud1_namestub + "_diff.ply", cloud2_namestub + "_diff.ply"); } return true; } diff --git a/raylib/rayutils.h b/raylib/rayutils.h index c3da9fb1d..470c5e27a 100644 --- a/raylib/rayutils.h +++ b/raylib/rayutils.h @@ -25,8 +25,6 @@ #include #include -#define VISUALISE_TOOL "QT_QPA_PLATFORM=xcb meshlab" // the first term fixed opening problems on some platforms - namespace ray { const double kPi = M_PI; From 6e2c312410a816b8ab3d62c6016367d69026f7ff Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Tue, 9 Jun 2026 09:44:27 +1000 Subject: [PATCH 19/37] tweak so raysplit file distance works on trunk-only files --- raylib/rayforeststructure.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/raylib/rayforeststructure.cpp b/raylib/rayforeststructure.cpp index f3511e9a3..8e4349b53 100644 --- a/raylib/rayforeststructure.cpp +++ b/raylib/rayforeststructure.cpp @@ -404,10 +404,12 @@ void ForestStructure::splitCloud(const Cloud &cloud, double offset, Cloud &insid { for (auto &segment: tree.segments()) { - if (segment.parent_id != -1) + if (segment.parent_id != -1 || tree.segments().size()==1) { - Eigen::Vector3d pos1 = tree.segments()[segment.parent_id].tip; + Eigen::Vector3d pos1 = tree.segments()[std::max(0,segment.parent_id)].tip; Eigen::Vector3d pos2 = segment.tip; + if (segment.parent_id == -1) + pos2[2] += 1000.0; // just do cylindrical split on trunk-only files double r = segment.radius; Eigen::Vector3d dir = (pos2 - pos1).normalized(); pos1 -= dir*offset; From f2f2264ab4b60cf051858a6529bff1c3ab8ee16e Mon Sep 17 00:00:00 2001 From: Tim Devereux Date: Fri, 17 Apr 2026 21:59:31 +0000 Subject: [PATCH 20/37] Migrate LAS/LAZ I/O from libLAS to LASzip C API Switches readLas/writeLas and LasWriter from libLAS to the laszip_api.h C API, adding LAS 1.4 support (formats 6-10 and 64-bit point counts) and replacing the legacy DataFormatId bit-mask checks for time/colour. Updates the build to find LASzip 3.x and drops libLAS from the devcontainer. --- .devcontainer/Dockerfile | 18 +-- .devcontainer/devcontainer.json | 3 +- CMakeLists.txt | 8 +- cmake/FindLASzip.cmake | 26 ++++ raylib/raylaz.cpp | 206 +++++++++++++++++++++----------- raylib/raylaz.h | 8 +- 6 files changed, 180 insertions(+), 89 deletions(-) create mode 100644 cmake/FindLASzip.cmake diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1ff8d3f7c..b73a1d999 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -26,26 +26,16 @@ RUN apt-get update && apt-get upgrade -y && \ libgtest-dev && \ rm -rf /var/lib/apt/lists/* -# Clone, build and clean up LASzip +# Clone, build and clean up LASzip (3.x supports LAS 1.4 via laszip_api.h C API) RUN git clone https://github.com/LASzip/LASzip.git && \ cd LASzip && \ - git checkout tags/2.0.1 && \ + git checkout tags/3.4.4 && \ mkdir build && cd build && \ cmake .. && \ make -j$(nproc) && \ make install && \ - cp bin/Release/liblas* /usr/lib/ && \ cd ../.. && rm -rf LASzip -# Clone, build and clean up libLAS -RUN git clone https://github.com/libLAS/libLAS.git && \ - cd libLAS && \ - mkdir build && cd build && \ - cmake .. -DWITH_LASZIP=ON -DWITH_GEOTIFF=OFF && \ - make -j$(nproc) && \ - make install && \ - cd ../.. && rm -rf libLAS - # Clone, build and clean up Qhull RUN git clone https://github.com/qhull/qhull.git && \ cd qhull && \ @@ -69,6 +59,10 @@ RUN git clone https://github.com/ethz-asl/libnabo.git && \ # Update ldconfig RUN ldconfig /usr/local/lib +# Copy build script into image +COPY .devcontainer/commands/compile_install.sh /usr/local/bin/compile_install.sh +RUN chmod +x /usr/local/bin/compile_install.sh + # Set the working directory WORKDIR /workspaces/raycloudtools diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index abc42ae37..1768d359c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,5 +16,6 @@ }, "remoteUser": "root", "forwardPorts": [], - "postCreateCommand": "chmod +x /workspaces/raycloudtools/.devcontainer/commands/compile_install.sh && /workspaces/raycloudtools/.devcontainer/commands/compile_install.sh" + "postCreateCommand": "compile_install.sh", + "runArgs": ["--security-opt", "label=disable"] } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index d4234bcaf..714731e2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ set(RAY_VISTOOL meshlab CACHE STRING "command to use to visualise point cloud / set(RAY_IMAGETOOL kolourpaint CACHE STRING "command to use to visualise image files") # WITH_ build options. -option(WITH_LAS "With liblas for las file support?" OFF) +option(WITH_LAS "With LASzip for las/laz file support (LAS 1.0-1.4)?" OFF) option(WITH_QHULL "With libqhull support?" OFF) option(WITH_TIFF "With libgeotiff support?" OFF) option(WITH_TBB "With Intel Threading Building Blocks support multi-threadding?" OFF) @@ -60,9 +60,9 @@ set(RAYTOOLS_LINK ${libnabo_LIBRARIES} OpenMP::OpenMP_CXX Threads::Threads) # Optionally configured packages. if(WITH_LAS) - find_package(libLAS REQUIRED) - list(APPEND RAYTOOLS_INCLUDE ${libLAS_INCLUDE_DIRS}) - list(APPEND RAYTOOLS_LINK ${libLAS_LIBRARIES}) + find_package(LASzip REQUIRED) + list(APPEND RAYTOOLS_INCLUDE ${LASzip_INCLUDE_DIRS}) + list(APPEND RAYTOOLS_LINK ${LASzip_LIBRARIES}) endif(WITH_LAS) if(WITH_QHULL) diff --git a/cmake/FindLASzip.cmake b/cmake/FindLASzip.cmake new file mode 100644 index 000000000..6bfa2f0df --- /dev/null +++ b/cmake/FindLASzip.cmake @@ -0,0 +1,26 @@ +# This module searches for the standalone LASzip library (3.x+, with LAS 1.4 support) +# and defines: +# LASzip_LIBRARIES - link libraries +# LASzip_INCLUDE_DIRS - include directories (contains laszip/laszip_api.h) +# LASzip_FOUND - true if found + +find_path(LASzip_INCLUDE_DIRS + NAMES laszip/laszip_api.h + HINTS ENV LASzip_ROOT + PATH_SUFFIXES include +) + +find_library(LASzip_LIBRARY + NAMES laszip + HINTS ENV LASzip_ROOT + PATH_SUFFIXES lib +) + +set(LASzip_LIBRARIES ${LASzip_LIBRARY}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LASzip + REQUIRED_VARS LASzip_LIBRARIES LASzip_INCLUDE_DIRS +) + +mark_as_advanced(LASzip_INCLUDE_DIRS LASzip_LIBRARIES LASzip_LIBRARY) diff --git a/raylib/raylaz.cpp b/raylib/raylaz.cpp index 46fa9e685..439c9ce75 100644 --- a/raylib/raylaz.cpp +++ b/raylib/raylaz.cpp @@ -9,8 +9,7 @@ #include "rayunused.h" #if RAYLIB_WITH_LAS -#include -#include +#include #endif // RAYLIB_WITH_LAS namespace ray @@ -24,36 +23,56 @@ bool readLas(const std::string &file_name, #if RAYLIB_WITH_LAS std::cout << "readLas: filename: " << file_name << std::endl; - std::ifstream ifs; - ifs.open(file_name.c_str(), std::ios::in | std::ios::binary); + laszip_POINTER reader; + if (laszip_create(&reader)) + { + std::cerr << "readLas: failed to create LASzip reader" << std::endl; + return false; + } - if (ifs.fail()) + laszip_BOOL is_compressed; + if (laszip_open_reader(reader, file_name.c_str(), &is_compressed)) { - std::cerr << "readLas: failed to open stream" << std::endl; + laszip_CHAR *error; + laszip_get_error(reader, &error); + std::cerr << "readLas: failed to open stream: " << error << std::endl; + laszip_destroy(reader); return false; } - liblas::ReaderFactory f; - liblas::Reader reader = f.CreateWithStream(ifs); - liblas::Header const &header = reader.GetHeader(); + laszip_header_struct *header; + laszip_get_header_pointer(reader, &header); - Eigen::Vector3d offset(header.GetOffsetX(), header.GetOffsetY(), header.GetOffsetZ()); + Eigen::Vector3d offset(header->x_offset, header->y_offset, header->z_offset); if (offset_to_remove) { *offset_to_remove = offset; std::cout << "offset to remove: " << offset.transpose() << std::endl; } - const size_t number_of_points = header.GetPointRecordsCount(); + // LAS 1.4 uses a 64-bit point count; legacy uses the 32-bit field + const size_t number_of_points = + (header->version_minor >= 4 && header->extended_number_of_point_records > 0) + ? static_cast(header->extended_number_of_point_records) + : static_cast(header->number_of_point_records); + + const uint8_t format = header->point_data_format; + // Formats 1,3,4,5 have GPS time in LAS 1.0-1.3; formats 6-10 always have GPS time (LAS 1.4) + const bool using_time = (format == 1 || format == 3 || format == 4 || format == 5 || format >= 6); + // Formats 2,3,5 have RGB in LAS 1.0-1.3; formats 7,8,10 have RGB in LAS 1.4 + const bool using_colour = (format == 2 || format == 3 || format == 5 || format == 7 || format == 8 || format == 10); - bool using_time = (header.GetDataFormatId() & 1) > 0; - bool using_colour = (header.GetDataFormatId() & 2) > 0; if (!using_time) { std::cerr << "No timestamps found on laz file, these are required" << std::endl; + laszip_close_reader(reader); + laszip_destroy(reader); return false; } + laszip_point_struct *point; + laszip_get_point_pointer(reader, &point); + ray::Progress progress; ray::ProgressThread progress_thread(progress); const size_t num_chunks = (number_of_points + (chunk_size - 1)) / chunk_size; @@ -72,31 +91,34 @@ bool readLas(const std::string &file_name, colours.reserve(chunk_size); num_bounded = 0; - for (unsigned int i = 0; i < number_of_points; i++) + for (size_t i = 0; i < number_of_points; i++) { - reader.ReadNextPoint(); - liblas::Point point = reader.GetPoint(); + if (laszip_read_point(reader)) + { + laszip_CHAR *error; + laszip_get_error(reader, &error); + std::cerr << "readLas: error reading point " << i << ": " << error << std::endl; + break; + } - Eigen::Vector3d position; - position[0] = point.GetX(); - position[1] = point.GetY(); - position[2] = point.GetZ(); + laszip_F64 coords[3]; + laszip_get_coordinates(reader, coords); + Eigen::Vector3d position(coords[0], coords[1], coords[2]); ends.push_back(position); starts.push_back(position); // equal to position for laz files, as we do not store the start points if (using_colour) { - liblas::Color colour = point.GetColor(); RGBA col; - col.red = static_cast(colour.GetRed()); - col.green = static_cast(colour.GetGreen()); - col.blue = static_cast(colour.GetBlue()); + col.red = static_cast(point->rgb[0]); + col.green = static_cast(point->rgb[1]); + col.blue = static_cast(point->rgb[2]); colours.push_back(col); } - times.push_back(point.GetTime()); + times.push_back(point->gps_time); - const double point_int = point.GetIntensity(); + const double point_int = point->intensity; const double normalised_intensity = (255.0 * point_int) / max_intensity; const uint8_t intensity = static_cast(std::min(normalised_intensity, 255.0)); if (intensity > 0) @@ -109,8 +131,8 @@ bool readLas(const std::string &file_name, { colourByTime(times, colours); } - for (int i = 0; i < (int)colours.size(); i++) // add intensity into alhpa channel - colours[i].alpha = intensities[i]; + for (size_t j = 0; j < colours.size(); j++) // add intensity into alpha channel + colours[j].alpha = intensities[j]; apply(starts, ends, times, colours); starts.clear(); ends.clear(); @@ -125,6 +147,9 @@ bool readLas(const std::string &file_name, progress_thread.requestQuit(); progress_thread.join(); + laszip_close_reader(reader); + laszip_destroy(reader); + std::cout << "loaded " << file_name << " with " << number_of_points << " points" << std::endl; return true; #else // RAYLIB_WITH_LAS @@ -144,12 +169,12 @@ bool readLas(std::string file_name, std::vector &positions, std { std::vector starts; // dummy as lax just reads in point clouds, not ray clouds auto apply = [&](std::vector &start_points, std::vector &end_points, - std::vector &time_points, std::vector &colour_values) + std::vector &time_points, std::vector &colour_values) { starts.insert(starts.end(), start_points.begin(), start_points.end()); positions.insert(positions.end(), end_points.begin(), end_points.end()); times.insert(times.end(), time_points.begin(), time_points.end()); - colours.insert(colours.end(), colour_values.begin(), colour_values.end()); + colours.insert(colours.end(), colour_values.begin(), colour_values.end()); }; size_t num_bounded; bool success = @@ -169,37 +194,56 @@ bool RAYLIB_EXPORT writeLas(std::string file_name, const std::vectorversion_major = 1; + header->version_minor = 2; + header->point_data_format = 1; // GPS time only + const double scale = 1e-4; + header->x_scale_factor = scale; + header->y_scale_factor = scale; + header->z_scale_factor = scale; + header->x_offset = 0.0; + header->y_offset = 0.0; + header->z_offset = 0.0; + header->number_of_point_records = static_cast(points.size()); + const bool is_laz = file_name.find(".laz") != std::string::npos; std::cout << "Saving points to " << file_name << std::endl; - std::ofstream ofs; - ofs.open(file_name.c_str(), std::ios::out | std::ios::binary); - if (ofs.fail()) + if (laszip_open_writer(writer, file_name.c_str(), is_laz ? 1 : 0)) { - std::cerr << "Error: cannot open " << file_name << " for writing." << std::endl; + laszip_CHAR *error; + laszip_get_error(writer, &error); + std::cerr << "writeLas: failed to open file for writing: " << error << std::endl; + laszip_destroy(writer); return false; } - const double scale = 1e-4; - header.SetScale(scale, scale, scale); + laszip_point_struct *point; + laszip_get_point_pointer(writer, &point); - liblas::Writer writer(ofs, header); - liblas::Point point(&header); - point.SetHeader(&header); // TODO HACK Version 1.7.0 does not correctly resize the data. Commit - // 6e8657336ba445fcec3c9e70c2ebcd2e25af40b9 (1.8.0 3 July fixes it) - for (unsigned int i = 0; i < points.size(); i++) + for (size_t i = 0; i < points.size(); i++) { - point.SetCoordinates(points[i][0], points[i][1], points[i][2]); - point.SetIntensity(colours[i].alpha); + laszip_F64 coords[3] = { points[i][0], points[i][1], points[i][2] }; + laszip_set_coordinates(writer, coords); + point->intensity = colours[i].alpha; if (!times.empty()) - point.SetTime(times[i]); - writer.WritePoint(point); + point->gps_time = times[i]; + laszip_write_point(writer); } + + laszip_update_inventory(writer); + laszip_close_writer(writer); + laszip_destroy(writer); return true; #else // RAYLIB_WITH_LAS RAYLIB_UNUSED(file_name); @@ -215,21 +259,44 @@ bool RAYLIB_EXPORT writeLas(std::string file_name, const std::vectorversion_major = 1; + header_->version_minor = 2; + header_->point_data_format = 1; // GPS time only + const double scale = 1e-4; + header_->x_scale_factor = scale; + header_->y_scale_factor = scale; + header_->z_scale_factor = scale; + header_->x_offset = 0.0; + header_->y_offset = 0.0; + header_->z_offset = 0.0; + + const bool is_laz = file_name_.find(".laz") != std::string::npos; std::cout << "Saving points to " << file_name_ << std::endl; - out_.open(file_name_.c_str(), std::ios::out | std::ios::binary); - if (out_.fail()) + + if (laszip_open_writer(writer_handle_, file_name_.c_str(), is_laz ? 1 : 0)) { - std::cerr << "Error: cannot open " << file_name << " for writing." << std::endl; + laszip_CHAR *error; + laszip_get_error(writer_handle_, &error); + std::cerr << "LasWriter: failed to open file for writing: " << error << std::endl; + laszip_destroy(writer_handle_); + writer_handle_ = nullptr; return; } - const double scale = 1e-4; - header_.SetScale(scale, scale, scale); - writer_ = new liblas::Writer(out_, header_); + + laszip_get_point_pointer(writer_handle_, &point_); } #else // RAYLIB_WITH_LAS LasWriter::LasWriter(const std::string &file_name) @@ -244,7 +311,12 @@ LasWriter::LasWriter(const std::string &file_name) LasWriter::~LasWriter() { #if RAYLIB_WITH_LAS - delete writer_; + if (writer_handle_) + { + laszip_update_inventory(writer_handle_); + laszip_close_writer(writer_handle_); + laszip_destroy(writer_handle_); + } #else std::cerr << "writeLas: cannot write file as WITHLAS not enabled. Enable using: cmake .. -DWITH_LAS=true" << std::endl; @@ -259,21 +331,19 @@ bool LasWriter::writeChunk(const std::vector &points, const std { return true; // this is acceptable behaviour. It avoids calling function checking for emptiness each time } - if (out_.fail()) + if (!writer_handle_ || !point_) { std::cerr << "Error: cannot open " << file_name_ << " for writing." << std::endl; return false; } - liblas::Point point(&header_); - point.SetHeader(&header_); // TODO HACK Version 1.7.0 does not correctly resize the data. Commit - // 6e8657336ba445fcec3c9e70c2ebcd2e25af40b9 (1.8.0 3 July fixes it) - for (unsigned int i = 0; i < points.size(); i++) + for (size_t i = 0; i < points.size(); i++) { - point.SetCoordinates(points[i][0], points[i][1], points[i][2]); - point.SetIntensity(colours[i].alpha); + laszip_F64 coords[3] = { points[i][0], points[i][1], points[i][2] }; + laszip_set_coordinates(writer_handle_, coords); + point_->intensity = colours[i].alpha; if (!times.empty()) - point.SetTime(times[i]); - writer_->WritePoint(point); + point_->gps_time = times[i]; + laszip_write_point(writer_handle_); } return true; #else // RAYLIB_WITH_LAS diff --git a/raylib/raylaz.h b/raylib/raylaz.h index 4562931da..e7b482607 100644 --- a/raylib/raylaz.h +++ b/raylib/raylaz.h @@ -10,7 +10,7 @@ #include "rayutils.h" #if RAYLIB_WITH_LAS -#include +#include #endif // RAYLIB_WITH_LAS @@ -48,10 +48,10 @@ class RAYLIB_EXPORT LasWriter private: const std::string &file_name_; - std::ofstream out_; #if RAYLIB_WITH_LAS - liblas::Header header_; - liblas::Writer *writer_; + laszip_POINTER writer_handle_; + laszip_header_struct *header_; + laszip_point_struct *point_; #endif // RAYLIB_WITH_LAS }; } // namespace ray From 8594347146acf78e454cc09e25702b6a328a045b Mon Sep 17 00:00:00 2001 From: Tim Devereux Date: Fri, 17 Apr 2026 22:34:35 +0000 Subject: [PATCH 21/37] Update docs and Docker for LASzip 3.x migration - README: replace libLAS + LASzip 2.0.1 build steps with a single LASzip 3.4.4 instruction, note LAS 1.4 / COPC support. - docker/Dockerfile: drop the libLAS build stage, bump LASzip to 3.4.4 and build it Release. - .devcontainer/Dockerfile: build LASzip in Release (unoptimized builds make LAZ decompression 5-15x slower). - Remove the now-unused cmake/FindlibLAS.cmake module. --- .devcontainer/Dockerfile | 2 +- README.md | 7 ++-- cmake/FindlibLAS.cmake | 85 ---------------------------------------- docker/Dockerfile | 16 ++------ 4 files changed, 7 insertions(+), 103 deletions(-) delete mode 100644 cmake/FindlibLAS.cmake diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b73a1d999..284c1afe6 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -31,7 +31,7 @@ RUN git clone https://github.com/LASzip/LASzip.git && \ cd LASzip && \ git checkout tags/3.4.4 && \ mkdir build && cd build && \ - cmake .. && \ + cmake .. -DCMAKE_BUILD_TYPE=Release && \ make -j$(nproc) && \ make install && \ cd ../.. && rm -rf LASzip diff --git a/README.md b/README.md index b84904eef..20bfbc744 100644 --- a/README.md +++ b/README.md @@ -238,11 +238,10 @@ This gives an example of how the command line tools could be sequenced to analys *Optional build dependencies:* -For rayconvert to work from .laz files: -* git clone https://github.com/LASzip/LASzip.git, then git checkout tags/2.0.1, then mkdir build, cd build, cmake .., make, sudo make install. -* git clone https://github.com/libLAS/libLAS.git, then mkdir build, cd build, cmake .. -DWITH_LASZIP=ON, make, sudo make install (you'll need GEOTIFF to be off in libLAS, and to have installed boost) +For rayimport/rayexport to work with .las and .laz files (LAS 1.0 through 1.4, including COPC): +* git clone https://github.com/LASzip/LASzip.git, git checkout tags/3.4.4, then mkdir build, cd build, cmake .. -DCMAKE_BUILD_TYPE=Release, make, sudo make install. * in raycloudtools/build: cmake .. -DWITH_LAS=ON (or ccmake .. to turn on/off WITH_LAS) -* note that you may need to add the liblas path into LD_LIBRARY path, normally this can be done with the following line in your ~/.bashrc: export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib +* note that you may need to add the LASzip install path into LD_LIBRARY_PATH, normally this can be done with the following line in your ~/.bashrc: export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib For raywrap: diff --git a/cmake/FindlibLAS.cmake b/cmake/FindlibLAS.cmake deleted file mode 100644 index 5f6400ca4..000000000 --- a/cmake/FindlibLAS.cmake +++ /dev/null @@ -1,85 +0,0 @@ -# This module searches liblas and defines -# libLAS_LIBRARIES - link libraries -# libLAS_RUNTIME_LIBRARIES - runtime binaries (DLLs) -# libLAS_FOUND, if false, do not try to link -# libLAS_INCLUDE_DIR, libLAS_INCLUDE_DIRS, where to find the headers -# -# $libLAS_ROOT is an environment variable that would - -set(LL_HEADER liblas/liblas.hpp) -set(LL_HEADER_SUFFIX include) -set(LL_LIB liblas las) -set(LL_LIB_DEBUG) -set(LL_SHARED) -set(LL_SHARED_DEBUG) - -foreach(LLIB ${LL_LIB}) - list(APPEND LL_LIB_DEBUG ${LLIB}d) -endforeach(LLIB) - -foreach(LLIB ${LL_LIB}) - list(APPEND LL_SHARED ${LLIB}${CMAKE_SHARED_LIBRARY_SUFFIX}) -endforeach(LLIB) - -foreach(LLIB ${LL_LIB_DEBUG}) - list(APPEND LL_SHARED_DEBUG ${LLIB}${CMAKE_SHARED_LIBRARY_SUFFIX}) -endforeach(LLIB) - -# Target the C API if COMPONENTS specified as "capi" -if(libLAS_FIND_COMPONENTS STREQUAL "capi") - set(LL_HEADER liblas/capi/liblas.h) - set(LL_HEADER_SUFFIX include) - set(LL_LIB liblas_c las_c) -else(libLAS_FIND_COMPONENTS STREQUAL "capi") -endif(libLAS_FIND_COMPONENTS STREQUAL "capi") - -find_path(libLAS_INCLUDE_DIR ${LL_HEADER} HINTS ENV libLAS_ROOT PATH_SUFFIXES ${LL_HEADER_SUFFIX}) -set(libLAS_INCLUDE_DIRS ${libLAS_INCLUDE_DIR}) - -find_library(libLAS_LIBRARY_DEBUG NAMES ${LL_LIB_DEBUG} HINTS ENV libLAS_ROOT PATH_SUFFIXES lib) -find_library(libLAS_LIBRARY_RELEASE NAMES ${LL_LIB} HINTS ENV libLAS_ROOT PATH_SUFFIXES lib) - -find_file(libLAS_RUNTIME_DEBUG NAMES ${LL_SHARED_DEBUG} HINTS ENV libLAS_ROOT PATH_SUFFIXES bin) -find_file(libLAS_RUNTIME_RELEASE NAMES ${LL_SHARED} HINTS ENV libLAS_ROOT PATH_SUFFIXES bin) - -if(libLAS_LIBRARY_DEBUG) - list(APPEND libLAS_LIBRARIES debug ${libLAS_LIBRARY_DEBUG}) - if(libLAS_LIBRARY_RELEASE) - list(APPEND libLAS_LIBRARIES optimized ${libLAS_LIBRARY_RELEASE}) - endif(libLAS_LIBRARY_RELEASE) -else(libLAS_LIBRARY_DEBUG) - list(APPEND libLAS_LIBRARIES ${libLAS_LIBRARY_RELEASE}) -endif(libLAS_LIBRARY_DEBUG) - -if(libLAS_RUNTIME_DEBUG) - list(APPEND libLAS_RUNTIME_LIBRARIES debug ${libLAS_RUNTIME_DEBUG}) - if(libLAS_RUNTIME_RELEASE) - list(APPEND libLAS_RUNTIME_LIBRARIES optimized ${libLAS_RUNTIME_RELEASE}) - endif(libLAS_RUNTIME_RELEASE) -else(libLAS_RUNTIME_DEBUG) - list(APPEND libLAS_RUNTIME_LIBRARIES ${libLAS_RUNTIME_RELEASE}) -endif(libLAS_RUNTIME_DEBUG) - -if(libLAS_RUNTIME_DEBUG) - get_filename_component(RUNTIME_DIR "${libLAS_RUNTIME_DEBUG}" DIRECTORY) - list(APPEND libLAS_RUNTIME_DIRS "${RUNTIME_DIR}") -endif(libLAS_RUNTIME_DEBUG) -if(libLAS_RUNTIME_RELEASE) - get_filename_component(RUNTIME_DIR "${libLAS_RUNTIME_RELEASE}" DIRECTORY) - list(APPEND libLAS_RUNTIME_DIRS "${RUNTIME_DIR}") -endif(libLAS_RUNTIME_RELEASE) -if (libLAS_RUNTIME_DIRS) - list(REMOVE_DUPLICATES libLAS_RUNTIME_DIRS) -endif(libLAS_RUNTIME_DIRS) -set(libLAS_RUNTIME_DIRS "${libLAS_RUNTIME_DIRS}" CACHE PATH "LIBLAS runtime directories") - - -# handle the QUIETLY and REQUIRED arguments and set libLAS_FOUND to TRUE if -# all listed variables are TRUE -include(FindPackageHandleStandardArgs) -FIND_PACKAGE_HANDLE_STANDARD_ARGS(libLAS REQUIRED_VARS libLAS_LIBRARIES libLAS_INCLUDE_DIR) -if(libLAS_RUNTIME_LIBRARIES) - set(libLAS_RUNTIME_LIBRARIES ${libLAS_RUNTIME_LIBRARIES} CACHE PATH "LIBBLAS runtime libraries") -endif(libLAS_RUNTIME_LIBRARIES) - -mark_as_advanced(libLAS_INCLUDE_DIR libLAS_LIBRARIES libLAS_RUNTIME_LIBRARIES) diff --git a/docker/Dockerfile b/docker/Dockerfile index a27b4f0d7..0df8349c2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -31,26 +31,16 @@ WORKDIR /deps RUN mkdir -p /deps && \ cd /deps -# Clone, build and clean up LASzip +# Clone, build and clean up LASzip (3.x supports LAS 1.4 via laszip_api.h C API) RUN git clone https://github.com/LASzip/LASzip.git && \ cd LASzip && \ - git checkout tags/2.0.1 && \ + git checkout tags/3.4.4 && \ mkdir build && cd build && \ - cmake .. && \ + cmake .. -DCMAKE_BUILD_TYPE=Release && \ make -j$(nproc) && \ make install && \ - cp bin/Release/liblas* /usr/lib/ && \ cd ../.. && rm -rf LASzip -# Clone, build and clean up libLAS -RUN git clone https://github.com/libLAS/libLAS.git && \ - cd libLAS && \ - mkdir build && cd build && \ - cmake .. -DWITH_LASZIP=ON -DWITH_GEOTIFF=OFF -DCMAKE_CXX_STANDARD=14 && \ - make -j$(nproc) && \ - make install && \ - cd ../.. && rm -rf libLAS - # Clone, build and clean up Qhull RUN git clone https://github.com/qhull/qhull.git && \ cd qhull && \ From 6b16e327e6e23df206e84134c37a1367aa954755 Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Fri, 12 Jun 2026 09:29:21 +1000 Subject: [PATCH 22/37] fixing weird merge conflicts on git pull --- raycloudtools/rayimport/rayimport.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raycloudtools/rayimport/rayimport.cpp b/raycloudtools/rayimport/rayimport.cpp index a93a6dae9..c04cc72d6 100644 --- a/raycloudtools/rayimport/rayimport.cpp +++ b/raycloudtools/rayimport/rayimport.cpp @@ -240,4 +240,4 @@ int main(int argc, char *argv[]) if (save_file != "") ray::viewFile(save_file); return res; -} \ No newline at end of file +} From 83935ec2ada0c34c0123dfbded061b4374f649cb Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Fri, 12 Jun 2026 09:25:11 +1000 Subject: [PATCH 23/37] added mean ray range in rayinfo --- raycloudtools/rayinfo/rayinfo.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/raycloudtools/rayinfo/rayinfo.cpp b/raycloudtools/rayinfo/rayinfo.cpp index 1838e1425..806949cc2 100644 --- a/raycloudtools/rayinfo/rayinfo.cpp +++ b/raycloudtools/rayinfo/rayinfo.cpp @@ -74,6 +74,8 @@ int rayInfo(int argc, char *argv[]) double min_ray_length = std::numeric_limits::max(); double max_ray_length = std::numeric_limits::lowest(); double max_bounded_ray_length = std::numeric_limits::lowest(); + double mean_bounded_ray_length = 0.0; + int mean_count = 0; ray::RGBA min_col(255, 255, 255, 255), max_col(0, 0, 0, 0); int num_pixels_covered = 0; const double voxel_width = 0.5; @@ -125,6 +127,8 @@ int rayInfo(int argc, char *argv[]) if (colours[i].alpha > 0) { max_bounded_ray_length = std::max(max_bounded_ray_length, ray_length); + mean_bounded_ray_length += ray_length; + mean_count++; min_col.red = std::min(min_col.red, colours[i].red); min_col.green = std::min(min_col.green, colours[i].green); @@ -145,6 +149,7 @@ int rayInfo(int argc, char *argv[]) { usage(); } + mean_bounded_ray_length /= (double)mean_count; // print the results to screen std::cout << std::endl; @@ -196,7 +201,7 @@ int rayInfo(int argc, char *argv[]) std::cout << num_minutes << " mins "; std::cout << seconds << " s \t(in seconds: " << path_period << ")" << std::endl; } - std::cout << " ray length: \t\t" << min_ray_length << " to " << max_ray_length << " m, max end point ray length: " << max_bounded_ray_length << " m" << std::endl; + std::cout << " ray length: \t\t" << min_ray_length << " to " << max_ray_length << " m, max end point ray length: " << max_bounded_ray_length << " m, mean: " << mean_bounded_ray_length << std::endl; std::cout << " colour range (RGBA): \t" << (int)min_col.red << "," << (int)min_col.green << "," << (int)min_col.blue << "," << (int)min_col.alpha << " to " << (int)max_col.red << "," << (int)max_col.green << "," << (int)max_col.blue << "," << (int)max_col.alpha << std::endl; From ce76d4df22d7d4f0d7763bc539176d7fab0a78c0 Mon Sep 17 00:00:00 2001 From: Rod Taylor Date: Thu, 19 Feb 2026 05:57:06 +0000 Subject: [PATCH 24/37] Git stores unix permissions. --- .devcontainer/Dockerfile | 6 +----- .devcontainer/commands/compile_install.sh | 0 .devcontainer/devcontainer.json | 5 ++--- 3 files changed, 3 insertions(+), 8 deletions(-) mode change 100644 => 100755 .devcontainer/commands/compile_install.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 284c1afe6..1074f22e5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -59,12 +59,8 @@ RUN git clone https://github.com/ethz-asl/libnabo.git && \ # Update ldconfig RUN ldconfig /usr/local/lib -# Copy build script into image -COPY .devcontainer/commands/compile_install.sh /usr/local/bin/compile_install.sh -RUN chmod +x /usr/local/bin/compile_install.sh - # Set the working directory WORKDIR /workspaces/raycloudtools # Set the default shell to bash -SHELL ["/bin/bash", "-c"] \ No newline at end of file +SHELL ["/bin/bash", "-c"] diff --git a/.devcontainer/commands/compile_install.sh b/.devcontainer/commands/compile_install.sh old mode 100644 new mode 100755 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1768d359c..e5db119e2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,6 +16,5 @@ }, "remoteUser": "root", "forwardPorts": [], - "postCreateCommand": "compile_install.sh", - "runArgs": ["--security-opt", "label=disable"] -} \ No newline at end of file + "postCreateCommand": ".devcontainer/commands/compile_install.sh" +} From 37a1d82272f56498236ab16a0460b209a6774762 Mon Sep 17 00:00:00 2001 From: Rod Taylor Date: Thu, 19 Feb 2026 06:03:37 +0000 Subject: [PATCH 25/37] twxs.cmake is deprecated. --- .devcontainer/devcontainer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e5db119e2..4ead7d7da 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,7 +9,6 @@ "extensions": [ "ms-vscode.cpptools", "ms-vscode.cmake-tools", - "twxs.cmake", "ms-vscode.cpptools-extension-pack" ] } From 87d1786ed63f382bf0a86cc305122eef45184422 Mon Sep 17 00:00:00 2001 From: Rod Taylor Date: Thu, 19 Feb 2026 14:54:36 +1000 Subject: [PATCH 26/37] Update changed forest moments in tests. --- tests/raytest/raytests.cpp | 64 ++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/tests/raytest/raytests.cpp b/tests/raytest/raytests.cpp index ef3b99960..e7a3a3b79 100644 --- a/tests/raytest/raytests.cpp +++ b/tests/raytest/raytests.cpp @@ -38,25 +38,35 @@ namespace raytest /// Compare the statistical (1st and 2nd order) moments of the two ray clouds. This almost surely /// detects differing clouds, and always equal clouds, given a tolerance @c eps. - void compareMoments(const Eigen::ArrayXd &m1, const std::vector &m2, double eps = 0.1) + std::size_t compareMoments(const Eigen::ArrayXd &m1, const std::vector &m2, double eps = 0.1) { - for (size_t i = 0; i eps) + { + return i; + } } + return i; } /// Compare the statistical (1st and 2nd order) moments of the two ray clouds. This almost surely /// detects differing clouds, and always equal clouds, given a tolerance @c eps. - void compareMomentsPercentageError(const Eigen::ArrayXd &m1, const std::vector &m2, double percentage = 5.0) + std::size_t compareMomentsPercentageError(const Eigen::ArrayXd &m1, const std::vector &m2, double percentage = 5.0) { - double eps = 0.01 * percentage; - for (size_t i = 0; i eps) + { + return i; + } } + return i; } /// Creates two copies of the same room with a rotational difference, then aligns the first onto the second @@ -68,8 +78,8 @@ namespace raytest EXPECT_EQ(command("rayalign room.ply room2.ply"), 0); ray::Cloud cloud; EXPECT_TRUE(cloud.load("room_aligned.ply")); - compareMoments(cloud.getMoments(), {-0.0618268, -0.077552, 0.0531072, 7.58334e-08, 7.97642e-08, 1.93877e-08, -0.180532, -0.219257, 0.0654452, 2.47241, 2.08183, 1.28226, 17.539, 10.1994, 0.304682, 0.761892, 0.429502, 0.987362, 0.318932, 0.225742, 0.389901, 0.111705}); } - + EXPECT_EQ(compareMoments(cloud.getMoments(), {-0.0618268, -0.077552, 0.0531072, 7.58334e-08, 7.97642e-08, 1.93877e-08, -0.180532, -0.219257, 0.0654452, 2.47241, 2.08183, 1.28226, 17.539, 10.1994, 0.304682, 0.761892, 0.429502, 0.987362, 0.318932, 0.225742, 0.389901, 0.111705}), cloud.getMoments().size()); + } /// Colours a room according to the normal direction of the surfaces, comparing to the expected results TEST(Basic, RayColour) { @@ -77,7 +87,7 @@ namespace raytest EXPECT_EQ(command("raycolour room.ply normal"), 0); ray::Cloud cloud; EXPECT_TRUE(cloud.load("room_coloured.ply")); - compareMoments(cloud.getMoments(), {-0.108066, -0.0410134, 0.052168, 7.05134e-08, 8.45038e-08, 1.93877e-08, -0.276144, -0.0760758, 0.065631, 2.42455, 2.13738, 1.28226, 17.539, 10.1994, 0.497919, 0.496369, 0.490293, 0.987362, 0.248361, 0.203648, 0.385192, 0.111705}); + EXPECT_EQ(compareMoments(cloud.getMoments(), {-0.108066, -0.0410134, 0.052168, 7.05134e-08, 8.45038e-08, 1.93877e-08, -0.276144, -0.0760758, 0.065631, 2.42455, 2.13738, 1.28226, 17.539, 10.1994, 0.497919, 0.496369, 0.490293, 0.987362, 0.248361, 0.203648, 0.385192, 0.111705}), cloud.getMoments().size()); } /// Creates two rooms, with different transformations, then combines them, and compares to the expected result. @@ -90,7 +100,7 @@ namespace raytest EXPECT_EQ(command("./raycombine min room.ply room2.ply 1 rays"), 0); ray::Cloud cloud; EXPECT_TRUE(cloud.load("room_combined.ply")); - compareMoments(cloud.getMoments(), {-0.0867714, -0.0679941, 0.546619, 0.0215326, 0.0272819, 0.499969, -0.305657, -0.186353, 0.582642, 2.95777, 2.47531, 1.63323, 17.4967, 10.1789, 0.305355, 0.763356, 0.427376, 0.979005, 0.318409, 0.225661, 0.389366, 0.143369}); + EXPECT_EQ(compareMoments(cloud.getMoments(), {-0.0867714, -0.0679941, 0.546619, 0.0215326, 0.0272819, 0.499969, -0.305657, -0.186353, 0.582642, 2.95777, 2.47531, 1.63323, 17.4967, 10.1789, 0.305355, 0.763356, 0.427376, 0.979005, 0.318409, 0.225661, 0.389366, 0.143369}), cloud.getMoments().size()); } /// Creates a building with random seed 1, and compares to the expected results @@ -99,7 +109,7 @@ namespace raytest EXPECT_EQ(command("raycreate building 1"), 0); ray::Cloud cloud; EXPECT_TRUE(cloud.load("building.ply")); - compareMoments(cloud.getMoments(), {-3.168, 16.5472, 7.04175, 3.28539, 18.0871, 2.96654, -3.19551, 16.564, 7.30826, 4.14359, 18.2463, 3.34431, 935.715, 540.236, 0.499997, 0.500408, 0.429416, 0.998894, 0.372134, 0.372389, 0.390499, 0.0332398}); + EXPECT_EQ(compareMoments(cloud.getMoments(), {-3.168, 16.5472, 7.04175, 3.28539, 18.0871, 2.96654, -3.19551, 16.564, 7.30826, 4.14359, 18.2463, 3.34431, 935.715, 540.236, 0.499997, 0.500408, 0.429416, 0.998894, 0.372134, 0.372389, 0.390499, 0.0332398}), cloud.getMoments().size()); } /// Creates a forest and decimates it, comparing to the expected result @@ -111,7 +121,7 @@ namespace raytest EXPECT_TRUE(cloud.load("forest_decimated.ply")); // Below does not compare the time values (or the colour values, which are based on time here) // because spatial decimation does not constraint which time it picks points from. - compareMoments(cloud.getMoments(), {-0.222571, 1.08156, 1.67264, 6.00755, 5.78731, 0.508713, -0.202668, 1.09517, 2.6238, 6.0285, 5.85715, 3.22093, 69.0574, 35.2775, 0.48969, 0.498403, 0.443549, 1, 0.379062, 0.366963, 0.389535, 0}); + EXPECT_EQ(compareMoments(cloud.getMoments(), {-0.222571, 1.08156, 1.67264, 6.00755, 5.78731, 0.508713, -0.202668, 1.09517, 2.6238, 6.0285, 5.85715, 3.22093, 69.0574, 35.2775, 0.48969, 0.498403, 0.443549, 1, 0.379062, 0.366963, 0.389535, 0}), cloud.getMoments().size()); } /// Creates a room, and calls denoise using a fixed distance threshols, and compares to expected result @@ -121,7 +131,7 @@ namespace raytest EXPECT_EQ(command("raydenoise room.ply 3 cm"), 0); ray::Cloud cloud; EXPECT_TRUE(cloud.load("room_denoised.ply")); - compareMoments(cloud.getMoments(), {-0.108066, -0.0410134, 0.052168, 8.67026e-08, 8.81787e-08, 2.24394e-08, -0.464107, -0.113806, 0.161496, 2.82122, 2.34281, 1.35279, 17.81, 10.2005, 0.297047, 0.758802, 0.440232, 0.975166, 0.317215, 0.226682, 0.390971, 0.155618}); + EXPECT_EQ(compareMoments(cloud.getMoments(), {-0.108066, -0.0410134, 0.052168, 8.67026e-08, 8.81787e-08, 2.24394e-08, -0.464107, -0.113806, 0.161496, 2.82122, 2.34281, 1.35279, 17.81, 10.2005, 0.297047, 0.758802, 0.440232, 0.975166, 0.317215, 0.226682, 0.390971, 0.155618}), cloud.getMoments().size()); } /// Creates two rooms, the second is decimated and transformed, then rayrestore is called to apply this transformation to @@ -136,7 +146,7 @@ namespace raytest EXPECT_EQ(command("rayrestore room2_decimated.ply 10 cm room.ply"), 0); ray::Cloud cloud; EXPECT_TRUE(cloud.load("room_restored.ply")); - compareMoments(cloud.getMoments(), {2.07399, 0.575952, 3.05217, 7.85442e-08, 7.70963e-08, 1.93877e-08, 1.9391, 0.682169, 3.06563, 2.10068, 2.45642, 1.28226, 17.539, 10.1994, 0.304682, 0.761892, 0.429502, 0.987362, 0.318932, 0.225742, 0.389901, 0.111705}); + EXPECT_EQ(compareMoments(cloud.getMoments(), {2.07399, 0.575952, 3.05217, 7.85442e-08, 7.70963e-08, 1.93877e-08, 1.9391, 0.682169, 3.06563, 2.10068, 2.45642, 1.28226, 17.539, 10.1994, 0.304682, 0.761892, 0.429502, 0.987362, 0.318932, 0.225742, 0.389901, 0.111705}), cloud.getMoments().size()); } /// Creates a forest and rotates it in all three axes, comparing to the expected result @@ -146,7 +156,7 @@ namespace raytest EXPECT_EQ(command("rayrotate forest.ply 10,20,30"), 0); ray::Cloud cloud; EXPECT_TRUE(cloud.load("forest.ply")); - compareMoments(cloud.getMoments(), {-0.254879, 0.846076, 2.02322, 5.62648, 5.68622, 2.56306, 0.266873, 0.772016, 3.2888, 5.54595, 5.69021, 4.28394, 62.683, 36.1903, 0.514327, 0.504407, 0.413534, 1, 0.372377, 0.365965, 0.391709, 0}); + EXPECT_EQ(compareMoments(cloud.getMoments(), {-0.254879, 0.846076, 2.02322, 5.62648, 5.68622, 2.56306, 0.266873, 0.772016, 3.2888, 5.54595, 5.69021, 4.28394, 62.683, 36.1903, 0.514327, 0.504407, 0.413534, 1, 0.372377, 0.365965, 0.391709, 0}), cloud.getMoments().size()); } /// Creates a room and smooths this ray cloud, comparing to the expected result @@ -156,7 +166,7 @@ namespace raytest EXPECT_EQ(command("raysmooth room.ply"), 0); ray::Cloud cloud; EXPECT_TRUE(cloud.load("room_smooth.ply")); - compareMoments(cloud.getMoments(), {-0.108066, -0.0410134, 0.052168, 7.05134e-08, 8.45038e-08, 1.93877e-08, -0.27615, -0.0761079, 0.0656267, 2.42413, 2.13691, 1.28163, 17.539, 10.1994, 0.304682, 0.761892, 0.429502, 0.987362, 0.318932, 0.225742, 0.389901, 0.111705}); + EXPECT_EQ(compareMoments(cloud.getMoments(), {-0.108066, -0.0410134, 0.052168, 7.05134e-08, 8.45038e-08, 1.93877e-08, -0.27615, -0.0761079, 0.0656267, 2.42413, 2.13691, 1.28163, 17.539, 10.1994, 0.304682, 0.761892, 0.429502, 0.987362, 0.318932, 0.225742, 0.389901, 0.111705}), cloud.getMoments().size()); } /// Creates a room, then splits it around a plane, comparing agaisnt the expected result @@ -166,7 +176,7 @@ namespace raytest EXPECT_EQ(command("raysplit room.ply plane 0,0.1,1.5"), 0); ray::Cloud cloud; EXPECT_TRUE(cloud.load("room_outside.ply")); - compareMoments(cloud.getMoments(), {-0.467731, 1.05075, 1.43662, 2.20441, 1.60162, 0.106775, -0.77974, 1.03139, 1.57353, 3.67521, 2.64766, 0.485084, 17.3995, 10.279, 0.311066, 0.759795, 0.425206, 0.951355, 0.321609, 0.226785, 0.39073, 0.215125}); + EXPECT_EQ(compareMoments(cloud.getMoments(), {-0.467731, 1.05075, 1.43662, 2.20441, 1.60162, 0.106775, -0.77974, 1.03139, 1.57353, 3.67521, 2.64766, 0.485084, 17.3995, 10.279, 0.311066, 0.759795, 0.425206, 0.951355, 0.321609, 0.226785, 0.39073, 0.215125}), cloud.getMoments().size()); } /// Creates a room and runs raytransients, comparing the identified transients ray cloud to the expected results @@ -176,7 +186,7 @@ namespace raytest EXPECT_EQ(command("raytransients min room.ply 1 rays"), 0); ray::Cloud cloud; EXPECT_TRUE(cloud.load("room_transient.ply")); - compareMoments(cloud.getMoments(), {-1.05406, -0.240721, -0.0629182, 5.05649e-08, 3.32941e-08, 2.54759e-08, 0.268724, -0.136746, -0.596782, 1.04798, 0.921776, 0.527205, 32.1452, 6.7491, 0.205871, 0.395641, 0.884296, 1, 0.225501, 0.296487, 0.153923, 0}); + EXPECT_EQ(compareMoments(cloud.getMoments(), {-1.05406, -0.240721, -0.0629182, 5.05649e-08, 3.32941e-08, 2.54759e-08, 0.268724, -0.136746, -0.596782, 1.04798, 0.921776, 0.527205, 32.1452, 6.7491, 0.205871, 0.395641, 0.884296, 1, 0.225501, 0.296487, 0.153923, 0}), cloud.getMoments().size()); } /// Creates a forest and translates it in all three axes, comparing to the expected result @@ -186,7 +196,7 @@ namespace raytest EXPECT_EQ(command("raytranslate forest.ply 10,20,30"), 0); ray::Cloud cloud; EXPECT_TRUE(cloud.load("forest.ply")); - compareMoments(cloud.getMoments(), {9.66298, 21.3454, 31.7177, 6.0926, 5.75511, 0.56438, 9.69155, 21.3605, 33.0883, 6.10555, 5.82564, 3.20507, 62.683, 36.1903, 0.514327, 0.504407, 0.413534, 1, 0.372377, 0.365965, 0.391709, 0}); + EXPECT_EQ(compareMoments(cloud.getMoments(), {9.66298, 21.3454, 31.7177, 6.0926, 5.75511, 0.56438, 9.69155, 21.3605, 33.0883, 6.10555, 5.82564, 3.20507, 62.683, 36.1903, 0.514327, 0.504407, 0.413534, 1, 0.372377, 0.365965, 0.391709, 0}), cloud.getMoments().size()); } #if RAYLIB_WITH_QHULL @@ -197,7 +207,7 @@ namespace raytest EXPECT_EQ(command("raywrap terrain.ply upwards 1.0"), 0); ray::Mesh mesh; EXPECT_TRUE(ray::readPlyMesh("terrain_mesh.ply", mesh)); - compareMoments(mesh.getMoments(), {0.0386662, -1.52168, -0.139079, 3.30621, 3.35391, 0.705937}); + EXPECT_EQ(compareMoments(mesh.getMoments(), {0.0386662, -1.52168, -0.139079, 3.30621, 3.35391, 0.705937}), mesh.getMoments().size()); } /// Tests extraction of terrain and extraction of trees @@ -207,24 +217,24 @@ namespace raytest EXPECT_EQ(command("rayextract terrain forest.ply"), 0); ray::Mesh mesh; EXPECT_TRUE(ray::readPlyMesh("forest_mesh.ply", mesh)); - compareMoments(mesh.getMoments(), {-0.00147491, -0.00191917, -0.0617946, 5.77995, 5.80266, 0.0426993}); + EXPECT_EQ(compareMoments(mesh.getMoments(), {-0.00147491, -0.00191917, -0.0617946, 5.77995, 5.80266, 0.0426993}), mesh.getMoments().size()); EXPECT_EQ(command("rayextract trees forest.ply forest_mesh.ply"), 0); ray::ForestStructure forest; EXPECT_TRUE(forest.load("forest_trees.txt")); - compareMomentsPercentageError(forest.getMoments(), {20, 22.2172, 1058.42, 1.39416, 0.112794, 1.6403, 21918, 0, 75.75}); + EXPECT_EQ(compareMomentsPercentageError(forest.getMoments(), {20, 22.2172, 1058.42, 1.39416, 0.112794, 2.2863, 21918, 0, 105.4}), forest.getMoments().size()); EXPECT_EQ(command("rayextract forest forest.ply --ground forest_mesh.ply"), 0); ray::ForestStructure forest2; EXPECT_TRUE(forest2.load("forest_forest.txt")); - compareMoments(forest2.getMoments(), {11, 8.43829, 586.427, 1.40054, 0.200644, 0, 16697, 8.49419, 3.09917}); + EXPECT_EQ(compareMomentsPercentageError(forest2.getMoments(), {11, 8.43829, 586.427, 1.40054, 0.200644, 0, 16697, 8.49419, 3.09917}), forest2.getMoments().size()); EXPECT_EQ(command("rayextract trunks forest.ply"), 0); ray::ForestStructure forest3; EXPECT_TRUE(forest3.load("forest_trunks.txt")); - compareMoments(forest3.getMoments(), {21, 20.0797, 1124.61, 1.60427, 0.135159, 0, 0, 0, 0}); + EXPECT_EQ(compareMomentsPercentageError(forest3.getMoments(), {21, 20.0797, 1124.61, 1.60427, 0.135159, 0, 0, 0, 0}), forest3.getMoments().size()); } #endif // RAYLIB_WITH_QHULL } // raytest From d4acc5254dbe95a8bdf7367d55ce092a0ca5c248 Mon Sep 17 00:00:00 2001 From: Rod Taylor Date: Thu, 19 Feb 2026 22:06:47 +0000 Subject: [PATCH 27/37] No more std::rand. --- raylib/extraction/rayforest_watershed.cpp | 15 ++++++++------- raylib/extraction/rayleaves.cpp | 7 ++++--- raylib/extraction/rayterrain.cpp | 3 ++- raylib/extraction/raytrunks.cpp | 7 ++++--- raylib/raybuildinggen.cpp | 8 ++++---- raylib/rayrandom.h | 7 ++++--- 6 files changed, 26 insertions(+), 21 deletions(-) diff --git a/raylib/extraction/rayforest_watershed.cpp b/raylib/extraction/rayforest_watershed.cpp index e31ba7360..fc346cb90 100644 --- a/raylib/extraction/rayforest_watershed.cpp +++ b/raylib/extraction/rayforest_watershed.cpp @@ -11,6 +11,7 @@ #include #include #include "../rayply.h" +#include "../rayrandom.h" namespace ray { @@ -39,9 +40,9 @@ void Forest::renderWatershed(const std::string &cloud_name_stub, std::vector(x), 0.5 + static_cast(y), 0); pos[2] = original_heightfield_(x, y); @@ -54,10 +55,10 @@ void Forest::renderWatershed(const std::string &cloud_name_stub, std::vector leaves; std::vector leaf_counter(grid.voxels().size()); - std::srand(1); + ::ray::srand(1); for (size_t i = 0; i= 1.0) { diff --git a/raylib/extraction/rayterrain.cpp b/raylib/extraction/rayterrain.cpp index 718bf8004..8fcf11ea2 100644 --- a/raylib/extraction/rayterrain.cpp +++ b/raylib/extraction/rayterrain.cpp @@ -9,6 +9,7 @@ #include "../rayply.h" #include "../rayprogress.h" #include "../rayprogressthread.h" +#include "../rayrandom.h" #if RAYLIB_WITH_TBB #include @@ -123,7 +124,7 @@ void constructOctalSpacePartition(std::vector &nodes, std::vector 0) { - const int ind = rand() % static_cast(points.size()); + const int ind = ::ray::rand() % static_cast(points.size()); nodes[i++].pos = points[ind]; points[ind] = points.back(); points.pop_back(); diff --git a/raylib/extraction/raytrunks.cpp b/raylib/extraction/raytrunks.cpp index 0b83acf94..a706ca591 100644 --- a/raylib/extraction/raytrunks.cpp +++ b/raylib/extraction/raytrunks.cpp @@ -9,6 +9,7 @@ #include "../raycuboid.h" #include "../raygrid.h" #include "../rayply.h" +#include "../rayrandom.h" #include "raygrid2d.h" namespace ray @@ -652,9 +653,9 @@ void Trunks::saveDebugTrunks(const std::string &filename, bool verbose, const st for (auto &id : lowest_trunk_ids) { const Trunk &trunk = trunks[id]; - colour.red = uint8_t(rand() % 255); - colour.green = uint8_t(rand() % 255); - colour.blue = uint8_t(rand() % 255); + colour.red = uint8_t(::ray::rand() % 255); + colour.green = uint8_t(::ray::rand() % 255); + colour.blue = uint8_t(::ray::rand() % 255); const Eigen::Vector3d side1 = trunk.dir.cross(Eigen::Vector3d(1, 2, 3)).normalized(); const Eigen::Vector3d side2 = side1.cross(trunk.dir); diff --git a/raylib/raybuildinggen.cpp b/raylib/raybuildinggen.cpp index 86b0e9d0a..3c7c4ffd9 100644 --- a/raylib/raybuildinggen.cpp +++ b/raylib/raybuildinggen.cpp @@ -67,7 +67,7 @@ void BuildingGen::splitRoom(const Cuboid &cuboid, std::vector &cuboids, double table_width = 1.0; double table_length = 1.5; const double table_height = 1.1; - if (ray::rand() % 2) + if (::ray::rand() % 2) std::swap(table_width, table_length); Eigen::Vector3d table_rad = 0.5 * Eigen::Vector3d(table_width, table_length, table_height); Eigen::Vector3d table_pos( @@ -87,8 +87,8 @@ void BuildingGen::splitRoom(const Cuboid &cuboid, std::vector &cuboids, double cupboard_width = 0.6; double cupboard_length = 2.0; double cupboard_height = random(1.0, 3.0); - int wall_id = ray::rand() % 2; - int side = ray::rand() % 2; + int wall_id = ::ray::rand() % 2; + int side = ::ray::rand() % 2; if (wall_id == 1) std::swap(cupboard_width, cupboard_length); Eigen::Vector3d cupboard_rad = 0.5 * Eigen::Vector3d(cupboard_width, cupboard_length, cupboard_height); @@ -201,7 +201,7 @@ void BuildingGen::generate() left.max_bound_[0] = building.min_bound_[0] - params_.outer_wall_width; left.min_bound_[0] = -big; cuboids.push_back(left); - int num_windows = ray::rand() % 11; + int num_windows = ::ray::rand() % 11; for (int i = 0; i < num_windows; i++) { Eigen::Vector3d pos = diff --git a/raylib/rayrandom.h b/raylib/rayrandom.h index 7c676c6f8..5e1fbf7a7 100644 --- a/raylib/rayrandom.h +++ b/raylib/rayrandom.h @@ -18,12 +18,13 @@ namespace ray /// This is about ~5x faster then using the standard std::rand() method with no additional drawbacks. class RAYLIB_EXPORT PCGRandomGenerator { -public: +private: PCGRandomGenerator() { seed(12748, 3147, 792751, 14992); } // we can't initialise using std::rand, as it isn't platform independent +public: /// The minimum possible result value (inclusive). For compatibility with @c std::shuffle() static constexpr unsigned min() { return std::numeric_limits::min(); } /// The maximum possible result value (inclusive). For compatibility with @c std::shuffle() @@ -62,14 +63,14 @@ class RAYLIB_EXPORT PCGRandomGenerator }; /// Return a random number using some magic engine behind the hood. -/// Use this over std::rand() as there's a good chance this will be faster/better. +/// Use this over std::rand() as it will be consistent across platforms. unsigned int RAYLIB_EXPORT rand(); void RAYLIB_EXPORT srand(unsigned int seed); /// Return a uniformed number between [0,1). inline double randUniformDouble() { - return static_cast(ray::rand() % std::numeric_limits::max()) / + return static_cast(::ray::rand() % std::numeric_limits::max()) / static_cast(std::numeric_limits::max()); } } // namespace ray From 6cf7fbe5523ee4891b8f41a13d97a875dea187f3 Mon Sep 17 00:00:00 2001 From: Rod Taylor Date: Thu, 19 Feb 2026 23:37:26 +0000 Subject: [PATCH 28/37] Fix compiler warnings. --- raylib/extraction/raygrid2d.cpp | 2 +- raylib/extraction/raytrunks.cpp | 1 + raylib/imageread.h | 17 +++++----- raylib/rayalignment.cpp | 4 +-- raylib/rayutils.h | 57 +++++++++++++++++++-------------- 5 files changed, 46 insertions(+), 35 deletions(-) diff --git a/raylib/extraction/raygrid2d.cpp b/raylib/extraction/raygrid2d.cpp index 36efceb9d..fb5536064 100644 --- a/raylib/extraction/raygrid2d.cpp +++ b/raylib/extraction/raygrid2d.cpp @@ -203,7 +203,7 @@ void RayIndexGrid2D::init(const Eigen::Vector3d &min_bound, const Eigen::Vector3 << std::endl; pixels_.resize(dims_[0] * dims_[1]); - memset(&pixels_[0], 0, sizeof(Pixel) * pixels_.size()); + std::fill(pixels_.begin(), pixels_.end(), Pixel{}); } void RayIndexGrid2D::fillRays(const Cloud &cloud) diff --git a/raylib/extraction/raytrunks.cpp b/raylib/extraction/raytrunks.cpp index a706ca591..1040e87be 100644 --- a/raylib/extraction/raytrunks.cpp +++ b/raylib/extraction/raytrunks.cpp @@ -726,6 +726,7 @@ std::vector> Trunks::load(const std::string & if (i < 3) { base[i] = std::stod(token.c_str()); + radius = 0.0; } else { diff --git a/raylib/imageread.h b/raylib/imageread.h index bafd7117a..8a5e7fdb8 100644 --- a/raylib/imageread.h +++ b/raylib/imageread.h @@ -3038,9 +3038,9 @@ static int stbi__parse_entropy_coded_data(stbi__jpeg *z) static void stbi__jpeg_dequantize(short *data, stbi__uint16 *dequant) { - int i; + std::size_t i; for (i=0; i < 64; ++i) - data[i] *= dequant[i]; + data[i] *= static_cast(dequant[i]); } static void stbi__jpeg_finish(stbi__jpeg *z) @@ -4996,10 +4996,10 @@ static void stbi__de_iphone(stbi__png *z) stbi_uc a = p[3]; stbi_uc t = p[0]; if (a) { - stbi_uc half = a / 2; - p[0] = (p[2] * 255 + half) / a; - p[1] = (p[1] * 255 + half) / a; - p[2] = ( t * 255 + half) / a; + stbi_uc half = a >> 1; // divided by 2. + p[0] = static_cast((p[2] * 255 + half) / a); + p[1] = static_cast((p[1] * 255 + half) / a); + p[2] = static_cast(( t * 255 + half) / a); } else { p[0] = p[2]; p[2] = t; @@ -7055,9 +7055,10 @@ static void stbi__hdr_convert(float *output, stbi_uc *input, int req_comp) if ( input[3] != 0 ) { float f1; // Exponent - f1 = (float) ldexp(1.0f, input[3] - (int)(128 + 8)); + f1 = ldexp(1.0f, input[3] - (int)(128 + 8)); if (req_comp <= 2) - output[0] = (input[0] + input[1] + input[2]) * f1 / 3; + // Promote stbi_uc int to prevent overflow. Result always fits in range of float. + output[0] = static_cast(static_cast(input[0]) + input[1] + input[2]) * f1 / 3.0f; else { output[0] = input[0] * f1; output[1] = input[1] * f1; diff --git a/raylib/rayalignment.cpp b/raylib/rayalignment.cpp index c94421011..fae1a506e 100644 --- a/raylib/rayalignment.cpp +++ b/raylib/rayalignment.cpp @@ -56,7 +56,7 @@ void Array3D::init(const Eigen::Vector3d &box_min, double voxel_width, const Eig voxel_width_ = voxel_width; dims_ = dimensions; cells_.resize(dims_[0] * dims_[1] * dims_[2]); - memset(&cells_[0], 0, cells_.size() * sizeof(Complex)); + std::fill(cells_.begin(), cells_.end(), Complex{}); null_cell_ = 0; } @@ -150,7 +150,7 @@ void Array3D::fillWithRays(const Cloud &cloud) void Array1D::init(int length) { cells_.resize(length); - memset(&cells_[0], 0, cells_.size() * sizeof(Complex)); + std::fill(cells_.begin(), cells_.end(), Complex{}); } void Array1D::operator*=(const Array1D &other) diff --git a/raylib/rayutils.h b/raylib/rayutils.h index 470c5e27a..396c74d4b 100644 --- a/raylib/rayutils.h +++ b/raylib/rayutils.h @@ -9,7 +9,10 @@ #include "raylib/raylibconfig.h" #include "rayrandom.h" +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" #include +#pragma GCC diagnostic pop #include #include #include @@ -45,13 +48,13 @@ inline int runWithMemoryCheck(std::function main_f std::cerr << "Error: Not enough memory to process the input file," << std::endl; std::cerr << "consider using raydecimate or raysplit grid to operate on a smaller file." << std::endl; return 1; - } + } catch (std::length_error const &) // catch any memory allocation problems in generating large images { std::cerr << "Error: Not enough memory to process the input file," << std::endl; std::cerr << "consider using raydecimate or raysplit grid to operate on a smaller file." << std::endl; return 1; - } + } } inline std::vector split(const std::string &s, char delim) @@ -165,7 +168,7 @@ inline T median(std::vector list) { typename std::vector::iterator middle2 = middle - 1; nth_element(first, middle2, last); - return (*middle + *middle2) / static_cast(2); + return static_cast(*middle + *middle2) / static_cast(2); } } @@ -198,16 +201,21 @@ inline std::vector componentList(const std::vector &list, const T &compone struct RGBA { - RGBA(){} - RGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : red(r), green(g), blue(b), alpha(a) {} + RGBA() {} + RGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a) + : red(r) + , green(g) + , blue(b) + , alpha(a) + {} uint8_t red; uint8_t green; uint8_t blue; uint8_t alpha; - static RGBA white(){ return RGBA(255, 255, 255, 255); } - static RGBA terrain(){ return RGBA(149,105,72, 255); } - static RGBA treetrunk(){ return RGBA(192,166,141, 255); } - static RGBA leaves(){ return RGBA(60,102,44, 255); } + static RGBA white() { return RGBA(255, 255, 255, 255); } + static RGBA terrain() { return RGBA(149, 105, 72, 255); } + static RGBA treetrunk() { return RGBA(192, 166, 141, 255); } + static RGBA leaves() { return RGBA(60, 102, 44, 255); } }; /// Converts a value from 0 to 1 into a RGBA structure @@ -370,33 +378,34 @@ inline int sign(double x) } // for similar appraoch see: https://github.com/StrandedKitty/tiles-intersect/blob/master/src/index.js -template +template void walkGrid(const Eigen::Vector3d &start, const Eigen::Vector3d &end, T &object) { Eigen::Vector3d direction = end - start; double max_length = direction.norm(); Eigen::Vector3i p = Eigen::Vector3d(std::floor(start[0]), std::floor(start[1]), std::floor(start[2])).cast(); - const Eigen::Vector3i target = Eigen::Vector3d(std::floor(end[0]), std::floor(end[1]), std::floor(end[2])).cast(); - + const Eigen::Vector3i target = + Eigen::Vector3d(std::floor(end[0]), std::floor(end[1]), std::floor(end[2])).cast(); + const Eigen::Vector3i step(sign(direction[0]), sign(direction[1]), sign(direction[2])); direction /= max_length; - float eps = 1e-10; // remove tiny about so grid walking doesn't exceed its boundary + float eps = 1e-10f; // remove tiny about so grid walking doesn't exceed its boundary max_length -= eps; Eigen::Vector3d lengths, length_delta; - for (int j = 0; j<3; j++) + for (int j = 0; j < 3; j++) { - const double to = step[j] > 0 ? (double)p[j] + 1.0 - start[j] : start[j] - (double)p[j]; + const double to = step[j] > 0 ? (double)p[j] + 1.0 - start[j] : start[j] - (double)p[j]; const double dir = std::max(std::numeric_limits::epsilon(), std::abs(direction[j])); lengths[j] = to / dir; length_delta[j] = 1.0 / dir; } int ax = lengths[0] < lengths[1] && lengths[0] < lengths[2] ? 0 : (lengths[1] < lengths[2] ? 1 : 2); if (object(p, target, 0.0, lengths[ax], max_length)) - { - return; // only adding to one cell + { + return; // only adding to one cell } - - while (lengths[ax] < max_length) + + while (lengths[ax] < max_length) { p[ax] += step[ax]; const double in_length = lengths[ax]; @@ -404,19 +413,19 @@ void walkGrid(const Eigen::Vector3d &start, const Eigen::Vector3d &end, T &objec ax = lengths[0] < lengths[1] && lengths[0] < lengths[2] ? 0 : (lengths[1] < lengths[2] ? 1 : 2); if (object(p, target, in_length, lengths[ax], max_length)) { - break; // only adding to one cell - } - } + break; // only adding to one cell + } + } } inline int viewFile(const std::string &file_name, const std::string &file2_name = "") -{ +{ // Force Qt to use X11 platform (not Wayland). Can remove the first string if wish to use default render platform std::string command = "QT_QPA_PLATFORM=xcb " + std::string(R_VISTOOL) + " " + file_name; if (file2_name != "") command += " " + file2_name; return system(command.c_str()); -} +} } // namespace ray From f0c8531716d05df4f379805a6eb60e37e5617a55 Mon Sep 17 00:00:00 2001 From: Rod Taylor Date: Thu, 19 Feb 2026 22:24:48 +0000 Subject: [PATCH 29/37] Auto format on save on vscode. --- .gitignore | 1 - .vscode/settings.json | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 2ea21aab5..2fb1b8cee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -.vscode/ .idea/ build*/ cmake-build*/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..0637bc338 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "editor.formatOnSave": true, + "editor.rulers": [ + 120 + ], + "files.trimFinalNewlines": true, + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + "C_Cpp.default.cppStandard": "c++20", + "C_Cpp.default.cStandard": "c99", + "C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json", + "C_Cpp.files.exclude": { + "**/.vscode": true, + "build/**": true, + }, + "C_Cpp.exclusionPolicy": "checkFolders", + "[cpp]": { + "editor.defaultFormatter": "ms-vscode.cpptools", + "editor.tabSize": 2 + } +} From 40690498f1735017c83dec98e235dd08f59190a1 Mon Sep 17 00:00:00 2001 From: Rod Taylor Date: Fri, 29 May 2026 15:16:30 +1000 Subject: [PATCH 30/37] CMake not happy with old CMake. --- .devcontainer/Dockerfile | 1 - .devcontainer/devcontainer.json | 3 ++- docker/Dockerfile | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1074f22e5..ca35e7ce4 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -29,7 +29,6 @@ RUN apt-get update && apt-get upgrade -y && \ # Clone, build and clean up LASzip (3.x supports LAS 1.4 via laszip_api.h C API) RUN git clone https://github.com/LASzip/LASzip.git && \ cd LASzip && \ - git checkout tags/3.4.4 && \ mkdir build && cd build && \ cmake .. -DCMAKE_BUILD_TYPE=Release && \ make -j$(nproc) && \ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4ead7d7da..c5aa2947d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,8 @@ "name": "RayCloudTools Development", "build": { "dockerfile": "Dockerfile", - "context": ".." + "context": "..", + "options": ["--progress=plain"] }, "customizations": { "vscode": { diff --git a/docker/Dockerfile b/docker/Dockerfile index 0df8349c2..e06cf5a41 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -34,7 +34,6 @@ RUN mkdir -p /deps && \ # Clone, build and clean up LASzip (3.x supports LAS 1.4 via laszip_api.h C API) RUN git clone https://github.com/LASzip/LASzip.git && \ cd LASzip && \ - git checkout tags/3.4.4 && \ mkdir build && cd build && \ cmake .. -DCMAKE_BUILD_TYPE=Release && \ make -j$(nproc) && \ @@ -97,7 +96,7 @@ RUN git clone https://github.com/csiro-robotics/treetools.git && \ make install && \ cd ../.. && \ rm -rf treetools - + # Update ldconfig RUN ldconfig /usr/local/lib From 3171c06bbce7cbbc1788a43bf873996a72596f4a Mon Sep 17 00:00:00 2001 From: Rod Taylor Date: Mon, 1 Jun 2026 10:25:03 +1000 Subject: [PATCH 31/37] Suppress more warnings. --- raylib/extraction/rayterrain.cpp | 30 ++++++++++++++++++++++++------ raylib/raylaz.cpp | 4 ++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/raylib/extraction/rayterrain.cpp b/raylib/extraction/rayterrain.cpp index 8fcf11ea2..e4425a26d 100644 --- a/raylib/extraction/rayterrain.cpp +++ b/raylib/extraction/rayterrain.cpp @@ -50,8 +50,8 @@ struct Node { num_visits++; // This checks in a cone rather than just the corner of a cube shape that you would get -// from a raw Pareto front calculation -#define CONE_CHECK +// from a raw Pareto front calculation +#define CONE_CHECK Vector4d dif = corner - pos; if (dif == Vector4d(0, 0, 0, 0)) { @@ -178,7 +178,7 @@ void Terrain::getParetoFront(const std::vector &points, std::vector(0, nodes.size(), process_rays); #else - #pragma omp parallel for +#pragma omp parallel for for (size_t n = 0; n < nodes.size(); n++) { process_rays(n); @@ -201,7 +201,7 @@ void Terrain::getParetoFront(const std::vector &points, std::vector &positions, double gradient) { #if RAYLIB_WITH_QHULL - // The idea behind ground extraction is to tilt the upwards vector to the (1,1,1) direction then + // The idea behind ground extraction is to tilt the upwards vector to the (1,1,1) direction then // find the Pareto front in the three principle axes. https://en.wikipedia.org/wiki/Pareto_front // // Efficient Pareto front calculation is based on: Algorithms and Analyses for Maximal Vector Computation. Godfrey @@ -256,6 +256,10 @@ void Terrain::growUpwards(const std::vector &positions, double mesh_.indexList() = hull.mesh().indexList(); mesh_.vertices() = vecs; +#else + RAYLIB_UNUSED(positions); + RAYLIB_UNUSED(gradient); + std::cerr << "growUpwards: extracting terrain requires QHull, see README instructions for installation" << std::endl; #endif } @@ -350,11 +354,20 @@ void Terrain::growUpwardsFast(const std::vector &ends, double p std::cout << "size before: " << ends.size() << ", size after: " << points.size() << std::endl; growUpwards(points, gradient); +#else + RAYLIB_UNUSED(ends); + RAYLIB_UNUSED(pixel_width); + RAYLIB_UNUSED(min_bound); + RAYLIB_UNUSED(max_bound); + RAYLIB_UNUSED(gradient); + std::cerr << "growUpwardsFast: extracting terrain requires QHull, see README instructions for installation" + << std::endl; #endif } -// Convert the @c cloud input to the mesh_ member variable. -void Terrain::extract(const Cloud &cloud, const Eigen::Vector3d &offset, const std::string &file_prefix, double gradient, bool verbose) +// Convert the @c cloud input to the mesh_ member variable. +void Terrain::extract(const Cloud &cloud, const Eigen::Vector3d &offset, const std::string &file_prefix, + double gradient, bool verbose) { #if RAYLIB_WITH_QHULL // preprocessing to make the cloud smaller. @@ -387,6 +400,11 @@ void Terrain::extract(const Cloud &cloud, const Eigen::Vector3d &offset, const s local_cloud.save(file_prefix + "_terrain.ply"); } #else + RAYLIB_UNUSED(cloud); + RAYLIB_UNUSED(offset); + RAYLIB_UNUSED(file_prefix); + RAYLIB_UNUSED(gradient); + RAYLIB_UNUSED(verbose); std::cerr << "Error: extracting terrain requires QHull, see README instructions for installation" << std::endl; #endif } diff --git a/raylib/raylaz.cpp b/raylib/raylaz.cpp index 439c9ce75..e429c0017 100644 --- a/raylib/raylaz.cpp +++ b/raylib/raylaz.cpp @@ -153,6 +153,7 @@ bool readLas(const std::string &file_name, std::cout << "loaded " << file_name << " with " << number_of_points << " points" << std::endl; return true; #else // RAYLIB_WITH_LAS + RAYLIB_UNUSED(offset_to_remove); RAYLIB_UNUSED(max_intensity); RAYLIB_UNUSED(file_name); RAYLIB_UNUSED(apply); @@ -169,8 +170,7 @@ bool readLas(std::string file_name, std::vector &positions, std { std::vector starts; // dummy as lax just reads in point clouds, not ray clouds auto apply = [&](std::vector &start_points, std::vector &end_points, - std::vector &time_points, std::vector &colour_values) - { + std::vector &time_points, std::vector &colour_values) { starts.insert(starts.end(), start_points.begin(), start_points.end()); positions.insert(positions.end(), end_points.begin(), end_points.end()); times.insert(times.end(), time_points.begin(), time_points.end()); From bedc516535b225f4c4c555a59279fecefa210bd9 Mon Sep 17 00:00:00 2001 From: Rod Taylor Date: Mon, 1 Jun 2026 10:26:03 +1000 Subject: [PATCH 32/37] Add treetools to gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2fb1b8cee..399bfaf18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ build*/ cmake-build*/ +treetools/ From 15ab9685f4ab3d64924653098cdc224add3df58e Mon Sep 17 00:00:00 2001 From: Rod Taylor Date: Mon, 1 Jun 2026 14:14:30 +1000 Subject: [PATCH 33/37] Unpin some dependency revisions. --- .devcontainer/Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ca35e7ce4..4d659e82d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -38,7 +38,6 @@ RUN git clone https://github.com/LASzip/LASzip.git && \ # Clone, build and clean up Qhull RUN git clone https://github.com/qhull/qhull.git && \ cd qhull && \ - git checkout tags/v7.3.2 && \ cd build && \ cmake .. -DCMAKE_POSITION_INDEPENDENT_CODE:BOOL=true && \ make -j$(nproc) && \ @@ -48,7 +47,6 @@ RUN git clone https://github.com/qhull/qhull.git && \ # Clone, build and clean up libnabo RUN git clone https://github.com/ethz-asl/libnabo.git && \ cd libnabo && \ - git checkout tags/1.0.7 && \ mkdir build && cd build && \ cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo && \ make -j$(nproc) && \ From 8237372c8542bb717464509e4663cc3edfbc1141 Mon Sep 17 00:00:00 2001 From: Rod Taylor Date: Mon, 1 Jun 2026 14:15:55 +1000 Subject: [PATCH 34/37] Warn if system call in raydiffer fails. --- raylib/raydiffer.cpp | 24 ++++++++++++++---------- raylib/rayutils.h | 6 +++++- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/raylib/raydiffer.cpp b/raylib/raydiffer.cpp index c2b17ee14..8ecf77ac0 100644 --- a/raylib/raydiffer.cpp +++ b/raylib/raydiffer.cpp @@ -13,7 +13,7 @@ // if the points are in a plane then k=2 // if the points are in a volume then k=3 // this macro finds the k that best fit the uniform distribution. Otherwise we default to k=2 -#define FIND_CORRELATION_DIMENSION +#define FIND_CORRELATION_DIMENSION namespace ray { @@ -30,7 +30,7 @@ void calcNearestNeighbourDistances(const std::vector &cloud1, c num_bounded2++; Eigen::MatrixXf points_p(3, num_bounded); int j = 0; - for (unsigned int i = 0; i < cloud1.size(); i++) + for (unsigned int i = 0; i < cloud1.size(); i++) { if (cloud1[i][0] != std::numeric_limits::max()) points_p.col(j++) = cloud1[i]; @@ -39,7 +39,7 @@ void calcNearestNeighbourDistances(const std::vector &cloud1, c Eigen::MatrixXf points_q(3, num_bounded2); j = 0; - for (unsigned int i = 0; i < cloud2.size(); i++) + for (unsigned int i = 0; i < cloud2.size(); i++) { if (cloud2[i][0] != std::numeric_limits::max()) points_q.col(j++) = cloud2[i]; @@ -74,7 +74,7 @@ double getShoulder(double k, std::vector sorted_dists, double &min_error_ // 1. transform the result to make the distances uniform if their 3D points are uniformly distributed for (int i = 0; i sorted_dists, double &min_error_ double I = (float)i - yN; outside_const[i] = (float)((double)outside_const[i+1] + I*I); outside_linear[i] = (float)((double)outside_linear[i+1] + 2.0*I*d); - outside_square[i] =(float)((double)outside_square[i+1] + d*d); + outside_square[i] =(float)((double)outside_square[i+1] + d*d); } // 3. accumulate linear term forwards, but store only best results @@ -96,7 +96,7 @@ double getShoulder(double k, std::vector sorted_dists, double &min_error_ double inside_linear = 0.0; double inside_square = 0.0; double min_error_sqr = 0.0; - double min_error_i = 0.0; + double min_error_i = 0.0; min_error_dist = 0.0; for (int i = 0; i sorted_dists, double &min_error_ double linear = outside_linear[i]/d; double a = square; double b = -linear - 2.0*yN*square; - double c = outside_const[i] + linear*yN + square*yN*yN; + double c = outside_const[i] + linear*yN + square*yN*yN; // add the inside part a += inside_square/sqr(sorted_dists[i]); // the division is to match the gradient to y @@ -227,7 +227,7 @@ double printDistanceStatistics(const std::vector &dists_to_cloud1, const return similarity; } -bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std::string &cloud2_namestub, std::vector &dists_to_cloud1, +bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std::string &cloud2_namestub, std::vector &dists_to_cloud1, std::vector &dists_to_cloud2, double dist_threshold, bool individual_files, bool visualise) { std::cout << "saving out differences, coloured red for differences to " << cloud1_namestub << ".ply and green for differences in " << cloud2_namestub << ".ply" << std::endl; @@ -247,7 +247,7 @@ bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std:: std::vector *dists = &dists_to_cloud1; const float eps = 1e-8f; // in case there is inaccuracy in the KNN distance estimation for co-located point pairs auto colour = [&](std::vector &starts, std::vector &ends, - std::vector ×, std::vector &colours) + std::vector ×, std::vector &colours) { // firstly we store a count per cell chunk.clear(); @@ -283,7 +283,7 @@ bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std:: if (!individual_files) { auto colour2 = [&](std::vector &starts, std::vector &ends, - std::vector ×, std::vector &colours) + std::vector ×, std::vector &colours) { // firstly we store a count per cell chunk.clear(); @@ -313,7 +313,9 @@ bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std:: writer.end(); if (visualise) + { viewFile(cloud1_namestub + "_diff.ply"); + } } else { @@ -326,7 +328,9 @@ bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std:: return false; writer.end(); if (visualise) + { viewFile(cloud1_namestub + "_diff.ply", cloud2_namestub + "_diff.ply"); + } } return true; } diff --git a/raylib/rayutils.h b/raylib/rayutils.h index 396c74d4b..c2e016442 100644 --- a/raylib/rayutils.h +++ b/raylib/rayutils.h @@ -424,7 +424,11 @@ inline int viewFile(const std::string &file_name, const std::string &file2_name std::string command = "QT_QPA_PLATFORM=xcb " + std::string(R_VISTOOL) + " " + file_name; if (file2_name != "") command += " " + file2_name; - return system(command.c_str()); + const auto status = system(command.c_str()); + if (status != 0) { + std::cerr << "failed to view file with command: " << command << std::endl; + } + return status; } } // namespace ray From 5940f542ab6ffb0d111bbfe80f95ff3a08c3e05a Mon Sep 17 00:00:00 2001 From: Tom Hines Date: Fri, 12 Jun 2026 10:16:40 +1000 Subject: [PATCH 35/37] .devcontainer: Made workspace mount name explicit so that it can be depended on even if the repo on the host has a different name. --- .devcontainer/commands/compile_install.sh | 4 +-- .devcontainer/devcontainer.json | 40 +++++++++++++---------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/.devcontainer/commands/compile_install.sh b/.devcontainer/commands/compile_install.sh index f2ae4d625..25a9dcc2e 100755 --- a/.devcontainer/commands/compile_install.sh +++ b/.devcontainer/commands/compile_install.sh @@ -5,7 +5,7 @@ set -e echo "Building RayCloudTools..." cd /workspaces/raycloudtools mkdir -p build -cd build +cd build # Construct the cmake command cmake .. \ @@ -41,4 +41,4 @@ make -j"$(nproc)" make install ldconfig /usr/local/lib -echo "Build process completed successfully." \ No newline at end of file +echo "Build process completed successfully." diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c5aa2947d..7f94ffde0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,20 +1,24 @@ { - "name": "RayCloudTools Development", - "build": { - "dockerfile": "Dockerfile", - "context": "..", - "options": ["--progress=plain"] - }, - "customizations": { - "vscode": { - "extensions": [ - "ms-vscode.cpptools", - "ms-vscode.cmake-tools", - "ms-vscode.cpptools-extension-pack" - ] - } - }, - "remoteUser": "root", - "forwardPorts": [], - "postCreateCommand": ".devcontainer/commands/compile_install.sh" + "name": "RayCloudTools Development", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "options": [ + "--progress=plain" + ] + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools", + "ms-vscode.cpptools-extension-pack" + ] + } + }, + "remoteUser": "root", + "forwardPorts": [], + "postCreateCommand": ".devcontainer/commands/compile_install.sh", + "workspaceFolder": "/workspaces/raycloudtools", + "workspaceMount": "type=bind,source=${localWorkspaceFolder},target=/workspaces/raycloudtools" } From 968e9e48278b99b52997ce750b39d00e898d3fa6 Mon Sep 17 00:00:00 2001 From: Tom Hines Date: Fri, 12 Jun 2026 10:21:45 +1000 Subject: [PATCH 36/37] Restored changes that were lost when resolving merge conflicts: Copy .devcontainer/commands/compile_install.sh into PATH in devcontainer. Added --security-opt=label=disable to devcontainer run args. --- .devcontainer/Dockerfile | 4 ++++ .devcontainer/devcontainer.json | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 4d659e82d..ec73932cc 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -56,6 +56,10 @@ RUN git clone https://github.com/ethz-asl/libnabo.git && \ # Update ldconfig RUN ldconfig /usr/local/lib +# Copy build script into image +COPY .devcontainer/commands/compile_install.sh /usr/local/bin/compile_install.sh +RUN chmod +x /usr/local/bin/compile_install.sh + # Set the working directory WORKDIR /workspaces/raycloudtools diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7f94ffde0..4c7553a0b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,9 +16,15 @@ ] } }, + "mounts": [ + "type=bind,source=${localWorkspaceFolder}/.devcontainer/commands/compile_install.sh,target=/usr/local/bin/compile_install.sh", + ], "remoteUser": "root", "forwardPorts": [], - "postCreateCommand": ".devcontainer/commands/compile_install.sh", + "postCreateCommand": "compile_install.sh", + "runArgs": [ + "--security-opt=label=disable" + ], "workspaceFolder": "/workspaces/raycloudtools", "workspaceMount": "type=bind,source=${localWorkspaceFolder},target=/workspaces/raycloudtools" } From f23d0bfa129f04888709826c26599f3fb68bf69a Mon Sep 17 00:00:00 2001 From: Thomas Lowe Date: Tue, 16 Jun 2026 15:01:19 +1000 Subject: [PATCH 37/37] changed raydiff colour to be more different than natural colours --- raycloudtools/raydiff/raydiff.cpp | 2 +- raylib/raydiffer.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/raycloudtools/raydiff/raydiff.cpp b/raycloudtools/raydiff/raydiff.cpp index ee60d38c6..e4f29d6b9 100644 --- a/raycloudtools/raydiff/raydiff.cpp +++ b/raycloudtools/raydiff/raydiff.cpp @@ -17,7 +17,7 @@ void usage(int exit_code = 1) { // clang-format off - std::cout << "Difference between two ray clouds output a coloured cloud, cloud1 differences in red, cloud2 differences in green, and similarity printed to screen." << std::endl; + std::cout << "Difference between two ray clouds output a coloured cloud, cloud1 differences in scarlet, cloud2 differences in green/cyan, and similarity printed to screen." << std::endl; std::cout << "usage (--view / -v to view results):" << std::endl; std::cout << "raydiff cloud1.ply cloud2.ply" << std::endl; std::cout << " --distance 0 - optional threshold in m for colouring differences. Default auto-detects distribution shoulder" << std::endl; diff --git a/raylib/raydiffer.cpp b/raylib/raydiffer.cpp index 8ecf77ac0..5d6e149ef 100644 --- a/raylib/raydiffer.cpp +++ b/raylib/raydiffer.cpp @@ -230,7 +230,7 @@ double printDistanceStatistics(const std::vector &dists_to_cloud1, const bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std::string &cloud2_namestub, std::vector &dists_to_cloud1, std::vector &dists_to_cloud2, double dist_threshold, bool individual_files, bool visualise) { - std::cout << "saving out differences, coloured red for differences to " << cloud1_namestub << ".ply and green for differences in " << cloud2_namestub << ".ply" << std::endl; + std::cout << "saving out differences, coloured scarlet for differences to " << cloud1_namestub << ".ply and green/cyan for differences in " << cloud2_namestub << ".ply" << std::endl; // now render visuals CloudWriter writer; @@ -243,7 +243,7 @@ bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std:: std::vector samples; int j = 0; - Eigen::Vector3d diff_col(255,0,0); + Eigen::Vector3d diff_col(255,0,127); std::vector *dists = &dists_to_cloud1; const float eps = 1e-8f; // in case there is inaccuracy in the KNN distance estimation for co-located point pairs auto colour = [&](std::vector &starts, std::vector &ends, @@ -277,7 +277,7 @@ bool writeDifferencesToRayClouds(const std::string &cloud1_namestub, const std:: j = 0; dists_to_cloud1.clear(); dists_to_cloud1.shrink_to_fit(); - Eigen::Vector3d diff2_col(0,255,0); + Eigen::Vector3d diff2_col(0,255,127); dists = &dists_to_cloud2; if (!individual_files)