From 7e45420ed4596b6caf9320046902257ec65491b4 Mon Sep 17 00:00:00 2001 From: Tim Devereux Date: Fri, 15 May 2026 03:59:25 +0000 Subject: [PATCH 1/5] Write LAS/LAZ 1.4 and preserve output format extension - LasRayCloudWriter and LasWriter now write LAS 1.4 (version_minor=4, point formats 7 and 6 respectively) instead of LAS 1.2 - passthrough expanded from 6 to 8 bytes/point using LAS 1.4 extended fields (extended_return_number, extended_classification, extended_scan_angle etc.) - LAS 1.2 sources converted to 8-byte LAS 1.4 layout on read - Both LasWriter and LasRayCloudWriter destructors now patch the LAS 1.4 64-bit extended point count at header offset 247 in addition to the legacy 32-bit count at offset 107 - All tool output filenames now inherit the input file extension (.las, .laz, or .ply) instead of hardcoding .las - raysplitter splitGrid and splitColour output files also use input extension --- raycloudtools/rayalign/rayalign.cpp | 5 +- raycloudtools/raycolour/raycolour.cpp | 2 +- raycloudtools/raycombine/raycombine.cpp | 6 +- raycloudtools/raydenoise/raydenoise.cpp | 2 +- raycloudtools/raydiff/raydiff.cpp | 8 +- raycloudtools/rayrestore/rayrestore.cpp | 2 +- raycloudtools/raysmooth/raysmooth.cpp | 2 +- raycloudtools/raysplit/raysplit.cpp | 5 +- raycloudtools/raytransients/raytransients.cpp | 5 +- raylib/raycloud.cpp | 14 +- raylib/raycloud.h | 13 +- raylib/raylaz.cpp | 129 +++++++++++------- raylib/raysplitter.cpp | 15 +- 13 files changed, 127 insertions(+), 81 deletions(-) diff --git a/raycloudtools/rayalign/rayalign.cpp b/raycloudtools/rayalign/rayalign.cpp index d7e0f7402..4c2d54a57 100644 --- a/raycloudtools/rayalign/rayalign.cpp +++ b/raycloudtools/rayalign/rayalign.cpp @@ -43,7 +43,8 @@ int rayAlign(int argc, char *argv[]) if (!cross_align && !self_align) usage(); - std::string aligned_name = cloud_a.nameStub() + "_aligned.las"; + const std::string ext_a = ray::getFileNameExtension(cloud_a.name()); + std::string aligned_name = cloud_a.nameStub() + "_aligned." + ext_a; if (self_align) { if (!ray::alignCloudToAxes(cloud_a.name(), aligned_name)) @@ -79,7 +80,7 @@ int rayAlign(int argc, char *argv[]) { alignCloud0ToCloud1(clouds, 0.5, verbose); if (verbose) - clouds[0].save(cloud_a.nameStub() + "_coarse_aligned.las"); + clouds[0].save(cloud_a.nameStub() + "_coarse_aligned." + ext_a); } ray::FineAlignment fineAlign(clouds, non_rigid, verbose); diff --git a/raycloudtools/raycolour/raycolour.cpp b/raycloudtools/raycolour/raycolour.cpp index ee61d3e12..52626b6a3 100644 --- a/raycloudtools/raycolour/raycolour.cpp +++ b/raycloudtools/raycolour/raycolour.cpp @@ -112,7 +112,7 @@ int rayColour(int argc, char *argv[]) usage(); std::string in_file = cloud_file.name(); - const std::string out_file = cloud_file.nameStub() + "_coloured.las"; + const std::string out_file = cloud_file.nameStub() + "_coloured." + ray::getFileNameExtension(cloud_file.name()); const std::string type = colour_type.selectedKey(); uint8_t split_alpha = 100; diff --git a/raycloudtools/raycombine/raycombine.cpp b/raycloudtools/raycombine/raycombine.cpp index 2fe682949..8aebddc04 100644 --- a/raycloudtools/raycombine/raycombine.cpp +++ b/raycloudtools/raycombine/raycombine.cpp @@ -116,7 +116,9 @@ int rayCombine(int argc, char *argv[]) { config.merge_type = ray::MergeType::All; } - std::string combined_file = output.isSet() ? output_file.name() : file_stub + "_combined.las"; + const std::string combine_ext = ray::getFileNameExtension( + (threeway || threeway_concatenate) ? base_cloud.name() : cloud_files.files()[0].name()); + std::string combined_file = output.isSet() ? output_file.name() : file_stub + "_combined." + combine_ext; if (concatenate_all) { ray::CloudWriter writer; @@ -161,7 +163,7 @@ int rayCombine(int argc, char *argv[]) merger.mergeMultiple(clouds, &progress); std::cout << merger.differenceCloud().rayCount() << " transients, " << merger.fixedCloud().rayCount() << " fixed rays." << std::endl; - merger.differenceCloud().save(file_stub + "_differences.las"); + merger.differenceCloud().save(file_stub + "_differences." + combine_ext); } progress_thread.join(); diff --git a/raycloudtools/raydenoise/raydenoise.cpp b/raycloudtools/raydenoise/raydenoise.cpp index 92c5f9a4e..30e8ae600 100644 --- a/raycloudtools/raydenoise/raydenoise.cpp +++ b/raycloudtools/raydenoise/raydenoise.cpp @@ -149,7 +149,7 @@ int rayDenoise(int argc, char *argv[]) << " rays removed with nearest neighbour sigma more than " << sigmas.value() << std::endl; } - new_cloud.save(cloud_file.nameStub() + "_denoised.las"); + new_cloud.save(cloud_file.nameStub() + "_denoised." + ray::getFileNameExtension(cloud_file.name())); return 0; } diff --git a/raycloudtools/raydiff/raydiff.cpp b/raycloudtools/raydiff/raydiff.cpp index d7a34d67e..2ffd72836 100644 --- a/raycloudtools/raydiff/raydiff.cpp +++ b/raycloudtools/raydiff/raydiff.cpp @@ -261,7 +261,9 @@ int rayDiff(int argc, char *argv[]) j++; } } - cloud1.save(cloud1_name.nameStub() + "_diff.las"); + const std::string ext1 = ray::getFileNameExtension(cloud1_name.name()); + const std::string ext2 = ray::getFileNameExtension(cloud2_name.name()); + cloud1.save(cloud1_name.nameStub() + "_diff." + ext1); j = 0; for (int i = 0; i<(int)cloud2.ends.size(); i++) { @@ -277,11 +279,11 @@ int rayDiff(int argc, char *argv[]) j++; } } - cloud2.save(cloud2_name.nameStub() + "_diff.las"); + cloud2.save(cloud2_name.nameStub() + "_diff." + ext2); if (visualise.isSet()) { - std::string command = std::string(VISUALISE_TOOL) + std::string(" ") + cloud1_name.nameStub() + "_diff.ply " + cloud2_name.nameStub() + "_diff.las"; + std::string command = std::string(VISUALISE_TOOL) + std::string(" ") + cloud1_name.nameStub() + "_diff." + ext1 + " " + cloud2_name.nameStub() + "_diff." + ext2; system(command.c_str()); } return (int)similarity; diff --git a/raycloudtools/rayrestore/rayrestore.cpp b/raycloudtools/rayrestore/rayrestore.cpp index 0ea39440b..3db99af59 100644 --- a/raycloudtools/rayrestore/rayrestore.cpp +++ b/raycloudtools/rayrestore/rayrestore.cpp @@ -232,7 +232,7 @@ int rayRestore(int argc, char *argv[]) // now apply the estimated transformation. We need to chunk save the _restored file, using the // re-chunkloaded full_cloud_file ray::CloudWriter writer; - if (!writer.begin(full_cloud_file.nameStub() + "_restored.las")) + if (!writer.begin(full_cloud_file.nameStub() + "_restored." + ray::getFileNameExtension(full_cloud_file.name()))) usage(); ray::Cloud chunk; diff --git a/raycloudtools/raysmooth/raysmooth.cpp b/raycloudtools/raysmooth/raysmooth.cpp index b4f298ef5..bad0acd89 100644 --- a/raycloudtools/raysmooth/raysmooth.cpp +++ b/raycloudtools/raysmooth/raysmooth.cpp @@ -65,7 +65,7 @@ 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.las"); + cloud.save(cloud_file.nameStub() + "_smooth." + ray::getFileNameExtension(cloud_file.name())); return 0; } diff --git a/raycloudtools/raysplit/raysplit.cpp b/raycloudtools/raysplit/raysplit.cpp index 71b785270..3d508bd51 100644 --- a/raycloudtools/raysplit/raysplit.cpp +++ b/raycloudtools/raysplit/raysplit.cpp @@ -74,8 +74,9 @@ int raySplit(int argc, char *argv[]) usage(); } - const std::string in_name = cloud_file.nameStub() + "_inside.las"; - const std::string out_name = cloud_file.nameStub() + "_outside.las"; + const std::string split_ext = ray::getFileNameExtension(cloud_file.name()); + const std::string in_name = cloud_file.nameStub() + "_inside." + split_ext; + const std::string out_name = cloud_file.nameStub() + "_outside." + split_ext; const std::string rc_name = cloud_file.name(); // ray cloud name bool res = true; diff --git a/raycloudtools/raytransients/raytransients.cpp b/raycloudtools/raytransients/raytransients.cpp index de22f93b7..c4f1b8634 100644 --- a/raycloudtools/raytransients/raytransients.cpp +++ b/raycloudtools/raytransients/raytransients.cpp @@ -85,8 +85,9 @@ int rayTransients(int argc, char *argv[]) const ray::Cloud &transient = filter.differenceCloud(); const ray::Cloud &fixed = filter.fixedCloud(); - transient.save(cloud_file.nameStub() + "_transient.las"); - fixed.save(cloud_file.nameStub() + "_fixed.las"); + const std::string ext = ray::getFileNameExtension(cloud_file.name()); + transient.save(cloud_file.nameStub() + "_transient." + ext); + fixed.save(cloud_file.nameStub() + "_fixed." + ext); return 0; } diff --git a/raylib/raycloud.cpp b/raylib/raycloud.cpp index db34edbc1..efa2d1bf7 100644 --- a/raylib/raycloud.cpp +++ b/raylib/raycloud.cpp @@ -181,14 +181,14 @@ void Cloud::removeUnboundedRays() times[i] = times[valids[i]]; colours[i] = colours[valids[i]]; if (!passthrough.empty()) - memcpy(passthrough.data() + i * 6, passthrough.data() + valids[i] * 6, 6); + memcpy(passthrough.data() + i * 8, passthrough.data() + valids[i] * 8, 8); } starts.resize(valids.size()); ends.resize(valids.size()); times.resize(valids.size()); colours.resize(valids.size()); if (!passthrough.empty()) - passthrough.resize(valids.size() * 6); + passthrough.resize(valids.size() * 8); } void Cloud::decimate(double voxel_width, std::set &voxel_set) @@ -203,14 +203,14 @@ void Cloud::decimate(double voxel_width, std::set colours[i] = colours[id]; times[i] = times[id]; if (!passthrough.empty()) - memcpy(passthrough.data() + i * 6, passthrough.data() + id * 6, 6); + memcpy(passthrough.data() + i * 8, passthrough.data() + id * 8, 8); } starts.resize(subsample.size()); ends.resize(subsample.size()); colours.resize(subsample.size()); times.resize(subsample.size()); if (!passthrough.empty()) - passthrough.resize(subsample.size() * 6); + passthrough.resize(subsample.size() * 8); } void Cloud::eigenSolve(const std::vector &ray_ids, const Eigen::MatrixXi &indices, int index, int num_neighbours, @@ -499,10 +499,10 @@ void Cloud::addRay(const Cloud &other_cloud, size_t index) colours.push_back(other_cloud.colours[index]); if (!other_cloud.tree_ids.empty()) tree_ids.push_back(other_cloud.tree_ids[index]); - if (other_cloud.passthrough.size() >= (index + 1) * 6) + if (other_cloud.passthrough.size() >= (index + 1) * 8) { - const uint8_t *src = other_cloud.passthrough.data() + index * 6; - passthrough.insert(passthrough.end(), src, src + 6); + const uint8_t *src = other_cloud.passthrough.data() + index * 8; + passthrough.insert(passthrough.end(), src, src + 8); } } diff --git a/raylib/raycloud.h b/raylib/raycloud.h index 65bed11ef..fed2d4661 100644 --- a/raylib/raycloud.h +++ b/raylib/raycloud.h @@ -39,13 +39,14 @@ class RAYLIB_EXPORT Cloud std::vector times; std::vector colours; std::vector tree_ids; ///< optional per-point tree ID (populated by rayextract trees) - /// Optional flat array of non-raycloud LAS fields, 6 bytes per point: - /// [0] return_number[0:2] | num_returns[3:5] | scan_dir[6] | edge[7] - /// [1] classification[0:4] | synthetic[5] | keypoint[6] | withheld[7] - /// [2] scan_angle_rank (int8 reinterpreted as uint8) + /// Optional flat array of LAS 1.4 extended fields, 8 bytes per point: + /// [0] ext_return_number[0:3] | ext_num_returns[4:7] + /// [1] classification_flags[0:3] | scanner_channel[4:5] | scan_dir[6] | edge[7] + /// [2] extended_classification (0-255) /// [3] user_data - /// [4..5] point_source_ID (little-endian) - /// Empty unless loaded from a LAS/LAZ file. + /// [4..5] extended_scan_angle (int16 LE, 0.006 deg units) + /// [6..7] point_source_ID (uint16 LE) + /// LAS 1.2 sources are converted to this layout on read. Empty unless loaded from LAS/LAZ. std::vector passthrough; void clear(); diff --git a/raylib/raylaz.cpp b/raylib/raylaz.cpp index d87a09f21..2c20947b8 100644 --- a/raylib/raylaz.cpp +++ b/raylib/raylaz.cpp @@ -143,21 +143,48 @@ bool readLas(const std::string &file_name, starts.push_back(position); } - // Pack the six standard LAS fields we don't use as raycloud fields into passthrough. + // Pack 8 bytes of LAS 1.4 extended fields per point into passthrough. + // Layout: [0] ext_return[0:3]|ext_num_returns[4:7] + // [1] class_flags[0:3]|scanner_chan[4:5]|scan_dir[6]|edge[7] + // [2] extended_classification + // [3] user_data + // [4-5] extended_scan_angle (int16 LE, 0.006 deg units) + // [6-7] point_source_ID (uint16 LE) if (passthrough_out) { - const uint8_t rb = static_cast(point->return_number) - | static_cast(point->number_of_returns << 3) - | static_cast(point->scan_direction_flag << 6) - | static_cast(point->edge_of_flight_line << 7); - const uint8_t cb = static_cast(point->classification) - | static_cast(point->synthetic_flag << 5) - | static_cast(point->keypoint_flag << 6) - | static_cast(point->withheld_flag << 7); - passthrough_out->push_back(rb); - passthrough_out->push_back(cb); - passthrough_out->push_back(static_cast(point->scan_angle_rank)); + uint8_t b0, b1, b2; + int16_t ext_angle; + if (format >= 6) + { + b0 = static_cast(point->extended_return_number & 0x0F) | + static_cast((point->extended_number_of_returns & 0x0F) << 4); + b1 = static_cast(point->extended_classification_flags & 0x0F) | + static_cast((point->extended_scanner_channel & 0x03) << 4) | + static_cast((point->scan_direction_flag & 0x1) << 6) | + static_cast((point->edge_of_flight_line & 0x1) << 7); + b2 = point->extended_classification; + ext_angle = point->extended_scan_angle; + } + else + { + // Convert LAS 1.2 legacy fields to LAS 1.4 extended layout. + b0 = static_cast(point->return_number & 0x0F) | + static_cast((point->number_of_returns & 0x0F) << 4); + b1 = static_cast(point->synthetic_flag & 0x1) | + static_cast((point->keypoint_flag & 0x1) << 1) | + static_cast((point->withheld_flag & 0x1) << 2) | + static_cast((point->scan_direction_flag & 0x1) << 6) | + static_cast((point->edge_of_flight_line & 0x1) << 7); + b2 = static_cast(point->classification & 0x1F); + // scan_angle_rank is integer degrees; extended_scan_angle is 0.006 deg units + ext_angle = static_cast(static_cast(point->scan_angle_rank) * 167); + } + passthrough_out->push_back(b0); + passthrough_out->push_back(b1); + passthrough_out->push_back(b2); passthrough_out->push_back(point->user_data); + passthrough_out->push_back(static_cast(static_cast(ext_angle) & 0xFFu)); + passthrough_out->push_back(static_cast(static_cast(ext_angle) >> 8)); passthrough_out->push_back(static_cast(point->point_source_ID & 0xFFu)); passthrough_out->push_back(static_cast(point->point_source_ID >> 8)); } @@ -268,8 +295,8 @@ 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 + header->version_minor = 4; + header->point_data_format = 6; // LAS 1.4: GPS time only const double scale = 1e-4; header->x_scale_factor = scale; header->y_scale_factor = scale; @@ -277,7 +304,7 @@ bool RAYLIB_EXPORT writeLas(std::string file_name, const std::vectorx_offset = 0.0; header->y_offset = 0.0; header->z_offset = 0.0; - header->number_of_point_records = static_cast(points.size()); + header->extended_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; @@ -337,8 +364,8 @@ LasWriter::LasWriter(const std::string &file_name) laszip_get_header_pointer(writer_handle_, &header_); header_->version_major = 1; - header_->version_minor = 2; - header_->point_data_format = 1; // GPS time only + header_->version_minor = 4; + header_->point_data_format = 6; // LAS 1.4: GPS time only const double scale = 1e-4; header_->x_scale_factor = scale; header_->y_scale_factor = scale; @@ -380,17 +407,20 @@ LasWriter::~LasWriter() laszip_update_inventory(writer_handle_); laszip_close_writer(writer_handle_); laszip_destroy(writer_handle_); - // laszip_close_writer clobbers number_of_point_records for streaming writes (our count - // wasn't known at open_writer). Patch it on disk: LAS 1.2 legacy count at offset 107. + // laszip_close_writer clobbers point counts for streaming writes. Patch both the legacy + // 32-bit count (offset 107) and the LAS 1.4 64-bit extended count (offset 247) on disk. if (points_written_ > 0) { std::fstream f(file_name_, std::ios::in | std::ios::out | std::ios::binary); if (f.is_open()) { - const laszip_U32 count = - static_cast(std::min(points_written_, std::numeric_limits::max())); + const laszip_U32 legacy = static_cast( + std::min(points_written_, std::numeric_limits::max())); f.seekp(107); - f.write(reinterpret_cast(&count), sizeof(count)); + f.write(reinterpret_cast(&legacy), sizeof(legacy)); + const laszip_U64 extended = static_cast(points_written_); + f.seekp(247); + f.write(reinterpret_cast(&extended), sizeof(extended)); } } } @@ -472,8 +502,8 @@ LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tre laszip_get_header_pointer(writer_handle_, &header); header->version_major = 1; - header->version_minor = 2; - header->point_data_format = 3; // GPS time + RGB + header->version_minor = 4; + header->point_data_format = 7; // LAS 1.4: GPS time + RGB (equivalent of format 3) const double scale = 1e-4; header->x_scale_factor = scale; header->y_scale_factor = scale; @@ -501,11 +531,10 @@ LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tre return; } - // Explicitly set point type 3 (GPS time + RGB) and total record size. - // laszip_add_attribute computes the record length using the default format 1 base (28), so we - // must override it here to the correct format 3 base (34) + extra bytes. - const laszip_U16 record_size = static_cast(34 + 12 + (with_tree_id_ ? 4 : 0)); - if (laszip_set_point_type_and_size(writer_handle_, 3, record_size)) + // Explicitly set point type 7 (LAS 1.4 GPS time + RGB) and total record size. + // LAS 1.4 format 7 base = 36 bytes. laszip_add_attribute uses a default base, so override here. + const laszip_U16 record_size = static_cast(36 + 12 + (with_tree_id_ ? 4 : 0)); + if (laszip_set_point_type_and_size(writer_handle_, 7, record_size)) { laszip_CHAR *error; laszip_get_error(writer_handle_, &error); @@ -560,16 +589,20 @@ LasRayCloudWriter::~LasRayCloudWriter() laszip_update_inventory(writer_handle_); laszip_close_writer(writer_handle_); laszip_destroy(writer_handle_); - // Patch the LAS 1.2 legacy point count on disk (same fix as LasWriter). + // laszip_close_writer clobbers point counts for streaming writes. Patch both the legacy + // 32-bit count (offset 107) and the LAS 1.4 64-bit extended count (offset 247) on disk. if (points_written_ > 0) { std::fstream f(file_name_, std::ios::in | std::ios::out | std::ios::binary); if (f.is_open()) { - const laszip_U32 count = - static_cast(std::min(points_written_, std::numeric_limits::max())); + const laszip_U32 legacy = static_cast( + std::min(points_written_, std::numeric_limits::max())); f.seekp(107); - f.write(reinterpret_cast(&count), sizeof(count)); + f.write(reinterpret_cast(&legacy), sizeof(legacy)); + const laszip_U64 extended = static_cast(points_written_); + f.seekp(247); + f.write(reinterpret_cast(&extended), sizeof(extended)); } } } @@ -600,21 +633,23 @@ bool LasRayCloudWriter::writeChunk(const std::vector &starts, point_->rgb[0] = static_cast(colours[i].red) * 257u; point_->rgb[1] = static_cast(colours[i].green) * 257u; point_->rgb[2] = static_cast(colours[i].blue) * 257u; - // Restore preserved standard LAS fields if available. - if (passthrough.size() >= (i + 1) * 6) + // Restore LAS 1.4 extended fields from 8-byte passthrough if available. + // Layout mirrors readLas passthrough packing (see readLas for byte definitions). + if (passthrough.size() >= (i + 1) * 8) { - const uint8_t *p = passthrough.data() + i * 6; - point_->return_number = p[0] & 0x7u; - point_->number_of_returns = (p[0] >> 3) & 0x7u; - point_->scan_direction_flag = (p[0] >> 6) & 0x1u; - point_->edge_of_flight_line = (p[0] >> 7) & 0x1u; - point_->classification = p[1] & 0x1Fu; - point_->synthetic_flag = (p[1] >> 5) & 0x1u; - point_->keypoint_flag = (p[1] >> 6) & 0x1u; - point_->withheld_flag = (p[1] >> 7) & 0x1u; - point_->scan_angle_rank = static_cast(p[2]); - point_->user_data = p[3]; - point_->point_source_ID = static_cast(p[4]) | (static_cast(p[5]) << 8); + const uint8_t *p = passthrough.data() + i * 8; + point_->extended_return_number = p[0] & 0x0Fu; + point_->extended_number_of_returns = (p[0] >> 4) & 0x0Fu; + point_->extended_classification_flags= p[1] & 0x0Fu; + point_->extended_scanner_channel = (p[1] >> 4) & 0x03u; + point_->scan_direction_flag = (p[1] >> 6) & 0x1u; + point_->edge_of_flight_line = (p[1] >> 7) & 0x1u; + point_->extended_classification = p[2]; + point_->user_data = p[3]; + int16_t ext_angle; + memcpy(&ext_angle, p + 4, 2); + point_->extended_scan_angle = ext_angle; + point_->point_source_ID = static_cast(p[6]) | (static_cast(p[7]) << 8); } // Store start - end as three float32 extra bytes so starts can be reconstructed. const float sx = static_cast(starts[i][0] - ends[i][0]); diff --git a/raylib/raysplitter.cpp b/raylib/raysplitter.cpp index 6007c834c..397e0290b 100644 --- a/raylib/raysplitter.cpp +++ b/raylib/raysplitter.cpp @@ -11,6 +11,7 @@ #include "raycloudwriter.h" #include "raycuboid.h" #include "extraction/raytrees.h" +#include "rayparse.h" namespace ray { @@ -305,7 +306,7 @@ bool splitGrid(const std::string &file_name, const std::string &cloud_name_stub, { overlap /= 2.0; // it now means overlap relative to grid edge Cloud::Info info; - Cloud::getInfo(cloud_name_stub + ".ply", info); + Cloud::getInfo(file_name, info); const Eigen::Vector3d &min_bound = info.rays_bound.min_bound_; const Eigen::Vector3d &max_bound = info.rays_bound.max_bound_; @@ -356,8 +357,9 @@ bool splitGrid(const std::string &file_name, const std::string &cloud_name_stub, std::vector chunks(max_open_files); // splitting performed per chunk + const std::string grid_ext = getFileNameExtension(file_name); auto per_chunk = [&min_index, &max_index, &width, min_time, &dimensions, &cells, &chunks, length, &cell_width, - &cloud_name_stub, &overlap, &pass, &max_open_files ](std::vector &starts, + &cloud_name_stub, &overlap, &pass, &max_open_files, &grid_ext](std::vector &starts, std::vector &ends, std::vector ×, std::vector &colours) { for (size_t i = 0; i < ends.size(); i++) @@ -417,7 +419,7 @@ bool splitGrid(const std::string &file_name, const std::string &cloud_name_stub, name << "_" << z; if (cell_width[3] > 0.0) name << "_" << t; - name << ".ply"; + name << "." << grid_ext; cells[index].begin(name.str()); } if (!cuboid.intersects(ends[i])) // end point is outside, so mark an unbounded ray @@ -512,7 +514,8 @@ bool splitColour(const std::string &file_name, const std::string &cloud_name_stu std::cout << "batch processing colours " << batch << ", to " << batch_max << std::endl; } // splitting performed per chunk - auto per_chunk = [&vox_map, &batch, &max_files_at_once, &chunk_size, &cells, &chunks, &cloud_name_stub, &num_colours, seg_colour]( + const std::string colour_ext = getFileNameExtension(file_name); + auto per_chunk = [&vox_map, &batch, &max_files_at_once, &chunk_size, &cells, &chunks, &cloud_name_stub, &num_colours, seg_colour, &colour_ext]( std::vector &starts, std::vector &ends, std::vector ×, std::vector &colours) { for (size_t i = 0; i < ends.size(); i++) @@ -532,11 +535,11 @@ bool splitColour(const std::string &file_name, const std::string &cloud_name_stu std::stringstream name; if (seg_colour) { - name << cloud_name_stub << "_" << convertColourToInt(colour) << ".ply"; + name << cloud_name_stub << "_" << convertColourToInt(colour) << "." << colour_ext; } else { - name << cloud_name_stub << "_" << (int)colour.red << "_" << (int)colour.green << "_" << (int)colour.blue << ".ply"; + name << cloud_name_stub << "_" << (int)colour.red << "_" << (int)colour.green << "_" << (int)colour.blue << "." << colour_ext; } cells[index].begin(name.str()); } From 99f0ae5129c178a48045f81f93e957ba28bed9de Mon Sep 17 00:00:00 2001 From: Tim Devereux Date: Fri, 15 May 2026 04:04:16 +0000 Subject: [PATCH 2/5] rayimport: preserve .laz extension when input is .laz --- raycloudtools/rayimport/rayimport.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/raycloudtools/rayimport/rayimport.cpp b/raycloudtools/rayimport/rayimport.cpp index e95420ae4..020bea68e 100644 --- a/raycloudtools/rayimport/rayimport.cpp +++ b/raycloudtools/rayimport/rayimport.cpp @@ -31,7 +31,7 @@ void usage(int exit_code = 1) std::cout << " --remove_start_pos - translate so first point is at 0,0,0" << std::endl; std::cout << "rayimport pointcloudfile unbound transformfile - load unbound data (pulses that missed) from RIEGL .rxp file" << std::endl; std::cout << " transformfile is a text file containing a 4x4 transformation matrix" << std::endl; - std::cout << "The output is a .las file of the same name (or with suffix _raycloud if the input was a .ply file)." << std::endl; + std::cout << "The output is a .las or .laz file of the same name (or with suffix _raycloud if the input was a .ply file)." << std::endl; // clang-format on exit(exit_code); } @@ -94,11 +94,13 @@ int rayImport(int argc, char *argv[]) } std::string save_file = cloud_file.nameStub(); - if (cloud_file.nameExt() == "ply") + const std::string in_ext = cloud_file.nameExt(); + if (in_ext == "ply") save_file += "_raycloud"; + const std::string save_ext = (in_ext == "laz") ? "laz" : "las"; size_t num_bounded; ray::CloudWriter writer; - if (!writer.begin(save_file + ".las")) + if (!writer.begin(save_file + "." + save_ext)) usage(); Eigen::Vector3d start_pos(0, 0, 0); double min_time = std::numeric_limits::max(); From 29c541be8bdd17bfb24ef0be15dc98826084d505 Mon Sep 17 00:00:00 2001 From: Tim Devereux Date: Fri, 15 May 2026 04:07:17 +0000 Subject: [PATCH 3/5] Set LAS 1.4 header_size=375 in all three writers LASzip rejects writes with version_minor=4 unless header_size is at least 235 (LAS 1.3+). Correct LAS 1.4 header is 375 bytes. --- raylib/raylaz.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/raylib/raylaz.cpp b/raylib/raylaz.cpp index 2c20947b8..5c22b7cd5 100644 --- a/raylib/raylaz.cpp +++ b/raylib/raylaz.cpp @@ -296,6 +296,7 @@ bool RAYLIB_EXPORT writeLas(std::string file_name, const std::vectorversion_major = 1; header->version_minor = 4; + header->header_size = 375; // LAS 1.4 header is 375 bytes header->point_data_format = 6; // LAS 1.4: GPS time only const double scale = 1e-4; header->x_scale_factor = scale; @@ -365,6 +366,7 @@ LasWriter::LasWriter(const std::string &file_name) header_->version_major = 1; header_->version_minor = 4; + header_->header_size = 375; // LAS 1.4 header is 375 bytes header_->point_data_format = 6; // LAS 1.4: GPS time only const double scale = 1e-4; header_->x_scale_factor = scale; @@ -503,6 +505,7 @@ LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tre header->version_major = 1; header->version_minor = 4; + header->header_size = 375; // LAS 1.4 header is 375 bytes header->point_data_format = 7; // LAS 1.4: GPS time + RGB (equivalent of format 3) const double scale = 1e-4; header->x_scale_factor = scale; From caec21fc1f41e816036eced2359b57360d87cefc Mon Sep 17 00:00:00 2001 From: Tim Devereux Date: Fri, 15 May 2026 04:15:49 +0000 Subject: [PATCH 4/5] Fix LAS 1.4 writer: set header_size=375 and offset_to_point_data LASzip internally uses the LAS 1.2 default header size (227) when computing offset_to_point_data during VLR additions. After all VLRs are added, adjust offset_to_point_data by +148 (375-227) in LasRayCloudWriter, and set it explicitly to 375 for LasWriter and writeLas which have no VLRs. Also fix rayimport: always add _raycloud suffix so the output file never conflicts with the input file. Preserves .laz extension for .laz inputs. --- raycloudtools/rayimport/rayimport.cpp | 6 ++---- raylib/raylaz.cpp | 8 ++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/raycloudtools/rayimport/rayimport.cpp b/raycloudtools/rayimport/rayimport.cpp index 020bea68e..be3c9a6ff 100644 --- a/raycloudtools/rayimport/rayimport.cpp +++ b/raycloudtools/rayimport/rayimport.cpp @@ -31,7 +31,7 @@ void usage(int exit_code = 1) std::cout << " --remove_start_pos - translate so first point is at 0,0,0" << std::endl; std::cout << "rayimport pointcloudfile unbound transformfile - load unbound data (pulses that missed) from RIEGL .rxp file" << std::endl; std::cout << " transformfile is a text file containing a 4x4 transformation matrix" << std::endl; - std::cout << "The output is a .las or .laz file of the same name (or with suffix _raycloud if the input was a .ply file)." << std::endl; + std::cout << "The output is a _raycloud.las/.laz file (preserving .laz if the input is .laz)." << std::endl; // clang-format on exit(exit_code); } @@ -93,10 +93,8 @@ int rayImport(int argc, char *argv[]) usage(); } - std::string save_file = cloud_file.nameStub(); + std::string save_file = cloud_file.nameStub() + "_raycloud"; const std::string in_ext = cloud_file.nameExt(); - if (in_ext == "ply") - save_file += "_raycloud"; const std::string save_ext = (in_ext == "laz") ? "laz" : "las"; size_t num_bounded; ray::CloudWriter writer; diff --git a/raylib/raylaz.cpp b/raylib/raylaz.cpp index 5c22b7cd5..a2bf81522 100644 --- a/raylib/raylaz.cpp +++ b/raylib/raylaz.cpp @@ -306,6 +306,7 @@ bool RAYLIB_EXPORT writeLas(std::string file_name, const std::vectory_offset = 0.0; header->z_offset = 0.0; header->extended_number_of_point_records = static_cast(points.size()); + header->offset_to_point_data = 375; // LAS 1.4 header, no VLRs const bool is_laz = file_name.find(".laz") != std::string::npos; std::cout << "Saving points to " << file_name << std::endl; @@ -375,6 +376,7 @@ LasWriter::LasWriter(const std::string &file_name) header_->x_offset = 0.0; header_->y_offset = 0.0; header_->z_offset = 0.0; + header_->offset_to_point_data = 375; // LAS 1.4 header, no VLRs const bool is_laz = file_name_.find(".laz") != std::string::npos; std::cout << "Saving points to " << file_name_ << std::endl; @@ -558,6 +560,12 @@ LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tre return; } + // LASzip internally computed offset_to_point_data using the LAS 1.2 default header + // size (227) when VLRs were added. Correct it for the LAS 1.4 header (375 bytes). + laszip_header_struct *hdr; + laszip_get_header_pointer(writer_handle_, &hdr); + hdr->offset_to_point_data += (375 - 227); + const bool is_laz = file_name_.find(".laz") != std::string::npos; std::cout << "Saving ray cloud to " << file_name_ << std::endl; From 486530504b7d68d0a55acb6f137ce5b22adedb62 Mon Sep 17 00:00:00 2001 From: Tim Devereux Date: Tue, 19 May 2026 02:08:04 +0000 Subject: [PATCH 5/5] Preserve LAS/LAZ sensor extra-byte attributes across all tools All raycloudtools that read and write LAS/LAZ files now pass the original sensor extra-byte VLR through unchanged, so attributes like InternalTime, Amplitude, BeamDirection, etc. survive every pipeline step: rayimport, raydecimate, raycolour, raytranslate, raysplit (all split modes), raycombine, raysmooth, raydenoise. Key changes: - raylaz: add readLasExtraBytesVlr helper; stream passthrough bytes (8 std LAS fields + orig sensor bytes) alongside each point; fix has_tree_id_attr detection from VLR instead of byte count - raycloudwriter: accept extra_bytes_vlr in begin() and register attributes before opening the laszip writer - raycloud: convertCloud (used by raytranslate/rayrotate) now pre-reads VLR and routes passthrough bytes per chunk - raysplitter: all six split functions (split, splitPlane, splitCapsule, splitBox, splitGrid, splitColour) pre-read VLR, pass it to every CloudWriter::begin call, and copy passthrough bytes to each output sub-ray including clipped pieces - raydecimation: streaming raydecimate passes passthrough through - raycolour, raycombine, rayimport, rayterrain: same pattern --- raycloudtools/raycolour/raycolour.cpp | 44 ++- raycloudtools/raycombine/raycombine.cpp | 48 ++- raycloudtools/raydecimate/raydecimate.cpp | 10 +- raycloudtools/rayimport/rayimport.cpp | 21 +- raylib/extraction/rayterrain.cpp | 14 +- raylib/raycloud.cpp | 65 +++- raylib/raycloud.h | 9 +- raylib/raycloudwriter.cpp | 9 +- raylib/raycloudwriter.h | 6 +- raylib/raydecimation.cpp | 319 +++++++++++++------- raylib/raydecimation.h | 16 +- raylib/raylaz.cpp | 244 ++++++++++++--- raylib/raylaz.h | 26 +- raylib/raysplitter.cpp | 347 +++++++++++++++++++--- 14 files changed, 923 insertions(+), 255 deletions(-) diff --git a/raycloudtools/raycolour/raycolour.cpp b/raycloudtools/raycolour/raycolour.cpp index 52626b6a3..fce75657c 100644 --- a/raycloudtools/raycolour/raycolour.cpp +++ b/raycloudtools/raycolour/raycolour.cpp @@ -7,6 +7,7 @@ #include "raylib/raycloud.h" #include "raylib/raycloudwriter.h" #include "raylib/rayparse.h" +#include "raylib/raylaz.h" #define STB_IMAGE_IMPLEMENTATION #include "raylib/imageread.h" @@ -54,7 +55,8 @@ void spectrumRGB(double value, ray::RGBA &colour) } /// Function to colour the cloud from a horizontal projection of a supplied image, stretching to match the cloud bounds. -void colourFromImage(const std::string &cloud_file, const std::string &image_file, ray::CloudWriter &writer) +void colourFromImage(const std::string &cloud_file, const std::string &image_file, ray::CloudWriter &writer, + std::vector &passthrough_buf) { ray::Cloud::Info info; if (!ray::Cloud::getInfo(cloud_file, info)) @@ -74,7 +76,8 @@ void colourFromImage(const std::string &cloud_file, const std::string &image_fil << ", stretching to fit) " << std::endl; } - auto colour_from_image = [&bounds, &writer, &image_data, width_x, width_y, width, height, num_channels]( + auto colour_from_image = [&bounds, &writer, &image_data, width_x, width_y, width, height, num_channels, + &passthrough_buf]( std::vector &starts, std::vector &ends, std::vector ×, std::vector &colours) { for (size_t i = 0; i < ends.size(); i++) @@ -86,10 +89,19 @@ void colourFromImage(const std::string &cloud_file, const std::string &image_fil colours[i].green = image_data[index + 1]; colours[i].blue = image_data[index + 2]; } - writer.writeChunk(starts, ends, times, colours); + std::vector chunk_pass(passthrough_buf); + passthrough_buf.clear(); + writer.writeChunk(starts, ends, times, colours, chunk_pass); }; - if (!ray::Cloud::read(cloud_file, colour_from_image)) + const std::string ext = ray::getFileNameExtension(cloud_file); + if (ext == "las" || ext == "laz") + { + size_t num_bounded; + if (!ray::readLas(cloud_file, colour_from_image, num_bounded, 1.0, nullptr, 1000000, nullptr, &passthrough_buf)) + usage(); + } + else if (!ray::Cloud::read(cloud_file, colour_from_image)) usage(); stbi_image_free(image_data); @@ -118,11 +130,19 @@ int rayColour(int argc, char *argv[]) if (type != "shape" && type != "normal" && type != "branches") // chunk loading possible for simple cases { + const std::string ext = ray::getFileNameExtension(cloud_file.name()); + uint16_t orig_extra = 0; + std::vector extra_bytes_vlr; + if (ext == "las" || ext == "laz") + ray::readLasExtraBytesVlr(cloud_file.name(), orig_extra, extra_bytes_vlr); + ray::CloudWriter writer; - if (!writer.begin(out_file)) + if (!writer.begin(out_file, extra_bytes_vlr)) usage(); - auto colour_rays = [flat_colour, flat_alpha, &type, &col, &alpha, &writer, &split_alpha]( + std::vector passthrough_buf; + + auto colour_rays = [flat_colour, flat_alpha, &type, &col, &alpha, &writer, &split_alpha, &passthrough_buf]( std::vector &starts, std::vector &ends, std::vector ×, std::vector &colours) { if (flat_colour) @@ -172,12 +192,20 @@ int rayColour(int argc, char *argv[]) else usage(); } - writer.writeChunk(starts, ends, times, colours); + std::vector chunk_pass(passthrough_buf); + passthrough_buf.clear(); + writer.writeChunk(starts, ends, times, colours, chunk_pass); }; if (image_format) { - colourFromImage(cloud_file.name(), image_file.name(), writer); + colourFromImage(cloud_file.name(), image_file.name(), writer, passthrough_buf); + } + else if (ext == "las" || ext == "laz") + { + size_t num_bounded; + if (!ray::readLas(cloud_file.name(), colour_rays, num_bounded, 1.0, nullptr, 1000000, nullptr, &passthrough_buf)) + usage(); } else if (!ray::Cloud::read(cloud_file.name(), colour_rays)) { diff --git a/raycloudtools/raycombine/raycombine.cpp b/raycloudtools/raycombine/raycombine.cpp index 8aebddc04..cca5276e3 100644 --- a/raycloudtools/raycombine/raycombine.cpp +++ b/raycloudtools/raycombine/raycombine.cpp @@ -11,6 +11,7 @@ #include "raylib/rayprogressthread.h" #include "raylib/raythreads.h" #include "raylib/raycloudwriter.h" +#include "raylib/raylaz.h" #include #include @@ -121,27 +122,44 @@ int rayCombine(int argc, char *argv[]) std::string combined_file = output.isSet() ? output_file.name() : file_stub + "_combined." + combine_ext; if (concatenate_all) { + // Pre-read extra-byte VLR from the first input file so the writer can register them. + std::vector extra_bytes_vlr; + const std::string first_file = cloud_files.files()[0].name(); + const std::string first_ext = ray::getFileNameExtension(first_file); + if (first_ext == "las" || first_ext == "laz") + { + uint16_t orig_extra = 0; + ray::readLasExtraBytesVlr(first_file, orig_extra, extra_bytes_vlr); + } + ray::CloudWriter writer; - if (!writer.begin(combined_file)) + if (!writer.begin(combined_file, extra_bytes_vlr)) usage(); - // By maintaining these buffers below, we avoid almost all memory fragmentation - auto concatenate = [&](std::vector &starts, std::vector &ends, - std::vector ×, std::vector &colours) - { - ray::Cloud chunk; - chunk.starts = starts; - chunk.ends = ends; - chunk.colours = colours; - chunk.times = times; - writer.writeChunk(chunk); - }; for (int i = 0; i < (int)cloud_files.files().size(); i++) { - if (!ray::Cloud::read(cloud_files.files()[i].name(), concatenate)) - usage(); + const std::string &fname = cloud_files.files()[i].name(); + const std::string fext = ray::getFileNameExtension(fname); + std::vector passthrough_buf; + auto concatenate = [&](std::vector &starts, std::vector &ends, + std::vector ×, std::vector &colours) { + std::vector chunk_pass(std::move(passthrough_buf)); + passthrough_buf.clear(); + writer.writeChunk(starts, ends, times, colours, chunk_pass); + }; + if (fext == "las" || fext == "laz") + { + size_t num_bounded; + if (!ray::readLas(fname, concatenate, num_bounded, 1.0, nullptr, 1000000, nullptr, &passthrough_buf)) + usage(); + } + else + { + if (!ray::Cloud::read(fname, concatenate)) + usage(); + } } - writer.end(); + writer.end(); return 0; } diff --git a/raycloudtools/raydecimate/raydecimate.cpp b/raycloudtools/raydecimate/raydecimate.cpp index 48ef5f695..f03290bdd 100644 --- a/raycloudtools/raydecimate/raydecimate.cpp +++ b/raycloudtools/raydecimate/raydecimate.cpp @@ -63,23 +63,23 @@ int rayDecimate(int argc, char *argv[]) bool res = false; if (double_format_points) { - res = ray::decimateSpatioTemporal(cloud_file.nameStub(), vox_width.value(), num_rays.value()); + res = ray::decimateSpatioTemporal(cloud_file.name(), vox_width.value(), num_rays.value()); } else if (quantity.selectedKey() == "cm/ray") { - res = ray::decimateRaysSpatial(cloud_file.nameStub(), width_for_ray.value()); + res = ray::decimateRaysSpatial(cloud_file.name(), width_for_ray.value()); } else if (quantity.selectedKey() == "cm") { - res = ray::decimateSpatial(cloud_file.nameStub(), vox_width.value()); + res = ray::decimateSpatial(cloud_file.name(), vox_width.value()); } else if (quantity.selectedKey() == "rays") { - res = ray::decimateTemporal(cloud_file.nameStub(), num_rays.value()); + res = ray::decimateTemporal(cloud_file.name(), num_rays.value()); } else if (quantity.selectedKey() == "cm/m") { - res = ray::decimateAngular(cloud_file.nameStub(), radius_per_length.value()); + res = ray::decimateAngular(cloud_file.name(), radius_per_length.value()); } if (!res) usage(); diff --git a/raycloudtools/rayimport/rayimport.cpp b/raycloudtools/rayimport/rayimport.cpp index be3c9a6ff..405a3b7a9 100644 --- a/raycloudtools/rayimport/rayimport.cpp +++ b/raycloudtools/rayimport/rayimport.cpp @@ -97,12 +97,24 @@ int rayImport(int argc, char *argv[]) const std::string in_ext = cloud_file.nameExt(); const std::string save_ext = (in_ext == "laz") ? "laz" : "las"; size_t num_bounded; + + // Pre-read original sensor extra-byte attributes from the input LAS/LAZ header so the writer + // can register and preserve them before opening the output file. + std::vector input_extra_bytes_vlr; + if (in_ext == "laz" || in_ext == "las") + { + uint16_t orig_extra = 0; + ray::readLasExtraBytesVlr(cloud_file.name(), orig_extra, input_extra_bytes_vlr); + } + ray::CloudWriter writer; - if (!writer.begin(save_file + "." + save_ext)) + if (!writer.begin(save_file + "." + save_ext, input_extra_bytes_vlr)) usage(); Eigen::Vector3d start_pos(0, 0, 0); double min_time = std::numeric_limits::max(); double max_time = std::numeric_limits::lowest(); + std::vector all_passthrough; + size_t prev_pass_size = 0; auto add_chunk = [&](std::vector &starts, std::vector &ends, std::vector ×, std::vector &colours) { if (start_pos.squaredNorm() == 0.0) @@ -178,7 +190,9 @@ int rayImport(int argc, char *argv[]) c.alpha = uint8_t(0); } } - if (!writer.writeChunk(starts, ends, times, colours)) + std::vector chunk_pass(all_passthrough.begin() + prev_pass_size, all_passthrough.end()); + prev_pass_size = all_passthrough.size(); + if (!writer.writeChunk(starts, ends, times, colours, chunk_pass)) usage(); }; @@ -218,7 +232,8 @@ int rayImport(int argc, char *argv[]) } else if (cloud_file.nameExt() == "laz" || cloud_file.nameExt() == "las") { - if (!ray::readLas(cloud_file.name(), add_chunk, num_bounded, maximum_intensity, offset)) + if (!ray::readLas(cloud_file.name(), add_chunk, num_bounded, maximum_intensity, offset, 1000000, nullptr, + &all_passthrough)) { usage(); } diff --git a/raylib/extraction/rayterrain.cpp b/raylib/extraction/rayterrain.cpp index 37f0aec9e..b337a63b2 100644 --- a/raylib/extraction/rayterrain.cpp +++ b/raylib/extraction/rayterrain.cpp @@ -237,6 +237,8 @@ void Terrain::growUpwards(const std::vector &positions, double // then find pareto the lower bound of the points getParetoFront(points, front); std::cout << "number of pareto front points: " << front.size() << std::endl; + if (front.empty()) + return; // then convert it into a mesh std::vector vecs(front.size()); @@ -278,6 +280,9 @@ void Terrain::growUpwardsFast(const std::vector &ends, double p const Eigen::Vector3d &min_bound, const Eigen::Vector3d &max_bound, double gradient) { #if RAYLIB_WITH_QHULL + if (ends.empty()) + return; + // the speed up is one of removing lots of 'above ground' points before running the growUpwards function // thereby making the problem size smaller. @@ -358,16 +363,19 @@ void Terrain::extract(const Cloud &cloud, const Eigen::Vector3d &offset, const s #if RAYLIB_WITH_QHULL // preprocessing to make the cloud smaller. Eigen::Vector3d min_bound, max_bound; - cloud.calcBounds(&min_bound, &max_bound); + if (!cloud.calcBounds(&min_bound, &max_bound)) + { + std::cerr << "Error: ray cloud has no bounded rays. Cannot extract terrain." << std::endl; + std::cerr << "If your sensor has no intensity data, re-import with --max_intensity 0 to mark all rays as bounded." << std::endl; + return; + } const double spacing = cloud.estimatePointSpacing(); const double pixel_width = 2.0 * spacing; std::vector ends; for (size_t i = 0; i < cloud.ends.size(); i++) { if (cloud.rayBounded(i)) - { ends.push_back(cloud.ends[i]); - } } growUpwardsFast(ends, pixel_width, min_bound, max_bound, gradient); mesh_.reduce(); // remove disconnected vertices in the mesh diff --git a/raylib/raycloud.cpp b/raylib/raycloud.cpp index efa2d1bf7..d35e14899 100644 --- a/raylib/raycloud.cpp +++ b/raylib/raycloud.cpp @@ -28,13 +28,15 @@ void Cloud::clear() colours.clear(); tree_ids.clear(); passthrough.clear(); + extra_bytes_size = 8; + extra_bytes_vlr.clear(); } void Cloud::save(const std::string &file_name) const { const std::string ext = getFileNameExtension(file_name); if (ext == "las" || ext == "laz") - writeLasRayCloud(file_name, starts, ends, times, colours, tree_ids, passthrough); + writeLasRayCloud(file_name, starts, ends, times, colours, tree_ids, passthrough, extra_bytes_vlr); else writePlyRayCloud(file_name, starts, ends, times, colours); } @@ -55,6 +57,8 @@ bool Cloud::loadLas(const std::string &file, int min_num_rays) { std::vector ids; std::vector pass; + uint16_t orig_extra = 0; + std::vector orig_vlr; auto apply = [&](std::vector &start_pts, std::vector &end_pts, std::vector &time_pts, std::vector &colour_pts) { starts.insert(starts.end(), start_pts.begin(), start_pts.end()); @@ -63,11 +67,15 @@ bool Cloud::loadLas(const std::string &file, int min_num_rays) colours.insert(colours.end(), colour_pts.begin(), colour_pts.end()); }; size_t num_bounded; - bool res = readLas(file, apply, num_bounded, 1.0, nullptr, 1000000, &ids, &pass); + bool res = readLas(file, apply, num_bounded, 1.0, nullptr, 1000000, &ids, &pass, &orig_extra, &orig_vlr); if (!ids.empty()) tree_ids = std::move(ids); if (!pass.empty()) + { passthrough = std::move(pass); + extra_bytes_size = 8 + orig_extra; + extra_bytes_vlr = std::move(orig_vlr); + } if ((int)ends.size() < min_num_rays) return false; return res; @@ -181,14 +189,14 @@ void Cloud::removeUnboundedRays() times[i] = times[valids[i]]; colours[i] = colours[valids[i]]; if (!passthrough.empty()) - memcpy(passthrough.data() + i * 8, passthrough.data() + valids[i] * 8, 8); + memcpy(passthrough.data() + i * extra_bytes_size, passthrough.data() + valids[i] * extra_bytes_size, extra_bytes_size); } starts.resize(valids.size()); ends.resize(valids.size()); times.resize(valids.size()); colours.resize(valids.size()); if (!passthrough.empty()) - passthrough.resize(valids.size() * 8); + passthrough.resize(valids.size() * extra_bytes_size); } void Cloud::decimate(double voxel_width, std::set &voxel_set) @@ -203,14 +211,14 @@ void Cloud::decimate(double voxel_width, std::set colours[i] = colours[id]; times[i] = times[id]; if (!passthrough.empty()) - memcpy(passthrough.data() + i * 8, passthrough.data() + id * 8, 8); + memcpy(passthrough.data() + i * extra_bytes_size, passthrough.data() + id * extra_bytes_size, extra_bytes_size); } starts.resize(subsample.size()); ends.resize(subsample.size()); colours.resize(subsample.size()); times.resize(subsample.size()); if (!passthrough.empty()) - passthrough.resize(subsample.size() * 8); + passthrough.resize(subsample.size() * extra_bytes_size); } void Cloud::eigenSolve(const std::vector &ray_ids, const Eigen::MatrixXi &indices, int index, int num_neighbours, @@ -475,6 +483,10 @@ double Cloud::estimatePointSpacing() const void Cloud::split(Cloud &cloud1, Cloud &cloud2, std::function fptr) { + cloud1.extra_bytes_size = extra_bytes_size; + cloud1.extra_bytes_vlr = extra_bytes_vlr; + cloud2.extra_bytes_size = extra_bytes_size; + cloud2.extra_bytes_vlr = extra_bytes_vlr; for (int i = 0; i < (int)ends.size(); i++) { Cloud &cloud = fptr(i) ? cloud2 : cloud1; @@ -499,10 +511,14 @@ void Cloud::addRay(const Cloud &other_cloud, size_t index) colours.push_back(other_cloud.colours[index]); if (!other_cloud.tree_ids.empty()) tree_ids.push_back(other_cloud.tree_ids[index]); - if (other_cloud.passthrough.size() >= (index + 1) * 8) + const uint16_t stride = other_cloud.extra_bytes_size; + if (other_cloud.passthrough.size() >= (index + 1) * stride) { - const uint8_t *src = other_cloud.passthrough.data() + index * 8; - passthrough.insert(passthrough.end(), src, src + 8); + const uint8_t *src = other_cloud.passthrough.data() + index * stride; + passthrough.insert(passthrough.end(), src, src + stride); + extra_bytes_size = stride; + if (extra_bytes_vlr.empty() && !other_cloud.extra_bytes_vlr.empty()) + extra_bytes_vlr = other_cloud.extra_bytes_vlr; } } @@ -584,16 +600,41 @@ bool Cloud::read(const std::string &file_name, bool convertCloud(const std::string &in_name, const std::string &out_name, std::function apply) { + const std::string ext = getFileNameExtension(in_name); + const bool is_las = (ext == "las" || ext == "laz"); + + std::vector extra_bytes_vlr; + if (is_las) + { + uint16_t orig_extra = 0; + readLasExtraBytesVlr(in_name, orig_extra, extra_bytes_vlr); + } + CloudWriter writer; - if (!writer.begin(out_name)) + if (!writer.begin(out_name, extra_bytes_vlr)) return false; + + std::vector passthrough_buf; auto applyToChunk = [&](std::vector &starts, std::vector &ends, std::vector ×, std::vector &colours) { for (size_t i = 0; i < ends.size(); i++) apply(starts[i], ends[i], times[i], colours[i]); - writer.writeChunk(starts, ends, times, colours); + std::vector chunk_pass(std::move(passthrough_buf)); + passthrough_buf.clear(); + writer.writeChunk(starts, ends, times, colours, chunk_pass); }; - if (!Cloud::read(in_name, applyToChunk)) + + bool res; + if (is_las) + { + size_t num_bounded; + res = readLas(in_name, applyToChunk, num_bounded, 1.0, nullptr, 1000000, nullptr, &passthrough_buf); + } + else + { + res = Cloud::read(in_name, applyToChunk); + } + if (!res) return false; writer.end(); return true; diff --git a/raylib/raycloud.h b/raylib/raycloud.h index fed2d4661..e4faafeaf 100644 --- a/raylib/raycloud.h +++ b/raylib/raycloud.h @@ -39,15 +39,22 @@ class RAYLIB_EXPORT Cloud std::vector times; std::vector colours; std::vector tree_ids; ///< optional per-point tree ID (populated by rayextract trees) - /// Optional flat array of LAS 1.4 extended fields, 8 bytes per point: + /// Flat array of per-point passthrough bytes, stride = extra_bytes_size. + /// The first 8 bytes per point are standard LAS 1.4 extended fields: /// [0] ext_return_number[0:3] | ext_num_returns[4:7] /// [1] classification_flags[0:3] | scanner_channel[4:5] | scan_dir[6] | edge[7] /// [2] extended_classification (0-255) /// [3] user_data /// [4..5] extended_scan_angle (int16 LE, 0.006 deg units) /// [6..7] point_source_ID (uint16 LE) + /// Bytes [8..extra_bytes_size-1] per point are the raw original LAS extra-byte attributes. /// LAS 1.2 sources are converted to this layout on read. Empty unless loaded from LAS/LAZ. std::vector passthrough; + /// Per-point stride of passthrough: 8 (standard fields) + N (original sensor extra bytes). + uint16_t extra_bytes_size = 8; + /// Raw EXTRA_BYTES VLR payload (192 bytes per original attribute) for registering attributes + /// in the output writer. Empty when there are no original extra bytes. + std::vector extra_bytes_vlr; void clear(); /// reserve the cloud's vectors diff --git a/raylib/raycloudwriter.cpp b/raylib/raycloudwriter.cpp index 8c6fcaad4..a8fa6758b 100644 --- a/raylib/raycloudwriter.cpp +++ b/raylib/raycloudwriter.cpp @@ -9,7 +9,7 @@ namespace ray { -bool CloudWriter::begin(const std::string &file_name) +bool CloudWriter::begin(const std::string &file_name, const std::vector &extra_bytes_vlr) { if (file_name.empty()) { @@ -22,7 +22,7 @@ bool CloudWriter::begin(const std::string &file_name) if (use_las_) { - las_writer_ = new LasRayCloudWriter(file_name_); + las_writer_ = new LasRayCloudWriter(file_name_, false, extra_bytes_vlr); return true; } @@ -65,13 +65,14 @@ bool CloudWriter::writeChunk(const Cloud &chunk) } bool CloudWriter::writeChunk(std::vector &starts, std::vector &ends, - std::vector ×, std::vector &colours) + std::vector ×, std::vector &colours, + const std::vector &passthrough) { if (use_las_) { if (!las_writer_) return false; - return las_writer_->writeChunk(starts, ends, times, colours); + return las_writer_->writeChunk(starts, ends, times, colours, {}, passthrough); } return writeRayCloudChunk(ofs_, buffer_, starts, ends, times, colours, has_warned_); } diff --git a/raylib/raycloudwriter.h b/raylib/raycloudwriter.h index bd0fcef3f..1f0c063ea 100644 --- a/raylib/raycloudwriter.h +++ b/raylib/raycloudwriter.h @@ -21,14 +21,16 @@ class RAYLIB_EXPORT CloudWriter ~CloudWriter() { end(); } /// Open the file to write to. Format is inferred from the extension (.las/.laz → LAS, else PLY). - bool begin(const std::string &file_name); + /// @c extra_bytes_vlr is the raw EXTRA_BYTES VLR payload (192 bytes per attribute) for original + /// sensor attributes; pass empty if there are none. + bool begin(const std::string &file_name, const std::vector &extra_bytes_vlr = {}); /// write a set of rays to the file bool writeChunk(const class Cloud &chunk); /// write a set of rays to the file, direct arguments bool writeChunk(std::vector &starts, std::vector &ends, std::vector ×, - std::vector &colours); + std::vector &colours, const std::vector &passthrough = {}); /// finish writing, flush and close the file void end(); diff --git a/raylib/raydecimation.cpp b/raylib/raydecimation.cpp index 5b839f98d..bd4f9a31a 100644 --- a/raylib/raydecimation.cpp +++ b/raylib/raydecimation.cpp @@ -8,27 +8,76 @@ #include #include #include "raycloudwriter.h" +#include "rayparse.h" +#include "raylaz.h" namespace ray { -bool decimateSpatial(const std::string &file_stub, double vox_width) +// Open writer and pre-read extra-bytes VLR from a LAS/LAZ file so original sensor attributes +// are registered in the output before any points are written. +static bool beginWriter(CloudWriter &writer, const std::string &out_file, + const std::string &in_file, const std::string &ext, + uint16_t &pass_stride_out, std::vector &extra_bytes_vlr_out) { + uint16_t orig_extra = 0; + if (ext == "las" || ext == "laz") + readLasExtraBytesVlr(in_file, orig_extra, extra_bytes_vlr_out); + pass_stride_out = static_cast(8 + orig_extra); + return writer.begin(out_file, extra_bytes_vlr_out); +} + +// Read a LAS/LAZ file with per-point passthrough, or fall back to Cloud::read for PLY. +// The callback signature matches Cloud::read. passthrough_buf is appended to before each callback, +// and cleared inside the callback after consuming the current chunk's bytes. +static bool readWithPassthrough(const std::string &file_name, const std::string &ext, + std::function &, + std::vector &, + std::vector &, + std::vector &)> apply, + std::vector *passthrough_buf) +{ + if ((ext == "las" || ext == "laz") && passthrough_buf) + { + size_t num_bounded; + return readLas(file_name, apply, num_bounded, 1.0, nullptr, 1000000, nullptr, passthrough_buf); + } + return Cloud::read(file_name, apply); +} + +bool decimateSpatial(const std::string &file_name, double vox_width) +{ + const std::string stub = getFileNameStub(file_name); + const std::string ext = getFileNameExtension(file_name); + ray::CloudWriter writer; - if (!writer.begin(file_stub + "_decimated.las")) + uint16_t pass_stride = 8; + std::vector extra_bytes_vlr; + if (!beginWriter(writer, stub + "_decimated." + ext, file_name, ext, pass_stride, extra_bytes_vlr)) return false; - // By maintaining these buffers below, we avoid almost all memory fragmentation ray::Cloud chunk; std::vector subsample; std::set voxel_set; + std::vector passthrough_buf; auto decimate = [&](std::vector &starts, std::vector &ends, - std::vector ×, std::vector &colours) + std::vector ×, std::vector &colours) { double width = 0.01 * vox_width; subsample.clear(); voxelSubsample(ends, width, subsample, voxel_set); chunk.resize(subsample.size()); + std::vector chunk_pass; + if (!passthrough_buf.empty()) + { + chunk_pass.reserve(subsample.size() * pass_stride); + for (int64_t i = 0; i < (int64_t)subsample.size(); i++) + { + const uint8_t *src = passthrough_buf.data() + subsample[i] * pass_stride; + chunk_pass.insert(chunk_pass.end(), src, src + pass_stride); + } + passthrough_buf.clear(); + } for (int64_t i = 0; i < (int64_t)subsample.size(); i++) { int64_t id = subsample[i]; @@ -37,29 +86,46 @@ bool decimateSpatial(const std::string &file_stub, double vox_width) chunk.colours[i] = colours[id]; chunk.times[i] = times[id]; } - writer.writeChunk(chunk); + writer.writeChunk(chunk.starts, chunk.ends, chunk.times, chunk.colours, chunk_pass); }; - if (!ray::Cloud::read(file_stub + ".ply", decimate)) + if (!readWithPassthrough(file_name, ext, decimate, &passthrough_buf)) return false; writer.end(); return true; } -bool decimateTemporal(const std::string &file_stub, int num_rays) +bool decimateTemporal(const std::string &file_name, int num_rays) { + const std::string stub = getFileNameStub(file_name); + const std::string ext = getFileNameExtension(file_name); + ray::CloudWriter writer; - if (!writer.begin(file_stub + "_decimated.las")) + uint16_t pass_stride = 8; + std::vector extra_bytes_vlr; + if (!beginWriter(writer, stub + "_decimated." + ext, file_name, ext, pass_stride, extra_bytes_vlr)) return false; - // By maintaining these buffers below, we avoid almost all memory fragmentation ray::Cloud chunk; + std::vector passthrough_buf; + auto decimate = [&](std::vector &starts, std::vector &ends, - std::vector ×, std::vector &colours) + std::vector ×, std::vector &colours) { size_t decimation = (size_t)num_rays; size_t count = (ends.size() + decimation - 1) / decimation; chunk.resize(count); + std::vector chunk_pass; + if (!passthrough_buf.empty()) + { + chunk_pass.reserve(count * pass_stride); + for (size_t i = 0; i < ends.size(); i += decimation) + { + const uint8_t *src = passthrough_buf.data() + i * pass_stride; + chunk_pass.insert(chunk_pass.end(), src, src + pass_stride); + } + passthrough_buf.clear(); + } for (size_t i = 0, c = 0; i < ends.size(); i += decimation, c++) { chunk.starts[c] = starts[i]; @@ -67,116 +133,129 @@ bool decimateTemporal(const std::string &file_stub, int num_rays) chunk.times[c] = times[i]; chunk.colours[c] = colours[i]; } - writer.writeChunk(chunk); + writer.writeChunk(chunk.starts, chunk.ends, chunk.times, chunk.colours, chunk_pass); }; - if (!ray::Cloud::read(file_stub + ".ply", decimate)) + if (!readWithPassthrough(file_name, ext, decimate, &passthrough_buf)) return false; writer.end(); return true; } -bool decimateSpatioTemporal(const std::string &file_stub, double vox_width, int num_rays) +bool decimateSpatioTemporal(const std::string &file_name, double vox_width, int num_rays) { + const std::string stub = getFileNameStub(file_name); + const std::string ext = getFileNameExtension(file_name); + ray::CloudWriter writer; - if (!writer.begin(file_stub + "_decimated.las")) + uint16_t pass_stride = 8; + std::vector extra_bytes_vlr; + if (!beginWriter(writer, stub + "_decimated." + ext, file_name, ext, pass_stride, extra_bytes_vlr)) return false; - // By maintaining these buffers below, we avoid almost all memory fragmentation - ray::Cloud chunk; std::map voxel_map; std::vector samples; + double voxel_width = 0.01 * vox_width; - auto decimate = [&](std::vector &, std::vector &ends, - std::vector &, std::vector &) + // Pass 1: count occupancy per voxel (no writing, no passthrough needed). + auto count_voxels = [&](std::vector &, std::vector &ends, + std::vector &, std::vector &) { - double voxel_width = 0.01 * vox_width; - // firstly we store a count per cell - for (size_t i = 0; i(); + Eigen::Vector3i coordsi = Eigen::Vector3d(std::floor(coords[0]), std::floor(coords[1]), + std::floor(coords[2])).cast(); auto found = voxel_map.find(coordsi); if (found == voxel_map.end()) { - voxel_map.insert(std::pair(coordsi, Eigen::Vector2i(1,0))); + voxel_map.insert({ coordsi, Eigen::Vector2i(1, 0) }); samples.push_back(coordsi); - } + } else { found->second[0]++; - } + } } - writer.writeChunk(chunk); }; - - if (!ray::Cloud::read(file_stub + ".ply", decimate)) + if (!ray::Cloud::read(file_name, count_voxels)) return false; - double voxel_width = 0.01 * vox_width; - for (auto &pos: samples) + for (auto &pos : samples) { int max_num = 0; - for (int x = pos[0]-1; x<=pos[0]+1; x++) - { - for (int y = pos[1]-1; y<=pos[1]+1; y++) - { - for (int z = pos[2]-1; z<=pos[2]+1; z++) + for (int x = pos[0] - 1; x <= pos[0] + 1; x++) + for (int y = pos[1] - 1; y <= pos[1] + 1; y++) + for (int z = pos[2] - 1; z <= pos[2] + 1; z++) { - auto found = voxel_map.find(Eigen::Vector3i(x,y,z)); + auto found = voxel_map.find(Eigen::Vector3i(x, y, z)); if (found != voxel_map.end()) - max_num = std::max(max_num, found->second[0]); // TODO: max of neighbours, or max 2x2 of neighbours? + max_num = std::max(max_num, found->second[0]); } - } - } - voxel_map.find(Eigen::Vector3i(pos[0],pos[1],pos[2]))->second[1] = max_num; + voxel_map.find(pos)->second[1] = max_num; } + // Pass 2: emit selected points with passthrough preserved. + std::vector passthrough_buf; auto finalise = [&](std::vector &starts, std::vector &ends, - std::vector ×, std::vector &colours) + std::vector ×, std::vector &colours) { - chunk.resize(0); - for (size_t i = 0; i out_starts, out_ends; + std::vector out_times; + std::vector out_colours; + std::vector chunk_pass; + for (size_t i = 0; i < ends.size(); i++) { Eigen::Vector3d coords = ends[i] / voxel_width; - Eigen::Vector3i coordsi = Eigen::Vector3d(std::floor(coords[0]), std::floor(coords[1]), std::floor(coords[2])).cast(); + Eigen::Vector3i coordsi = Eigen::Vector3d(std::floor(coords[0]), std::floor(coords[1]), + std::floor(coords[2])).cast(); auto found = voxel_map.find(coordsi); - if (found != voxel_map.end()) + if (found == voxel_map.end()) + continue; + int num = found->second[1]; + double segmentation = std::max(1.0, (double)num / (double)num_rays); + int &ends_left = found->second[0]; + if (std::fmod((double)ends_left + 1.0, segmentation) <= std::fmod((double)ends_left, segmentation)) { - int num = (found->second)[1]; - double segmentation = std::max(1.0, (double)num / (double)num_rays); - int &ends_left = (found->second)[0]; - if (std::fmod((double)ends_left+1.0, segmentation) <= std::fmod((double)ends_left, segmentation)) + out_starts.push_back(starts[i]); + out_ends.push_back(ends[i]); + out_colours.push_back(colours[i]); + out_times.push_back(times[i]); + if (!passthrough_buf.empty()) { - chunk.starts.push_back(starts[i]); - chunk.ends.push_back(ends[i]); - chunk.colours.push_back(colours[i]); - chunk.times.push_back(times[i]); + const uint8_t *src = passthrough_buf.data() + i * pass_stride; + chunk_pass.insert(chunk_pass.end(), src, src + pass_stride); } - ends_left--; } + ends_left--; } - writer.writeChunk(chunk); + passthrough_buf.clear(); + writer.writeChunk(out_starts, out_ends, out_times, out_colours, chunk_pass); }; - if (!ray::Cloud::read(file_stub + ".ply", finalise)) - return false; + if (!readWithPassthrough(file_name, ext, finalise, &passthrough_buf)) + return false; writer.end(); return true; } -bool decimateRaysSpatial(const std::string &file_stub, double vox_width) +bool decimateRaysSpatial(const std::string &file_name, double vox_width) { + const std::string stub = getFileNameStub(file_name); + const std::string ext = getFileNameExtension(file_name); + ray::CloudWriter writer; - if (!writer.begin(file_stub + "_decimated.las")) + uint16_t pass_stride = 8; + std::vector extra_bytes_vlr; + if (!beginWriter(writer, stub + "_decimated." + ext, file_name, ext, pass_stride, extra_bytes_vlr)) return false; - // By maintaining these buffers below, we avoid almost all memory fragmentation ray::Cloud chunk; - Subsampler subsampler; + std::vector passthrough_buf; + auto decimate = [&](std::vector &starts, std::vector &ends, - std::vector ×, std::vector &colours) + std::vector ×, std::vector &colours) { double width = 0.01 * vox_width; subsampler.subsample.clear(); @@ -188,9 +267,20 @@ bool decimateRaysSpatial(const std::string &file_stub, double vox_width) walkGrid(ends[i] / width, starts[i] / width, subsampler); #else walkGrid(starts[i] / width, ends[i] / width, subsampler); - #endif + #endif } chunk.resize(subsampler.subsample.size()); + std::vector chunk_pass; + if (!passthrough_buf.empty()) + { + chunk_pass.reserve(subsampler.subsample.size() * pass_stride); + for (int64_t i = 0; i < (int64_t)subsampler.subsample.size(); i++) + { + const uint8_t *src = passthrough_buf.data() + subsampler.subsample[i] * pass_stride; + chunk_pass.insert(chunk_pass.end(), src, src + pass_stride); + } + passthrough_buf.clear(); + } for (int64_t i = 0; i < (int64_t)subsampler.subsample.size(); i++) { int64_t id = subsampler.subsample[i]; @@ -199,105 +289,118 @@ bool decimateRaysSpatial(const std::string &file_stub, double vox_width) chunk.colours[i] = colours[id]; chunk.times[i] = times[id]; } - writer.writeChunk(chunk); + writer.writeChunk(chunk.starts, chunk.ends, chunk.times, chunk.colours, chunk_pass); }; - if (!ray::Cloud::read(file_stub + ".ply", decimate)) + if (!readWithPassthrough(file_name, ext, decimate, &passthrough_buf)) return false; writer.end(); return true; } -bool decimateAngular(const std::string &file_stub, double radius_per_length) +bool decimateAngular(const std::string &file_name, double radius_per_length) { + const std::string stub = getFileNameStub(file_name); + const std::string ext = getFileNameExtension(file_name); + ray::CloudWriter writer; - if (!writer.begin(file_stub + "_decimated.las")) + uint16_t pass_stride = 8; + std::vector extra_bytes_vlr; + if (!beginWriter(writer, stub + "_decimated." + ext, file_name, ext, pass_stride, extra_bytes_vlr)) return false; - ray::Cloud chunk; - - int min_index = -20; // about a millimetre + int min_index = -20; int max_index = 50; std::vector> voxel_sets(max_index + 1 - min_index); std::vector> visiteds(max_index + 1 - min_index); - std::vector candidate_indices; + std::vector candidate_indices; const double root2 = std::sqrt(2.0); const double logroot2 = std::log(root2); - std::vector voxel_widths(voxel_sets.size()); - for (int i = 0; i<(int)voxel_widths.size(); i++) - { - voxel_widths[i] = std::pow(root2, (double)(i+min_index)); - } + std::vector voxel_widths(voxel_sets.size()); + for (int i = 0; i < (int)voxel_widths.size(); i++) + voxel_widths[i] = std::pow(root2, (double)(i + min_index)); int index = -1; - auto decimate = [&](std::vector &starts, std::vector &ends, - std::vector &, std::vector &) + // Pass 1: identify candidate indices (no passthrough needed). + auto identify = [&](std::vector &starts, std::vector &ends, + std::vector &, std::vector &) { - for (size_t i = 0; i(); + Eigen::Vector3i coordsi = Eigen::Vector3d(std::floor(coords[0]), std::floor(coords[1]), + std::floor(coords[2])).cast(); int ind = map_index - min_index; - if (visiteds[ind].find(coordsi) != visiteds[ind].end()) // this level map has already been visited by a child (smaller ray length) + if (visiteds[ind].find(coordsi) != visiteds[ind].end()) continue; - if (voxel_sets[ind].insert(coordsi).second) { candidate_indices.push_back(index); - // now insert visiteds to suppress longer rays Eigen::Vector3i pos = coordsi; double scale = root2; - pos = Eigen::Vector3d(std::floor((double)coordsi[0]/scale), std::floor((double)coordsi[1]/scale), std::floor((double)coordsi[2]/scale)).cast(); + pos = Eigen::Vector3d(std::floor((double)coordsi[0] / scale), std::floor((double)coordsi[1] / scale), + std::floor((double)coordsi[2] / scale)).cast(); ind++; while (ind < (int)visiteds.size() && visiteds[ind].insert(pos).second) { ind++; scale *= root2; - pos = Eigen::Vector3d(std::floor((double)coordsi[0]/scale), std::floor((double)coordsi[1]/scale), std::floor((double)coordsi[2]/scale)).cast(); - } + pos = Eigen::Vector3d(std::floor((double)coordsi[0] / scale), std::floor((double)coordsi[1] / scale), + std::floor((double)coordsi[2] / scale)).cast(); + } } } - writer.writeChunk(chunk); }; - - if (!ray::Cloud::read(file_stub + ".ply", decimate)) + if (!ray::Cloud::read(file_name, identify)) return false; std::cout << "finalising" << std::endl; - for (auto &map: voxel_sets) - map.clear(); // redo + for (auto &map : voxel_sets) + map.clear(); index = -1; int head = 0; - // the finalise step uses the visiteds data to decide whether to include each ray + + // Pass 2: emit kept points with passthrough preserved. + std::vector passthrough_buf; auto finalise = [&](std::vector &starts, std::vector &ends, - std::vector ×, std::vector &colours) + std::vector ×, std::vector &colours) { - chunk.resize(0); - for (size_t i = 0; i out_starts, out_ends; + std::vector out_times; + std::vector out_colours; + std::vector chunk_pass; + for (size_t i = 0; i < ends.size(); i++) { index++; - if (index != candidate_indices[head]) + if (head >= (int)candidate_indices.size() || index != candidate_indices[head]) continue; head++; - double radius = (starts[i] - ends[i]).norm() * 0.01*radius_per_length; - int map_index = std::max(min_index, std::min((int)std::round(std::log(2.0*radius)/logroot2), max_index)); + double radius = (starts[i] - ends[i]).norm() * 0.01 * radius_per_length; + int map_index = std::max(min_index, std::min((int)std::round(std::log(2.0 * radius) / logroot2), max_index)); Eigen::Vector3d coords = ends[i] / voxel_widths[map_index - min_index]; - Eigen::Vector3i coordsi = Eigen::Vector3d(std::floor(coords[0]), std::floor(coords[1]), std::floor(coords[2])).cast(); + Eigen::Vector3i coordsi = Eigen::Vector3d(std::floor(coords[0]), std::floor(coords[1]), + std::floor(coords[2])).cast(); int ind = map_index - min_index; - if (visiteds[ind].find(coordsi) == visiteds[ind].end()) + if (visiteds[ind].find(coordsi) == visiteds[ind].end()) { - chunk.starts.push_back(starts[i]); - chunk.ends.push_back(ends[i]); - chunk.colours.push_back(colours[i]); - chunk.times.push_back(times[i]); + out_starts.push_back(starts[i]); + out_ends.push_back(ends[i]); + out_colours.push_back(colours[i]); + out_times.push_back(times[i]); + if (!passthrough_buf.empty()) + { + const uint8_t *src = passthrough_buf.data() + i * pass_stride; + chunk_pass.insert(chunk_pass.end(), src, src + pass_stride); + } } } - writer.writeChunk(chunk); + passthrough_buf.clear(); + writer.writeChunk(out_starts, out_ends, out_times, out_colours, chunk_pass); }; - if (!ray::Cloud::read(file_stub + ".ply", finalise)) + if (!readWithPassthrough(file_name, ext, finalise, &passthrough_buf)) return false; writer.end(); return true; diff --git a/raylib/raydecimation.h b/raylib/raydecimation.h index 49be9afec..5b7e86291 100644 --- a/raylib/raydecimation.h +++ b/raylib/raydecimation.h @@ -15,27 +15,27 @@ namespace ray { /// @brief subsample to 1 point per @c vox_width wide voxel in metres /// This is a spatially even subsampling, but also emphasises outlier as a side-effect -bool RAYLIB_EXPORT decimateSpatial(const std::string &file_stub, double vox_width); +bool RAYLIB_EXPORT decimateSpatial(const std::string &file_name, double vox_width); /// @brief subsample to every @c num_rays rays /// This is an unbiased subsampling, but will be over-sampled in stationary areas as a side-effect -/// Note that while this is called temporal decimation, it decimates evenly in file order, which isn't +/// Note that while this is called temporal decimation, it decimates evenly in file order, which isn't /// necessarily temporal order. Though it typically is stored that way on single scans. -bool RAYLIB_EXPORT decimateTemporal(const std::string &file_stub, int num_rays); +bool RAYLIB_EXPORT decimateTemporal(const std::string &file_name, int num_rays); /// @brief subsample to @c num_rays rays (temporally decimated) for each @c vox_width wide voxel /// This allows a more even distribution of points while maintaining details better than pure spatial decimation -bool RAYLIB_EXPORT decimateSpatioTemporal(const std::string &file_stub, double vox_width, int num_rays); +bool RAYLIB_EXPORT decimateSpatioTemporal(const std::string &file_name, double vox_width, int num_rays); /// @brief Maintains a maximum number of rays intersecting each voxel. This has some ambiguity, but is a useful routine /// as it maintains the integrity of the full ray cloud including free space, so is better for combine operations -/// By contrast, standard spatial decimation removes free space whenever the end points coincide -bool RAYLIB_EXPORT decimateRaysSpatial(const std::string &file_stub, double vox_width); +/// By contrast, standard spatial decimation removes free space whenever the end points coincide +bool RAYLIB_EXPORT decimateRaysSpatial(const std::string &file_name, double vox_width); -/// @brief decimate to no more than 1 point per voxel of width @c radius_per_length x ray length. +/// @brief decimate to no more than 1 point per voxel of width @c radius_per_length x ray length. /// This is used when error is proportional to ray length, prioritising closer measurements and leaving distant areas sparse -bool RAYLIB_EXPORT decimateAngular(const std::string &file_stub, double radius_per_length); +bool RAYLIB_EXPORT decimateAngular(const std::string &file_name, double radius_per_length); struct Subsampler diff --git a/raylib/raylaz.cpp b/raylib/raylaz.cpp index a2bf81522..491c4c157 100644 --- a/raylib/raylaz.cpp +++ b/raylib/raylaz.cpp @@ -22,7 +22,8 @@ bool readLas(const std::string &file_name, std::vector ×, std::vector &colours)> apply, size_t &num_bounded, double max_intensity, Eigen::Vector3d *offset_to_remove, size_t chunk_size, - std::vector *tree_ids_out, std::vector *passthrough_out) + std::vector *tree_ids_out, std::vector *passthrough_out, + uint16_t *orig_extra_size_out, std::vector *extra_bytes_vlr_out) { #if RAYLIB_WITH_LAS std::cout << "readLas: filename: " << file_name << std::endl; @@ -88,6 +89,54 @@ bool readLas(const std::string &file_name, } } + // LAS EXTRA_BYTES data_type → per-point byte size (types 0 and >10 are skipped) + static const uint16_t kExtraTypeSize[11] = { 0, 1, 1, 2, 2, 4, 4, 8, 8, 4, 8 }; + // Names of raycloud-owned extra attributes (these are skipped when extracting original data) + static const char *kRayCloudAttrs[] = { "sx", "sy", "sz", "alpha", "tree_id" }; + + uint16_t local_skip_size = 0; // bytes of our own extra attributes before original data + uint16_t local_orig_extra = 0; // bytes of original sensor data per point + bool has_tree_id_attr = false; // true only if VLR explicitly declares "tree_id" + std::vector local_orig_vlr; + + for (laszip_U32 v = 0; v < header->number_of_variable_length_records; v++) + { + auto &vlr = header->vlrs[v]; + if (strcmp(vlr.user_id, "LASF_Spec") != 0 || vlr.record_id != 4) + continue; + const int num_attrs = vlr.record_length_after_header / 192; + for (int a = 0; a < num_attrs; a++) + { + const uint8_t *rec = vlr.data + a * 192; + const uint8_t dtype = rec[2]; + const uint16_t attr_size = (dtype > 0 && dtype <= 10) ? kExtraTypeSize[dtype] : 0; + if (attr_size == 0) + continue; + char attr_name[33] = {}; + memcpy(attr_name, rec + 4, 32); + bool is_ours = false; + if (is_raycloud) + { + for (const char *own : kRayCloudAttrs) + if (strcmp(attr_name, own) == 0) { is_ours = true; break; } + if (strcmp(attr_name, "tree_id") == 0) + has_tree_id_attr = true; + } + if (is_ours) + local_skip_size += attr_size; + else + { + local_orig_extra += attr_size; + local_orig_vlr.insert(local_orig_vlr.end(), rec, rec + 192); + } + } + break; // only one EXTRA_BYTES VLR + } + + if (orig_extra_size_out) + *orig_extra_size_out = local_orig_extra; + if (extra_bytes_vlr_out) + *extra_bytes_vlr_out = local_orig_vlr; ray::Progress progress; ray::ProgressThread progress_thread(progress); @@ -131,7 +180,7 @@ bool readLas(const std::string &file_name, memcpy(&sy, point->extra_bytes + 4, 4); memcpy(&sz, point->extra_bytes + 8, 4); starts.push_back({ position[0] + sx, position[1] + sy, position[2] + sz }); - if (tree_ids_out && point->num_extra_bytes >= 16) + if (tree_ids_out && has_tree_id_attr) { int32_t tid; memcpy(&tid, point->extra_bytes + 12, 4); @@ -187,6 +236,13 @@ bool readLas(const std::string &file_name, passthrough_out->push_back(static_cast(static_cast(ext_angle) >> 8)); passthrough_out->push_back(static_cast(point->point_source_ID & 0xFFu)); passthrough_out->push_back(static_cast(point->point_source_ID >> 8)); + // Append original sensor extra bytes (after skipping our raycloud-owned attributes). + if (local_orig_extra > 0 && point->num_extra_bytes >= local_skip_size + local_orig_extra) + passthrough_out->insert(passthrough_out->end(), + point->extra_bytes + local_skip_size, + point->extra_bytes + local_skip_size + local_orig_extra); + else if (local_orig_extra > 0) + passthrough_out->insert(passthrough_out->end(), local_orig_extra, 0); } if (using_colour) @@ -246,14 +302,91 @@ bool readLas(const std::string &file_name, RAYLIB_UNUSED(apply); RAYLIB_UNUSED(num_bounded); RAYLIB_UNUSED(chunk_size); - RAYLIB_UNUSED(max_intensity); RAYLIB_UNUSED(tree_ids_out); RAYLIB_UNUSED(passthrough_out); + RAYLIB_UNUSED(orig_extra_size_out); + RAYLIB_UNUSED(extra_bytes_vlr_out); std::cerr << "readLas: cannot read file as WITHLAS not enabled. Enable using: cmake .. -DWITH_LAS=true" << std::endl; return false; #endif // RAYLIB_WITH_LAS } +bool readLasExtraBytesVlr(const std::string &file_name, uint16_t &orig_extra_size_out, + std::vector &extra_bytes_vlr_out) +{ +#if RAYLIB_WITH_LAS + laszip_POINTER reader; + if (laszip_create(&reader)) + return false; + + laszip_BOOL is_compressed; + if (laszip_open_reader(reader, file_name.c_str(), &is_compressed)) + { + laszip_destroy(reader); + return false; + } + + laszip_header_struct *header; + laszip_get_header_pointer(reader, &header); + + bool is_raycloud = false; + for (laszip_U32 v = 0; v < header->number_of_variable_length_records; v++) + { + if (strncmp(header->vlrs[v].user_id, "raycloudtools", 16) == 0 && header->vlrs[v].record_id == 1) + { + is_raycloud = true; + break; + } + } + + static const uint16_t kExtraTypeSize[11] = { 0, 1, 1, 2, 2, 4, 4, 8, 8, 4, 8 }; + static const char *kRayCloudAttrs[] = { "sx", "sy", "sz", "alpha", "tree_id" }; + + uint16_t local_orig_extra = 0; + std::vector local_orig_vlr; + + for (laszip_U32 v = 0; v < header->number_of_variable_length_records; v++) + { + auto &vlr = header->vlrs[v]; + if (strcmp(vlr.user_id, "LASF_Spec") != 0 || vlr.record_id != 4) + continue; + const int num_attrs = vlr.record_length_after_header / 192; + for (int a = 0; a < num_attrs; a++) + { + const uint8_t *rec = vlr.data + a * 192; + const uint8_t dtype = rec[2]; + const uint16_t attr_size = (dtype > 0 && dtype <= 10) ? kExtraTypeSize[dtype] : 0; + if (attr_size == 0) + continue; + char attr_name[33] = {}; + memcpy(attr_name, rec + 4, 32); + bool is_ours = false; + if (is_raycloud) + for (const char *own : kRayCloudAttrs) + if (strcmp(attr_name, own) == 0) { is_ours = true; break; } + if (!is_ours) + { + local_orig_extra += attr_size; + local_orig_vlr.insert(local_orig_vlr.end(), rec, rec + 192); + } + } + break; + } + + laszip_close_reader(reader); + laszip_destroy(reader); + + orig_extra_size_out = local_orig_extra; + extra_bytes_vlr_out = std::move(local_orig_vlr); + return true; +#else + RAYLIB_UNUSED(file_name); + RAYLIB_UNUSED(orig_extra_size_out); + RAYLIB_UNUSED(extra_bytes_vlr_out); + return false; +#endif +} + bool readLas(std::string file_name, std::vector &positions, std::vector ×, std::vector &colours, double max_intensity, Eigen::Vector3d *offset_to_remove) { @@ -471,10 +604,11 @@ bool LasWriter::writeChunk(const std::vector &points, const std bool RAYLIB_EXPORT writeLasRayCloud(const std::string &file_name, const std::vector &starts, const std::vector &ends, const std::vector ×, const std::vector &colours, const std::vector &tree_ids, - const std::vector &passthrough) + const std::vector &passthrough, + const std::vector &extra_bytes_vlr) { #if RAYLIB_WITH_LAS - LasRayCloudWriter writer(file_name, !tree_ids.empty()); + LasRayCloudWriter writer(file_name, !tree_ids.empty(), extra_bytes_vlr); return writer.writeChunk(starts, ends, times, colours, tree_ids, passthrough); #else // RAYLIB_WITH_LAS RAYLIB_UNUSED(file_name); @@ -488,10 +622,13 @@ bool RAYLIB_EXPORT writeLasRayCloud(const std::string &file_name, const std::vec } #if RAYLIB_WITH_LAS -LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tree_id) +LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tree_id, + const std::vector &extra_bytes_vlr) : file_name_(file_name) , points_written_(0) , with_tree_id_(with_tree_id) + , orig_extra_size_(0) + , passthrough_stride_(8) , writer_handle_(nullptr) , point_(nullptr) { @@ -507,8 +644,8 @@ LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tre header->version_major = 1; header->version_minor = 4; - header->header_size = 375; // LAS 1.4 header is 375 bytes - header->point_data_format = 7; // LAS 1.4: GPS time + RGB (equivalent of format 3) + header->header_size = 375; + header->point_data_format = 7; const double scale = 1e-4; header->x_scale_factor = scale; header->y_scale_factor = scale; @@ -517,15 +654,13 @@ LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tre header->y_offset = 0.0; header->z_offset = 0.0; - // Add three float32 extra attributes for ray start offset (start - end). - // LASzip API type 8 = F32. Scale/offset unused for floating-point types. - // Optionally add a fourth int32 attribute for the per-point tree ID. - // LASzip API type 5 = INT32. + // LASzip API type 8 = F32, type 5 = INT32, type 0 = U8. bool attr_err = laszip_add_attribute(writer_handle_, 8, "sx", "ray start x offset", 1.0, 0.0) || laszip_add_attribute(writer_handle_, 8, "sy", "ray start y offset", 1.0, 0.0) || laszip_add_attribute(writer_handle_, 8, "sz", "ray start z offset", 1.0, 0.0) || - (with_tree_id_ && laszip_add_attribute(writer_handle_, 5, "tree_id", "per-point tree ID", 1.0, 0.0)); + (with_tree_id_ && laszip_add_attribute(writer_handle_, 5, "tree_id", "per-point tree ID", 1.0, 0.0)) || + laszip_add_attribute(writer_handle_, 0, "alpha", "intensity 1-255", 1.0, 0.0); if (attr_err) { laszip_CHAR *error; @@ -536,9 +671,32 @@ LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tre return; } - // Explicitly set point type 7 (LAS 1.4 GPS time + RGB) and total record size. - // LAS 1.4 format 7 base = 36 bytes. laszip_add_attribute uses a default base, so override here. - const laszip_U16 record_size = static_cast(36 + 12 + (with_tree_id_ ? 4 : 0)); + // Register original sensor extra-byte attributes from the EXTRA_BYTES VLR payload. + // LAS spec data_type -> size: 1=u8(1), 2=i8(1), 3=u16(2), 4=i16(2), 5=u32(4), 6=i32(4), + // 7=u64(8), 8=i64(8), 9=float(4), 10=double(8) + // LASzip API type = LAS spec data_type - 1. + static const uint16_t kTypeSize[11] = { 0, 1, 1, 2, 2, 4, 4, 8, 8, 4, 8 }; + for (size_t off = 0; off + 192 <= extra_bytes_vlr.size(); off += 192) + { + const uint8_t *rec = extra_bytes_vlr.data() + off; + const uint8_t dtype = rec[2]; + if (dtype == 0 || dtype > 10) + continue; + char name[33] = {}, desc[33] = {}; + std::memcpy(name, rec + 4, 32); + std::memcpy(desc, rec + 160, 32); + const uint8_t opts = rec[3]; + double scale_v = 1.0, offset_v = 0.0; + if (opts & 0x08) std::memcpy(&scale_v, rec + 112, 8); + if (opts & 0x10) std::memcpy(&offset_v, rec + 136, 8); + laszip_add_attribute(writer_handle_, static_cast(dtype - 1), name, desc, scale_v, offset_v); + orig_extra_size_ += kTypeSize[dtype]; + } + passthrough_stride_ = static_cast(8 + orig_extra_size_); + + // LAS 1.4 format 7 base = 36 bytes. + const laszip_U16 record_size = + static_cast(36 + 12 + (with_tree_id_ ? 4 : 0) + 1 + orig_extra_size_); if (laszip_set_point_type_and_size(writer_handle_, 7, record_size)) { laszip_CHAR *error; @@ -549,7 +707,6 @@ LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tre return; } - // Custom VLR marks the file as a ray cloud so readers can detect it. if (laszip_add_vlr(writer_handle_, "raycloudtools", 1, 0, "raycloud", nullptr)) { laszip_CHAR *error; @@ -560,8 +717,8 @@ LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tre return; } - // LASzip internally computed offset_to_point_data using the LAS 1.2 default header - // size (227) when VLRs were added. Correct it for the LAS 1.4 header (375 bytes). + // LASzip uses LAS 1.2 default header size (227) when computing offset_to_point_data + // during VLR additions. Correct for the LAS 1.4 header (375 bytes). laszip_header_struct *hdr; laszip_get_header_pointer(writer_handle_, &hdr); hdr->offset_to_point_data += (375 - 227); @@ -582,12 +739,14 @@ LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tre laszip_get_point_pointer(writer_handle_, &point_); } #else // RAYLIB_WITH_LAS -LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tree_id) +LasRayCloudWriter::LasRayCloudWriter(const std::string &file_name, bool with_tree_id, + const std::vector &extra_bytes_vlr) : file_name_(file_name) , with_tree_id_(with_tree_id) { RAYLIB_UNUSED(file_name); RAYLIB_UNUSED(with_tree_id); + RAYLIB_UNUSED(extra_bytes_vlr); std::cerr << "LasRayCloudWriter: WITHLAS not enabled. Enable using: cmake .. -DWITH_LAS=true" << std::endl; } #endif // RAYLIB_WITH_LAS @@ -644,36 +803,43 @@ bool LasRayCloudWriter::writeChunk(const std::vector &starts, point_->rgb[0] = static_cast(colours[i].red) * 257u; point_->rgb[1] = static_cast(colours[i].green) * 257u; point_->rgb[2] = static_cast(colours[i].blue) * 257u; - // Restore LAS 1.4 extended fields from 8-byte passthrough if available. - // Layout mirrors readLas passthrough packing (see readLas for byte definitions). - if (passthrough.size() >= (i + 1) * 8) + // Restore LAS 1.4 extended fields from passthrough if available. + // First 8 bytes: standard LAS fields. Bytes [8..] are original sensor extra bytes. + if (passthrough.size() >= (i + 1) * passthrough_stride_) { - const uint8_t *p = passthrough.data() + i * 8; - point_->extended_return_number = p[0] & 0x0Fu; - point_->extended_number_of_returns = (p[0] >> 4) & 0x0Fu; - point_->extended_classification_flags= p[1] & 0x0Fu; - point_->extended_scanner_channel = (p[1] >> 4) & 0x03u; - point_->scan_direction_flag = (p[1] >> 6) & 0x1u; - point_->edge_of_flight_line = (p[1] >> 7) & 0x1u; - point_->extended_classification = p[2]; - point_->user_data = p[3]; + const uint8_t *p = passthrough.data() + i * passthrough_stride_; + point_->extended_return_number = p[0] & 0x0Fu; + point_->extended_number_of_returns = (p[0] >> 4) & 0x0Fu; + point_->extended_classification_flags = p[1] & 0x0Fu; + point_->extended_scanner_channel = (p[1] >> 4) & 0x03u; + point_->scan_direction_flag = (p[1] >> 6) & 0x1u; + point_->edge_of_flight_line = (p[1] >> 7) & 0x1u; + point_->extended_classification = p[2]; + point_->user_data = p[3]; int16_t ext_angle; - memcpy(&ext_angle, p + 4, 2); - point_->extended_scan_angle = ext_angle; - point_->point_source_ID = static_cast(p[6]) | (static_cast(p[7]) << 8); + std::memcpy(&ext_angle, p + 4, 2); + point_->extended_scan_angle = ext_angle; + point_->point_source_ID = static_cast(p[6]) | (static_cast(p[7]) << 8); + // Original sensor extra bytes follow the standard 8 bytes. + if (orig_extra_size_ > 0) + { + const uint16_t orig_start = static_cast(with_tree_id_ ? 17 : 13); + std::memcpy(point_->extra_bytes + orig_start, p + 8, orig_extra_size_); + } } // Store start - end as three float32 extra bytes so starts can be reconstructed. const float sx = static_cast(starts[i][0] - ends[i][0]); const float sy = static_cast(starts[i][1] - ends[i][1]); const float sz = static_cast(starts[i][2] - ends[i][2]); - memcpy(point_->extra_bytes, &sx, 4); - memcpy(point_->extra_bytes + 4, &sy, 4); - memcpy(point_->extra_bytes + 8, &sz, 4); + std::memcpy(point_->extra_bytes, &sx, 4); + std::memcpy(point_->extra_bytes + 4, &sy, 4); + std::memcpy(point_->extra_bytes + 8, &sz, 4); if (with_tree_id_) { const int32_t tid = (i < tree_ids.size()) ? tree_ids[i] : -1; - memcpy(point_->extra_bytes + 12, &tid, 4); + std::memcpy(point_->extra_bytes + 12, &tid, 4); } + point_->extra_bytes[with_tree_id_ ? 16 : 12] = colours[i].alpha; laszip_write_point(writer_handle_); } points_written_ += ends.size(); diff --git a/raylib/raylaz.h b/raylib/raylaz.h index 97bb02f81..a4027ce0a 100644 --- a/raylib/raylaz.h +++ b/raylib/raylaz.h @@ -23,15 +23,27 @@ bool RAYLIB_EXPORT readLas(std::string file_name, std::vector & /// Chunk-based version of readLas. This calls @c apply for every @c chunk_size points loaded. /// When @c tree_ids_out is non-null and the file contains a tree_id extra attribute, tree IDs are appended to it. +/// When @c passthrough_out is non-null, 8 standard LAS fields + original sensor extra bytes are appended per point. +/// @c orig_extra_size_out receives the per-point byte count of original sensor extra bytes (may be 0). +/// @c extra_bytes_vlr_out receives the raw EXTRA_BYTES VLR payload for the original sensor attributes. bool RAYLIB_EXPORT readLas(const std::string &file_name, std::function &starts, std::vector &ends, std::vector ×, std::vector &colours)> apply, size_t &num_bounded, double max_intensity, Eigen::Vector3d *offset_to_remove, size_t chunk_size = 1000000, std::vector *tree_ids_out = nullptr, - std::vector *passthrough_out = nullptr); + std::vector *passthrough_out = nullptr, + uint16_t *orig_extra_size_out = nullptr, + std::vector *extra_bytes_vlr_out = nullptr); +/// Read only the EXTRA_BYTES VLR from a las/laz file header without reading any point data. +/// On return, @c orig_extra_size_out is the total per-point byte count of non-raycloud extra attributes, +/// and @c extra_bytes_vlr_out contains the raw 192-byte VLR records for those attributes. +/// Returns false if the file cannot be opened or LAS support is not compiled in. +bool RAYLIB_EXPORT readLasExtraBytesVlr(const std::string &file_name, uint16_t &orig_extra_size_out, + std::vector &extra_bytes_vlr_out); + /// Write to a laz or las file. The intensity is the only part that is extracted from the @c colours argument. bool RAYLIB_EXPORT writeLas(std::string file_name, const std::vector &points, const std::vector ×, const std::vector &colours); @@ -39,11 +51,14 @@ bool RAYLIB_EXPORT writeLas(std::string file_name, const std::vector &starts, const std::vector &ends, const std::vector ×, const std::vector &colours, const std::vector &tree_ids = {}, - const std::vector &passthrough = {}); + const std::vector &passthrough = {}, + const std::vector &extra_bytes_vlr = {}); /// Class for chunked writing of las/laz files. class RAYLIB_EXPORT LasWriter @@ -71,10 +86,13 @@ class RAYLIB_EXPORT LasWriter /// Ray starts are stored as three float32 extra bytes (start - end offset). /// RGBA is fully preserved: RGB in LAS colour fields, alpha in intensity. /// When @c with_tree_id is true, a fourth int32 "tree_id" extra attribute is added. +/// When @c extra_bytes_vlr is non-empty, the original sensor extra-byte attributes are +/// registered from its 192-byte EXTRA_BYTES VLR records and written per point from passthrough[8+]. class RAYLIB_EXPORT LasRayCloudWriter { public: - explicit LasRayCloudWriter(const std::string &file_name, bool with_tree_id = false); + explicit LasRayCloudWriter(const std::string &file_name, bool with_tree_id = false, + const std::vector &extra_bytes_vlr = {}); ~LasRayCloudWriter(); bool writeChunk(const std::vector &starts, const std::vector &ends, const std::vector ×, const std::vector &colours, @@ -86,6 +104,8 @@ class RAYLIB_EXPORT LasRayCloudWriter std::string file_name_; uint64_t points_written_ = 0; bool with_tree_id_ = false; + uint16_t orig_extra_size_ = 0; ///< per-point original sensor extra bytes + uint16_t passthrough_stride_ = 8; ///< 8 + orig_extra_size_ #if RAYLIB_WITH_LAS laszip_POINTER writer_handle_; laszip_point_struct *point_; diff --git a/raylib/raysplitter.cpp b/raylib/raysplitter.cpp index 397e0290b..d676c50eb 100644 --- a/raylib/raysplitter.cpp +++ b/raylib/raysplitter.cpp @@ -12,6 +12,7 @@ #include "raycuboid.h" #include "extraction/raytrees.h" #include "rayparse.h" +#include "raylaz.h" namespace ray { @@ -20,35 +21,65 @@ namespace ray bool split(const std::string &file_name, const std::string &in_name, const std::string &out_name, std::function is_outside) { + const std::string ext = getFileNameExtension(file_name); + const bool is_las = (ext == "las" || ext == "laz"); + + std::vector extra_bytes_vlr; + if (is_las) + { + uint16_t orig_extra = 0; + readLasExtraBytesVlr(file_name, orig_extra, extra_bytes_vlr); + } + Cloud cloud_buffer; CloudWriter in_writer, out_writer; - if (!in_writer.begin(in_name)) + if (!in_writer.begin(in_name, extra_bytes_vlr)) return false; - if (!out_writer.begin(out_name)) + if (!out_writer.begin(out_name, extra_bytes_vlr)) return false; Cloud in_chunk, out_chunk; - /// move each ray into either the in_chunk or out_chunk, depending on the condition function is_outside - auto per_chunk = [&cloud_buffer, &in_writer, &out_writer, &in_chunk, &out_chunk, &is_outside]( - std::vector &starts, std::vector &ends, - std::vector ×, std::vector &colours) { - // I move these into the cloud buffer, so that they can be indexed easily in is_outside (by index). - cloud_buffer.starts = starts; - cloud_buffer.ends = ends; - cloud_buffer.times = times; + std::vector passthrough_buf; + + // Move each ray into either the in_chunk or out_chunk, depending on the condition is_outside. + // Passthrough (extra sensor bytes) is routed per-point via Cloud::addRay(const Cloud&, size_t). + auto per_chunk = [&](std::vector &starts, std::vector &ends, + std::vector ×, std::vector &colours) { + cloud_buffer.starts = starts; + cloud_buffer.ends = ends; + cloud_buffer.times = times; cloud_buffer.colours = colours; + if (!passthrough_buf.empty()) + { + cloud_buffer.passthrough = std::move(passthrough_buf); + passthrough_buf.clear(); + cloud_buffer.extra_bytes_size = static_cast(cloud_buffer.passthrough.size() / starts.size()); + cloud_buffer.extra_bytes_vlr = extra_bytes_vlr; + } for (int i = 0; i < (int)cloud_buffer.ends.size(); i++) { - Cloud &cloud = is_outside(cloud_buffer, i) ? out_chunk : in_chunk; - cloud.addRay(cloud_buffer.starts[i], cloud_buffer.ends[i], cloud_buffer.times[i], cloud_buffer.colours[i]); + Cloud &chunk = is_outside(cloud_buffer, i) ? out_chunk : in_chunk; + chunk.addRay(cloud_buffer, static_cast(i)); } in_writer.writeChunk(in_chunk); out_writer.writeChunk(out_chunk); in_chunk.clear(); out_chunk.clear(); + cloud_buffer.passthrough.clear(); }; - if (!Cloud::read(file_name, per_chunk)) + + bool res; + if (is_las) + { + size_t num_bounded; + res = readLas(file_name, per_chunk, num_bounded, 1.0, nullptr, 1000000, nullptr, &passthrough_buf); + } + else + { + res = Cloud::read(file_name, per_chunk); + } + if (!res) return false; in_writer.end(); out_writer.end(); @@ -59,16 +90,50 @@ bool split(const std::string &file_name, const std::string &in_name, const std:: bool splitPlane(const std::string &file_name, const std::string &in_name, const std::string &out_name, const Eigen::Vector3d &plane) { + const std::string ext = getFileNameExtension(file_name); + const bool is_las = (ext == "las" || ext == "laz"); + + std::vector extra_bytes_vlr; + if (is_las) + { + uint16_t orig_extra = 0; + readLasExtraBytesVlr(file_name, orig_extra, extra_bytes_vlr); + } + CloudWriter inside_writer, outside_writer; - if (!inside_writer.begin(in_name)) + if (!inside_writer.begin(in_name, extra_bytes_vlr)) return false; - if (!outside_writer.begin(out_name)) + if (!outside_writer.begin(out_name, extra_bytes_vlr)) return false; Cloud in_chunk, out_chunk; + Cloud cloud_buffer; + std::vector passthrough_buf; + + auto copy_pass = [&](Cloud &dst, size_t src_i) { + const uint16_t stride = cloud_buffer.extra_bytes_size; + if (stride > 0 && cloud_buffer.passthrough.size() >= (src_i + 1) * stride) + { + const uint8_t *p = cloud_buffer.passthrough.data() + src_i * stride; + dst.passthrough.insert(dst.passthrough.end(), p, p + stride); + dst.extra_bytes_size = stride; + if (dst.extra_bytes_vlr.empty()) + dst.extra_bytes_vlr = extra_bytes_vlr; + } + }; - // the split operation auto per_chunk = [&](std::vector &starts, std::vector &ends, std::vector ×, std::vector &colours) { + cloud_buffer.starts = starts; + cloud_buffer.ends = ends; + cloud_buffer.times = times; + cloud_buffer.colours = colours; + if (!passthrough_buf.empty()) + { + cloud_buffer.passthrough = std::move(passthrough_buf); + passthrough_buf.clear(); + cloud_buffer.extra_bytes_size = static_cast(cloud_buffer.passthrough.size() / starts.size()); + cloud_buffer.extra_bytes_vlr = extra_bytes_vlr; + } const Eigen::Vector3d plane_vec = plane / plane.dot(plane); for (size_t i = 0; i < ends.size(); i++) { @@ -77,7 +142,7 @@ bool splitPlane(const std::string &file_name, const std::string &in_name, const if (d1 * d2 > 0.0) // start and end are on the same side of the plane, so don't split... { Cloud &chunk = d1 > 0.0 ? out_chunk : in_chunk; - chunk.addRay(starts[i], ends[i], times[i], colours[i]); + chunk.addRay(cloud_buffer, i); } else // split the ray... { @@ -87,12 +152,16 @@ bool splitPlane(const std::string &file_name, const std::string &in_name, const if (d1 > 0.0) { out_chunk.addRay(starts[i], mid, times[i], col); + copy_pass(out_chunk, i); in_chunk.addRay(mid, ends[i], times[i], colours[i]); + copy_pass(in_chunk, i); } else { in_chunk.addRay(starts[i], mid, times[i], col); + copy_pass(in_chunk, i); out_chunk.addRay(mid, ends[i], times[i], colours[i]); + copy_pass(out_chunk, i); } } } @@ -100,10 +169,21 @@ bool splitPlane(const std::string &file_name, const std::string &in_name, const outside_writer.writeChunk(out_chunk); in_chunk.clear(); out_chunk.clear(); + cloud_buffer.passthrough.clear(); }; - if (!Cloud::read(file_name, per_chunk)) - return false; + bool res; + if (is_las) + { + size_t num_bounded; + res = readLas(file_name, per_chunk, num_bounded, 1.0, nullptr, 1000000, nullptr, &passthrough_buf); + } + else + { + res = Cloud::read(file_name, per_chunk); + } + if (!res) + return false; inside_writer.end(); outside_writer.end(); return true; @@ -113,12 +193,24 @@ bool splitPlane(const std::string &file_name, const std::string &in_name, const bool splitCapsule(const std::string &file_name, const std::string &in_name, const std::string &out_name, const Eigen::Vector3d &end1, const Eigen::Vector3d &end2, double radius) { + const std::string ext = getFileNameExtension(file_name); + const bool is_las = (ext == "las" || ext == "laz"); + + std::vector extra_bytes_vlr; + if (is_las) + { + uint16_t orig_extra = 0; + readLasExtraBytesVlr(file_name, orig_extra, extra_bytes_vlr); + } + CloudWriter inside_writer, outside_writer; - if (!inside_writer.begin(in_name)) + if (!inside_writer.begin(in_name, extra_bytes_vlr)) return false; - if (!outside_writer.begin(out_name)) + if (!outside_writer.begin(out_name, extra_bytes_vlr)) return false; Cloud in_chunk, out_chunk; + Cloud cloud_buffer; + std::vector passthrough_buf; Eigen::Vector3d dir = end2 - end1; double length = dir.norm(); @@ -127,9 +219,32 @@ bool splitCapsule(const std::string &file_name, const std::string &in_name, cons dir /= length; } + auto copy_pass = [&](Cloud &dst, size_t src_i) { + const uint16_t stride = cloud_buffer.extra_bytes_size; + if (stride > 0 && cloud_buffer.passthrough.size() >= (src_i + 1) * stride) + { + const uint8_t *p = cloud_buffer.passthrough.data() + src_i * stride; + dst.passthrough.insert(dst.passthrough.end(), p, p + stride); + dst.extra_bytes_size = stride; + if (dst.extra_bytes_vlr.empty()) + dst.extra_bytes_vlr = extra_bytes_vlr; + } + }; + // splitting per chunk - auto per_chunk = [&end1, &end2, &dir, &length, &radius, &in_chunk, &out_chunk, &inside_writer, &outside_writer](std::vector &starts, std::vector &ends, + auto per_chunk = [&](std::vector &starts, std::vector &ends, std::vector ×, std::vector &colours) { + cloud_buffer.starts = starts; + cloud_buffer.ends = ends; + cloud_buffer.times = times; + cloud_buffer.colours = colours; + if (!passthrough_buf.empty()) + { + cloud_buffer.passthrough = std::move(passthrough_buf); + passthrough_buf.clear(); + cloud_buffer.extra_bytes_size = static_cast(cloud_buffer.passthrough.size() / starts.size()); + cloud_buffer.extra_bytes_vlr = extra_bytes_vlr; + } for (size_t i = 0; i < ends.size(); i++) { Eigen::Vector3d start = starts[i]; @@ -151,6 +266,7 @@ bool splitCapsule(const std::string &file_name, const std::string &in_name, cons if (gap >= radius) { out_chunk.addRay(start, end, times[i], colours[i]); + copy_pass(out_chunk, i); continue; // if it doesn't hit the endless cylinder it won't hit the capsule } Eigen::Vector3d lateral_dir = ray - dir * ray.dot(dir); @@ -200,36 +316,52 @@ bool splitCapsule(const std::string &file_name, const std::string &in_name, cons if (closest_d >= 1.0 || farthest_d <= 0.0) { out_chunk.addRay(start, end, times[i], colours[i]); + copy_pass(out_chunk, i); continue; } // first outside ray if (closest_d > 0.0) { out_chunk.addRay(start, start + ray*closest_d, times[i], black); + copy_pass(out_chunk, i); } // second outside ray if (farthest_d < 1.0) { out_chunk.addRay(start + ray * farthest_d, end, times[i], colours[i]); + copy_pass(out_chunk, i); } // inside ray if (farthest_d < 1.0) { in_chunk.addRay(start + ray * std::max(0.0, closest_d), start + ray*std::min(farthest_d, 1.0), times[i], black); + copy_pass(in_chunk, i); } else { in_chunk.addRay(start + ray * std::max(0.0, closest_d), end, times[i], colours[i]); + copy_pass(in_chunk, i); } } inside_writer.writeChunk(in_chunk); outside_writer.writeChunk(out_chunk); in_chunk.clear(); out_chunk.clear(); + cloud_buffer.passthrough.clear(); }; - if (!Cloud::read(file_name, per_chunk)) - return false; + bool res; + if (is_las) + { + size_t num_bounded; + res = readLas(file_name, per_chunk, num_bounded, 1.0, nullptr, 1000000, nullptr, &passthrough_buf); + } + else + { + res = Cloud::read(file_name, per_chunk); + } + if (!res) + return false; inside_writer.end(); outside_writer.end(); return true; @@ -240,17 +372,51 @@ bool splitCapsule(const std::string &file_name, const std::string &in_name, cons bool splitBox(const std::string &file_name, const std::string &in_name, const std::string &out_name, const Eigen::Vector3d ¢re, const Eigen::Vector3d &extents) { + const std::string ext = getFileNameExtension(file_name); + const bool is_las = (ext == "las" || ext == "laz"); + + std::vector extra_bytes_vlr; + if (is_las) + { + uint16_t orig_extra = 0; + readLasExtraBytesVlr(file_name, orig_extra, extra_bytes_vlr); + } + CloudWriter inside_writer, outside_writer; - if (!inside_writer.begin(in_name)) + if (!inside_writer.begin(in_name, extra_bytes_vlr)) return false; - if (!outside_writer.begin(out_name)) + if (!outside_writer.begin(out_name, extra_bytes_vlr)) return false; Cloud in_chunk, out_chunk; + Cloud cloud_buffer; + std::vector passthrough_buf; + + auto copy_pass = [&](Cloud &dst, size_t src_i) { + const uint16_t stride = cloud_buffer.extra_bytes_size; + if (stride > 0 && cloud_buffer.passthrough.size() >= (src_i + 1) * stride) + { + const uint8_t *p = cloud_buffer.passthrough.data() + src_i * stride; + dst.passthrough.insert(dst.passthrough.end(), p, p + stride); + dst.extra_bytes_size = stride; + if (dst.extra_bytes_vlr.empty()) + dst.extra_bytes_vlr = extra_bytes_vlr; + } + }; // splitting per chunk auto per_chunk = [&](std::vector &starts, std::vector &ends, std::vector ×, std::vector &colours) { - // I move these into the cloud buffer, so that they can be indexed easily in fptr (by index). + cloud_buffer.starts = starts; + cloud_buffer.ends = ends; + cloud_buffer.times = times; + cloud_buffer.colours = colours; + if (!passthrough_buf.empty()) + { + cloud_buffer.passthrough = std::move(passthrough_buf); + passthrough_buf.clear(); + cloud_buffer.extra_bytes_size = static_cast(cloud_buffer.passthrough.size() / starts.size()); + cloud_buffer.extra_bytes_vlr = extra_bytes_vlr; + } const Cuboid cuboid(centre - extents, centre + extents); for (size_t i = 0; i < ends.size(); i++) { @@ -264,29 +430,44 @@ bool splitBox(const std::string &file_name, const std::string &in_name, const st col.red = col.green = col.blue = col.alpha = 0; } in_chunk.addRay(start, end, times[i], col); + copy_pass(in_chunk, i); if (start != starts[i]) // start part is clipped { col.red = col.green = col.blue = col.alpha = 0; out_chunk.addRay(starts[i], start, times[i], col); + copy_pass(out_chunk, i); } if (ends[i] != end) // end part is clipped { out_chunk.addRay(end, ends[i], times[i], colours[i]); + copy_pass(out_chunk, i); } } else // no intersection { out_chunk.addRay(starts[i], ends[i], times[i], colours[i]); + copy_pass(out_chunk, i); } } inside_writer.writeChunk(in_chunk); outside_writer.writeChunk(out_chunk); in_chunk.clear(); out_chunk.clear(); + cloud_buffer.passthrough.clear(); }; - if (!Cloud::read(file_name, per_chunk)) - return false; + bool res; + if (is_las) + { + size_t num_bounded; + res = readLas(file_name, per_chunk, num_bounded, 1.0, nullptr, 1000000, nullptr, &passthrough_buf); + } + else + { + res = Cloud::read(file_name, per_chunk); + } + if (!res) + return false; inside_writer.end(); outside_writer.end(); return true; @@ -304,6 +485,16 @@ bool splitGrid(const std::string &file_name, const std::string &cloud_name_stub, bool splitGrid(const std::string &file_name, const std::string &cloud_name_stub, const Eigen::Vector4d &cell_width, double overlap) { + const std::string ext = getFileNameExtension(file_name); + const bool is_las = (ext == "las" || ext == "laz"); + + std::vector extra_bytes_vlr; + if (is_las) + { + uint16_t orig_extra = 0; + readLasExtraBytesVlr(file_name, orig_extra, extra_bytes_vlr); + } + overlap /= 2.0; // it now means overlap relative to grid edge Cloud::Info info; Cloud::getInfo(file_name, info); @@ -347,6 +538,7 @@ bool splitGrid(const std::string &file_name, const std::string &cloud_name_stub, std::cout << "Warning: nominally more than " << max_open_files << " file pointers will be open at once." << std::endl; std::cout << "Diving the operation into " << 1+length/max_open_files << " passes" << std::endl; } + const std::string grid_ext = getFileNameExtension(file_name); for (int pass = 0; pass 0) @@ -355,13 +547,24 @@ bool splitGrid(const std::string &file_name, const std::string &cloud_name_stub, } std::vector cells(max_open_files); std::vector chunks(max_open_files); + Cloud cloud_buffer; + std::vector passthrough_buf; // splitting performed per chunk - const std::string grid_ext = getFileNameExtension(file_name); - auto per_chunk = [&min_index, &max_index, &width, min_time, &dimensions, &cells, &chunks, length, &cell_width, - &cloud_name_stub, &overlap, &pass, &max_open_files, &grid_ext](std::vector &starts, - std::vector &ends, std::vector ×, - std::vector &colours) { + auto per_chunk = [&](std::vector &starts, + std::vector &ends, std::vector ×, + std::vector &colours) { + cloud_buffer.starts = starts; + cloud_buffer.ends = ends; + cloud_buffer.times = times; + cloud_buffer.colours = colours; + if (!passthrough_buf.empty()) + { + cloud_buffer.passthrough = std::move(passthrough_buf); + passthrough_buf.clear(); + cloud_buffer.extra_bytes_size = static_cast(cloud_buffer.passthrough.size() / starts.size()); + cloud_buffer.extra_bytes_vlr = extra_bytes_vlr; + } for (size_t i = 0; i < ends.size(); i++) { // get set of cells that the ray may intersect @@ -420,13 +623,23 @@ bool splitGrid(const std::string &file_name, const std::string &cloud_name_stub, if (cell_width[3] > 0.0) name << "_" << t; name << "." << grid_ext; - cells[index].begin(name.str()); + cells[index].begin(name.str(), extra_bytes_vlr); } if (!cuboid.intersects(ends[i])) // end point is outside, so mark an unbounded ray { col.red = col.green = col.blue = col.alpha = 0; } chunks[index].addRay(start, end, times[i], col); + // copy passthrough bytes from original ray + const uint16_t stride = cloud_buffer.extra_bytes_size; + if (stride > 0 && cloud_buffer.passthrough.size() >= (i + 1) * stride) + { + const uint8_t *p = cloud_buffer.passthrough.data() + i * stride; + chunks[index].passthrough.insert(chunks[index].passthrough.end(), p, p + stride); + chunks[index].extra_bytes_size = stride; + if (chunks[index].extra_bytes_vlr.empty()) + chunks[index].extra_bytes_vlr = extra_bytes_vlr; + } } } } @@ -440,8 +653,20 @@ bool splitGrid(const std::string &file_name, const std::string &cloud_name_stub, chunks[i].clear(); } } + cloud_buffer.passthrough.clear(); }; - if (!Cloud::read(file_name, per_chunk)) + + bool res; + if (is_las) + { + size_t num_bounded; + res = readLas(file_name, per_chunk, num_bounded, 1.0, nullptr, 1000000, nullptr, &passthrough_buf); + } + else + { + res = Cloud::read(file_name, per_chunk); + } + if (!res) return false; for (int i = 0; i < max_open_files; i++) @@ -468,6 +693,16 @@ class RGBALess /// Special case for splitting based on a colour bool splitColour(const std::string &file_name, const std::string &cloud_name_stub, bool seg_colour) { + const std::string file_ext = getFileNameExtension(file_name); + const bool is_las = (file_ext == "las" || file_ext == "laz"); + + std::vector extra_bytes_vlr; + if (is_las) + { + uint16_t orig_extra = 0; + readLasExtraBytesVlr(file_name, orig_extra, extra_bytes_vlr); + } + std::map vox_map; // firstly, find out how many different colours there are int num_colours = 0; @@ -496,8 +731,9 @@ bool splitColour(const std::string &file_name, const std::string &cloud_name_stu { std::cout << "Warning: cloud has more unique colours than allowed for simultaneous files " << max_files_at_once << " so using multiple passes." << std::endl; } - + int chunk_size = std::min(num_colours, max_files_at_once); + const std::string colour_ext = getFileNameExtension(file_name); for (int batch = 0; batch < num_colours; batch+=max_files_at_once) { @@ -507,6 +743,8 @@ bool splitColour(const std::string &file_name, const std::string &cloud_name_stu } std::vector cells(chunk_size); std::vector chunks(chunk_size); + Cloud cloud_buffer; + std::vector passthrough_buf; int batch_max = std::min(num_colours, batch + max_files_at_once); if (num_colours > max_files_at_once) @@ -514,10 +752,19 @@ bool splitColour(const std::string &file_name, const std::string &cloud_name_stu std::cout << "batch processing colours " << batch << ", to " << batch_max << std::endl; } // splitting performed per chunk - const std::string colour_ext = getFileNameExtension(file_name); - auto per_chunk = [&vox_map, &batch, &max_files_at_once, &chunk_size, &cells, &chunks, &cloud_name_stub, &num_colours, seg_colour, &colour_ext]( - std::vector &starts, std::vector &ends, - std::vector ×, std::vector &colours) { + auto per_chunk = [&](std::vector &starts, std::vector &ends, + std::vector ×, std::vector &colours) { + cloud_buffer.starts = starts; + cloud_buffer.ends = ends; + cloud_buffer.times = times; + cloud_buffer.colours = colours; + if (!passthrough_buf.empty()) + { + cloud_buffer.passthrough = std::move(passthrough_buf); + passthrough_buf.clear(); + cloud_buffer.extra_bytes_size = static_cast(cloud_buffer.passthrough.size() / starts.size()); + cloud_buffer.extra_bytes_vlr = extra_bytes_vlr; + } for (size_t i = 0; i < ends.size(); i++) { RGBA colour = colours[i]; @@ -541,9 +788,9 @@ bool splitColour(const std::string &file_name, const std::string &cloud_name_stu { name << cloud_name_stub << "_" << (int)colour.red << "_" << (int)colour.green << "_" << (int)colour.blue << "." << colour_ext; } - cells[index].begin(name.str()); + cells[index].begin(name.str(), extra_bytes_vlr); } - chunks[index].addRay(starts[i], ends[i], times[i], colours[i]); + chunks[index].addRay(cloud_buffer, i); } } for (int i = 0; i < chunk_size; i++) @@ -554,8 +801,20 @@ bool splitColour(const std::string &file_name, const std::string &cloud_name_stu chunks[i].clear(); } } + cloud_buffer.passthrough.clear(); }; - if (!Cloud::read(file_name, per_chunk)) + + bool res; + if (is_las) + { + size_t num_bounded; + res = readLas(file_name, per_chunk, num_bounded, 1.0, nullptr, 1000000, nullptr, &passthrough_buf); + } + else + { + res = Cloud::read(file_name, per_chunk); + } + if (!res) return false; for (int i = 0; i < chunk_size; i++)