diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 2f96f7d..0769d3d 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -22,7 +22,7 @@ on: jobs: build: runs-on: ${{ matrix.os }} - timeout-minutes: 2 + timeout-minutes: 5 strategy: fail-fast: false matrix: diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 87ff0fa..e70e3e2 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -23,7 +23,7 @@ on: jobs: codecov: runs-on: ubuntu-latest - timeout-minutes: 3 + timeout-minutes: 5 steps: - uses: actions/checkout@v4 - name: Set reusable strings @@ -40,7 +40,7 @@ jobs: run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config Coverage -j 2 - name: Test working-directory: ${{ steps.strings.outputs.build-output-dir }} - run: ctest --build-config Coverage + run: ctest --build-config Coverage --output-on-failure - name: Coverage working-directory: ${{ steps.strings.outputs.build-output-dir }} run: ctest --build-config Coverage -T Coverage diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 97ebb3e..d1bb1a2 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -23,7 +23,7 @@ on: jobs: pre-commit: runs-on: ubuntu-latest - timeout-minutes: 2 + timeout-minutes: 3 steps: - uses: actions/checkout@v4 - name: Install pre-commit diff --git a/CMakeLists.txt b/CMakeLists.txt index 9555cd7..ec3ea31 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,9 +28,8 @@ project( set(COPYRIGHT "Copyright (c) 2024 Trailblaze Software. All rights reserved.") if(LASPP_BUILD_TESTS) - # include(FetchContent) FetchContent_Declare( laszip GIT_REPOSITORY - # https://github.com/LASzip/LASzip.git GIT_TAG 3.4.4) - # FetchContent_MakeAvailable(laszip) + include(cmake/SetupLaszip.cmake) + setup_laszip() endif() set(CMAKE_CXX_FLAGS_PROFILE "-O3 -g -pg") @@ -75,7 +74,9 @@ if(WIN32) "/wd4711" "/wd4625" "/wd5026" - "/wd5246") + "/wd5246" + "/wd4127" + "/wd4061") endif() elseif(UNIX) if(NOT CMAKE_BUILD_TYPE MATCHES "Release") @@ -135,7 +136,15 @@ if(LASPP_BUILD_TESTS) include(CTest) enable_testing() - file(GLOB_RECURSE TEST_SOURCES "test*.cpp") + file(GLOB_RECURSE UNFILTERED_TEST_SOURCES "${CMAKE_SOURCE_DIR}/*test*.cpp") + + # Filter to only those under a 'tests' directory + set(SOURCES "") + foreach(path ${UNFILTERED_TEST_SOURCES}) + if(path MATCHES "/tests/") + list(APPEND TEST_SOURCES "${path}") + endif() + endforeach() message(STATUS "Test sources: ${TEST_SOURCES}") foreach(TEST_SOURCE ${TEST_SOURCES}) @@ -146,6 +155,47 @@ if(LASPP_BUILD_TESTS) target_compile_definitions(${TEST_NAME} PRIVATE LASPP_DEBUG_ASSERTS) endforeach() + if(TARGET test_laszip_interop) + FetchContent_GetProperties(laszip) + if(NOT laszip_POPULATED) + message( + FATAL_ERROR + "laszip FetchContent properties not found. Call setup_laszip() first." + ) + endif() + # laszipper.cpp and lasunzipper.cpp are wrapper files that need internal + # laszip symbols + file(GLOB LASZIP_WRAPPER_SOURCES "${laszip_SOURCE_DIR}/src/laszipper.cpp" + "${laszip_SOURCE_DIR}/src/lasunzipper.cpp") + + # Add wrapper files as sources to the test + target_sources(test_laszip_interop PRIVATE ${LASZIP_WRAPPER_SOURCES}) + + # Link against laszip + link_target_to_laszip(test_laszip_interop) + + # Suppress warnings from laszip wrapper source files + if(MSVC) + set_source_files_properties( + ${LASZIP_WRAPPER_SOURCES} PROPERTIES COMPILE_FLAGS + "/wd4774 /wd4062 /wd4100") + else() + set_source_files_properties( + ${LASZIP_WRAPPER_SOURCES} + PROPERTIES + COMPILE_FLAGS + "-Wno-old-style-cast -Wno-shadow -Wno-sign-conversion -Wno-conversion" + ) + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set_source_files_properties( + ${LASZIP_WRAPPER_SOURCES} + PROPERTIES + COMPILE_FLAGS + "-Wno-old-style-cast -Wno-shadow -Wno-sign-conversion -Wno-conversion -Wno-useless-cast" + ) + endif() + endif() + endif() endif() include(GNUInstallDirs) diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index dc7f2ba..ba1e9b0 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -23,3 +23,20 @@ install( TARGETS ${LAS2LAS++_EXE_NAME} DESTINATION bin COMPONENT applications) + +if(LASPP_BUILD_TESTS) + # Test application for comparing LAS++ and LASzip + set(TEST_LASPP_LASZIP_EXE_NAME test_laspp_laszip) + + add_executable(${TEST_LASPP_LASZIP_EXE_NAME} test_laspp_laszip.cpp) + + target_link_libraries(${TEST_LASPP_LASZIP_EXE_NAME} + PRIVATE ${LIBRARY_NAME} OpenMP::OpenMP_CXX) + + link_target_to_laszip(${TEST_LASPP_LASZIP_EXE_NAME}) + + install( + TARGETS ${TEST_LASPP_LASZIP_EXE_NAME} + DESTINATION bin + COMPONENT applications) +endif() diff --git a/apps/test_laspp_laszip.cpp b/apps/test_laspp_laszip.cpp new file mode 100644 index 0000000..d3d2832 --- /dev/null +++ b/apps/test_laspp_laszip.cpp @@ -0,0 +1,346 @@ +/* + * SPDX-FileCopyrightText: (c) 2025 Trailblaze Software, all rights reserved + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * For LGPL2 incompatible licensing or development requests, please contact + * trailblaze.software@gmail.com + */ + +// Windows compatibility: prevent min/max macros and byte typedef conflicts +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#endif + +#include + +// Undefine Windows byte typedef if it exists to avoid conflicts with std::byte +#ifdef _WIN32 +#ifdef byte +#undef byte +#endif +#endif + +#include +#include +#include +#include +#include + +#include "las_point.hpp" +#include "las_reader.hpp" +#include "utilities/assert.hpp" + +using namespace laspp; + +namespace { + +// Helper function to convert laszip_point to LASPointFormat0 +LASPointFormat0 convert_laszip_to_format0(const laszip_point& laszip_pt) { + LASPointFormat0 pt; + pt.x = laszip_pt.X; + pt.y = laszip_pt.Y; + pt.z = laszip_pt.Z; + pt.intensity = laszip_pt.intensity; + pt.bit_byte.return_number = static_cast(laszip_pt.return_number & 0x07); + pt.bit_byte.number_of_returns = static_cast(laszip_pt.number_of_returns & 0x07); + pt.bit_byte.scan_direction_flag = static_cast(laszip_pt.scan_direction_flag); + pt.bit_byte.edge_of_flight_line = static_cast(laszip_pt.edge_of_flight_line); + pt.classification_byte.classification = + static_cast(laszip_pt.classification & 0x1F); + pt.classification_byte.synthetic = static_cast(laszip_pt.synthetic_flag); + pt.classification_byte.key_point = static_cast(laszip_pt.keypoint_flag); + pt.classification_byte.withheld = static_cast(laszip_pt.withheld_flag); + pt.scan_angle_rank = static_cast(static_cast(laszip_pt.scan_angle_rank)); + pt.user_data = laszip_pt.user_data; + pt.point_source_id = laszip_pt.point_source_ID; + return pt; +} + +// Helper function to convert laszip_point to LASPointFormat1 +LASPointFormat1 convert_laszip_to_format1(const laszip_point& laszip_pt) { + LASPointFormat1 pt; + static_cast(pt) = convert_laszip_to_format0(laszip_pt); + pt.gps_time.f64 = laszip_pt.gps_time; + return pt; +} + +// Helper function to convert laszip_point to LASPointFormat2 +LASPointFormat2 convert_laszip_to_format2(const laszip_point& laszip_pt) { + LASPointFormat2 pt; + static_cast(pt) = convert_laszip_to_format0(laszip_pt); + pt.red = laszip_pt.rgb[0]; + pt.green = laszip_pt.rgb[1]; + pt.blue = laszip_pt.rgb[2]; + return pt; +} + +// Helper function to convert laszip_point to LASPointFormat3 +LASPointFormat3 convert_laszip_to_format3(const laszip_point& laszip_pt) { + LASPointFormat3 pt; + static_cast(pt) = convert_laszip_to_format1(laszip_pt); + pt.red = laszip_pt.rgb[0]; + pt.green = laszip_pt.rgb[1]; + pt.blue = laszip_pt.rgb[2]; + return pt; +} + +// Helper function to convert laszip_point to LASPointFormat6 +LASPointFormat6 convert_laszip_to_format6(const laszip_point& laszip_pt) { + LASPointFormat6 pt; + pt.x = laszip_pt.X; + pt.y = laszip_pt.Y; + pt.z = laszip_pt.Z; + pt.intensity = laszip_pt.intensity; + pt.return_number = static_cast(laszip_pt.extended_return_number & 0x0F); + pt.number_of_returns = static_cast(laszip_pt.extended_number_of_returns & 0x0F); + pt.classification_flags = static_cast(laszip_pt.extended_classification_flags & 0x0F); + pt.scanner_channel = static_cast(laszip_pt.extended_scanner_channel & 0x03); + pt.scan_direction_flag = static_cast(laszip_pt.scan_direction_flag); + pt.edge_of_flight_line = static_cast(laszip_pt.edge_of_flight_line); + pt.classification = static_cast(laszip_pt.extended_classification); + pt.user_data = laszip_pt.user_data; + pt.scan_angle = laszip_pt.extended_scan_angle; + pt.point_source_id = laszip_pt.point_source_ID; + pt.gps_time = laszip_pt.gps_time; + return pt; +} + +// Helper function to convert laszip_point to LASPointFormat7 +LASPointFormat7 convert_laszip_to_format7(const laszip_point& laszip_pt) { + LASPointFormat7 pt; + static_cast(pt) = convert_laszip_to_format6(laszip_pt); + pt.red = laszip_pt.rgb[0]; + pt.green = laszip_pt.rgb[1]; + pt.blue = laszip_pt.rgb[2]; + return pt; +} + +// Helper function to convert laszip point to PointType +template +PointType convert_laszip_point(const laszip_point& laszip_pt) { + if constexpr (std::is_same_v) { + return convert_laszip_to_format0(laszip_pt); + } else if constexpr (std::is_same_v) { + return convert_laszip_to_format1(laszip_pt); + } else if constexpr (std::is_same_v) { + return convert_laszip_to_format2(laszip_pt); + } else if constexpr (std::is_same_v) { + return convert_laszip_to_format3(laszip_pt); + } else if constexpr (std::is_same_v) { + return convert_laszip_to_format6(laszip_pt); + } else if constexpr (std::is_same_v) { + return convert_laszip_to_format7(laszip_pt); + } else { + LASPP_FAIL("Unsupported point format"); + } +} + +// Read points with laspp +template +std::vector read_laspp_points(const std::filesystem::path& file_path, + laszip_U64& num_points) { + std::ifstream ifs(file_path, std::ios::binary); + LASPP_ASSERT(ifs.is_open(), "Failed to open file: ", file_path); + LASReader reader(ifs); + + num_points = reader.num_points(); + std::cout << "Reading " << num_points << " points from file: " << file_path << std::endl; + + std::vector laspp_points(num_points); + auto laspp_span = + reader.read_chunks(std::span(laspp_points), {0, reader.num_chunks()}); + LASPP_ASSERT_EQ(laspp_span.size(), num_points, "LAS++ read incorrect number of points"); + return laspp_points; +} + +// Open laszip reader and validate point count +template +laszip_POINTER open_laszip_reader(const std::filesystem::path& file_path, laszip_U64 num_points) { + laszip_POINTER reader_laszip; + LASPP_ASSERT_EQ(laszip_create(&reader_laszip), 0, "Failed to create laszip reader"); + + laszip_BOOL is_compressed = 0; + LASPP_ASSERT_EQ(laszip_open_reader(reader_laszip, file_path.string().c_str(), &is_compressed), 0, + "Failed to open file with laszip"); + + laszip_header* header_laszip; + LASPP_ASSERT_EQ(laszip_get_header_pointer(reader_laszip, &header_laszip), 0, + "Failed to get laszip header"); + + laszip_U64 laszip_num_points = header_laszip->extended_number_of_point_records; + if (laszip_num_points == 0) { + laszip_num_points = header_laszip->number_of_point_records; + } + LASPP_ASSERT_EQ(laszip_num_points, num_points, "Point count mismatch in headers"); + + return reader_laszip; +} + +// Read all points from laszip reader +template +std::vector read_all_laszip_points(laszip_POINTER reader_laszip, laszip_U64 num_points) { + laszip_point* laszip_pt; + LASPP_ASSERT_EQ(laszip_get_point_pointer(reader_laszip, &laszip_pt), 0, + "Failed to get laszip point pointer"); + + std::vector laszip_points; + laszip_points.reserve(num_points); + + for (laszip_U64 i = 0; i < num_points; ++i) { + LASPP_ASSERT_EQ(laszip_read_point(reader_laszip), 0, "Failed to read point ", i); + PointType converted_pt = convert_laszip_point(*laszip_pt); + laszip_points.push_back(converted_pt); + } + + return laszip_points; +} + +template +std::vector read_points_with_laszip(const std::filesystem::path& file_path, + laszip_U64 num_points) { + laszip_POINTER reader_laszip = open_laszip_reader(file_path, num_points); + + std::vector laszip_points = + read_all_laszip_points(reader_laszip, num_points); + + LASPP_ASSERT_EQ(laszip_close_reader(reader_laszip), 0, "Failed to close laszip reader"); + LASPP_ASSERT_EQ(laszip_destroy(reader_laszip), 0, "Failed to destroy laszip reader"); + + return laszip_points; +} + +template +void compare_and_report_mismatches(const std::vector& laspp_points, + const std::vector& laszip_points, + laszip_U64 num_points) { + size_t mismatch_count = 0; + for (size_t i = 0; i < num_points; ++i) { + if (laspp_points[i] != laszip_points[i]) { + if (mismatch_count == 0) { + std::cerr << "\nMismatch found at point " << i << ":" << std::endl; + std::cerr << "LAS++ point:" << std::endl; + std::cerr << laspp_points[i] << std::endl; + std::cerr << "LASzip point:" << std::endl; + std::cerr << laszip_points[i] << std::endl; + } + ++mismatch_count; + if (mismatch_count >= 10) { + std::cerr << "... (showing first 10 mismatches)" << std::endl; + break; + } + } + } + + if (mismatch_count == 0) { + std::cout << "✓ All " << num_points << " points match between LAS++ and LASzip!" << std::endl; + } else { + std::cerr << "✗ Found " << mismatch_count << " mismatched points out of " << num_points + << std::endl; + std::exit(1); + } +} + +template +void read_and_compare_points(const std::filesystem::path& file_path) { + laszip_U64 num_points = 0; + std::vector laspp_points = read_laspp_points(file_path, num_points); + std::vector laszip_points = read_points_with_laszip(file_path, num_points); + compare_and_report_mismatches(laspp_points, laszip_points, num_points); +} + +// Wrapper function to handle different point formats +void compare_file(const std::filesystem::path& file_path) { + std::ifstream ifs(file_path, std::ios::binary); + LASPP_ASSERT(ifs.is_open(), "Failed to open file: ", file_path); + LASReader reader(ifs); + + uint8_t point_format = reader.header().point_format(); + // Remove compression bit + uint8_t base_format = point_format & uint8_t(~(uint8_t(1u) << 7)); + + std::cout << "File: " << file_path << std::endl; + std::cout << "Point format: " << static_cast(base_format) << std::endl; + bool is_compressed = (point_format & (uint8_t(1u) << 7)) != 0; + std::cout << "Compressed: " << (is_compressed ? "yes" : "no") << std::endl; + std::cout << "Number of points: " << reader.num_points() << std::endl; + std::cout << std::endl; + + switch (base_format) { + case 0: + read_and_compare_points(file_path); + break; + case 1: + read_and_compare_points(file_path); + break; + case 2: + read_and_compare_points(file_path); + break; + case 3: + read_and_compare_points(file_path); + break; + case 4: + case 5: + std::cerr << "Error: Point formats 4 and 5 " + "(with wave packet data) are not yet supported" + << std::endl; + std::exit(1); + break; + case 6: + read_and_compare_points(file_path); + break; + case 7: + read_and_compare_points(file_path); + break; + case 8: + case 9: + case 10: + std::cerr << "Error: Point formats 8, 9, and 10 " + "are not yet supported" + << std::endl; + std::exit(1); + break; + default: + std::cerr << "Error: Unsupported point format: " << static_cast(base_format) + << std::endl; + std::exit(1); + } +} + +} // namespace + +int main(int argc, char* argv[]) { + if (argc != 2) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + std::cerr << " Compares data read by LAS++ and LASzip libraries" << std::endl; + return 1; + } + + std::filesystem::path file_path(argv[1]); + if (!std::filesystem::exists(file_path)) { + std::cerr << "Error: File does not exist: " << file_path << std::endl; + return 1; + } + + try { + compare_file(file_path); + std::cout << "\n✓ Test passed: All data matches!" << std::endl; + return 0; + } catch (const std::exception& e) { + std::cerr << "\n✗ Test failed with exception: " << e.what() << std::endl; + return 1; + } +} diff --git a/cmake/SetupLaszip.cmake b/cmake/SetupLaszip.cmake new file mode 100644 index 0000000..80a348f --- /dev/null +++ b/cmake/SetupLaszip.cmake @@ -0,0 +1,116 @@ +# SPDX-FileCopyrightText: (c) 2025 Trailblaze Software, all rights reserved +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; version 2.1. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# For LGPL2 incompatible licensing or development requests, please contact +# trailblaze.software@gmail.com + +# One-time setup of LASzip via FetchContent. +function(setup_laszip) + include(FetchContent) + + # Build LASzip as static so we can link it privately. + set(LASZIP_BUILD_STATIC + ON + CACHE BOOL "" FORCE) + + # Ensure we can control the MSVC runtime via CMAKE_MSVC_RUNTIME_LIBRARY. + if(POLICY CMP0091) + cmake_policy(SET CMP0091 NEW) + endif() + + # If MSVC and the parent project hasn't set the runtime yet, default to + # /MD(d). + if(MSVC AND NOT DEFINED CMAKE_MSVC_RUNTIME_LIBRARY) + set(CMAKE_MSVC_RUNTIME_LIBRARY + "MultiThreaded$<$:Debug>DLL" + CACHE STRING "" FORCE) + endif() + + # Declare & fetch LASzip. + FetchContent_Declare( + laszip + GIT_REPOSITORY https://github.com/LASzip/LASzip.git + GIT_TAG 3.4.4) + FetchContent_MakeAvailable(laszip) + + # Normalize target names with consistent, project-local aliases so consumers + # can just link against laszip::lib and laszip::api regardless of LASzip’s + # internal naming. + set(_lz_lib "") + set(_lz_api "") + + if(TARGET laszip3) + set(_lz_lib laszip3) + if(TARGET laszip_api3) + set(_lz_api laszip_api3) + endif() + elseif(TARGET laszip) + set(_lz_lib laszip) + if(TARGET laszip_api) + set(_lz_api laszip_api) + endif() + else() + message( + FATAL_ERROR "LASzip targets not found after FetchContent_MakeAvailable()") + endif() + + add_library(laszip::lib ALIAS ${_lz_lib}) + if(_lz_api) + add_library(laszip::api ALIAS ${_lz_api}) + endif() + + # Quiet warnings coming from LASzip itself. + if(NOT MSVC) + target_compile_options( + ${_lz_lib} + PRIVATE -Wno-maybe-uninitialized -Wno-format -Wno-format-security + -Wno-switch -Wno-stringop-truncation -Wno-format-overflow) + if(_lz_api) + target_compile_options( + ${_lz_api} + PRIVATE -Wno-maybe-uninitialized -Wno-format -Wno-format-security + -Wno-switch -Wno-stringop-truncation -Wno-format-overflow) + endif() + endif() +endfunction() + +# Helper: link target to LASzip (and mark includes as SYSTEM to suppress +# third-party warnings). Usage: link_target_to_laszip(my_target) +function(link_target_to_laszip target_name) + if(NOT TARGET laszip::lib) + message( + FATAL_ERROR "laszip::lib alias not found. Call setup_laszip() first.") + endif() + + target_link_libraries(${target_name} PRIVATE laszip::lib) + + if(TARGET laszip::api) + target_link_libraries(${target_name} PRIVATE laszip::api) + endif() + + # Treat LASzip headers as SYSTEM to avoid third-party warnings in your build + # output. (Interface includes propagate from LASzip targets; we just re-mark + # them as SYSTEM.) + get_target_property(_lz_includes laszip::lib INTERFACE_INCLUDE_DIRECTORIES) + if(_lz_includes) + target_include_directories(${target_name} SYSTEM PRIVATE ${_lz_includes}) + endif() + FetchContent_GetProperties(laszip) + if(NOT laszip_POPULATED) + message( + FATAL_ERROR + "laszip FetchContent properties not found. Call setup_laszip() first.") + endif() + target_include_directories( + ${target_name} SYSTEM PRIVATE ${laszip_SOURCE_DIR}/src + ${laszip_SOURCE_DIR}/dll) +endfunction() diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..d9fc3df --- /dev/null +++ b/codecov.yml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: (c) 2025 Trailblaze Software, all rights reserved +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; version 2.1. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# For LGPL2 incompatible licensing or development requests, please contact +# trailblaze.software@gmail.com + +ignore: + - "apps" diff --git a/src/las_header.hpp b/src/las_header.hpp index 5082752..358e1be 100644 --- a/src/las_header.hpp +++ b/src/las_header.hpp @@ -149,6 +149,9 @@ struct LASPP_PACKED LASHeaderPacked { double m_min_y; double m_max_z; double m_min_z; +}; + +struct LASPP_PACKED LASHeader14Packed : public LASHeaderPacked { uint64_t m_start_of_waveform_data_packet_record; uint64_t m_start_of_first_extended_variable_length_record; uint32_t m_number_of_extended_variable_length_records; @@ -157,7 +160,8 @@ struct LASPP_PACKED LASHeaderPacked { }; #pragma pack(pop) -static_assert(sizeof(LASHeaderPacked) == 375); +static_assert(sizeof(LASHeaderPacked) == 227); +static_assert(sizeof(LASHeader14Packed) == 375); class Bound3D { Vector3D m_min; @@ -207,7 +211,7 @@ class LASHeader { char m_generating_software[32] = {'\0'}; uint16_t m_file_creation_day = 0; uint16_t m_file_creation_year = 0; - uint16_t m_header_size = sizeof(LASHeaderPacked); + uint16_t m_header_size = sizeof(LASHeader14Packed); uint32_t m_offset_to_point_data = 0; uint32_t m_number_of_variable_length_records = 0; uint8_t m_point_data_record_format = 127; @@ -224,6 +228,15 @@ class LASHeader { template void apply_all_in_order(F f) { + apply_all_in_order_v12(f); + if (m_version_major == 1 && m_version_minor >= 4) { + apply_all_in_order_v14_extended(f); + } + } + + // Apply function to fields for versions 1.0-1.3 (227 bytes) + template + void apply_all_in_order_v12(F f) { f(m_file_signature); f(m_file_source_id); f(m_global_encoding); @@ -251,6 +264,12 @@ class LASHeader { f(m_bounds.min().y()); f(m_bounds.max().z()); f(m_bounds.min().z()); + // Version 1.0-1.3 stops here (227 bytes total) + } + + // Apply function to extended fields for version 1.4 (offset 227-375) + template + void apply_all_in_order_v14_extended(F f) { f(m_start_of_waveform_data_packet_record); f(m_start_of_first_extended_variable_length_record); f(m_number_of_extended_variable_length_records); @@ -260,9 +279,26 @@ class LASHeader { public: explicit LASHeader(std::istream& in_stream) { + in_stream.seekg(0); apply_all_in_order([&](auto& val) { LASPP_CHECK_READ(in_stream.read(reinterpret_cast(&val), sizeof(val))); }); + + // Validate header_size matches version + if (m_version_major == 1 && m_version_minor == 4) { + LASPP_ASSERT(m_header_size == sizeof(LASHeader14Packed), + "Version 1.4 requires header_size=375, got ", m_header_size); + } else { + LASPP_ASSERT(m_header_size == sizeof(LASHeaderPacked), + "Versions 1.0-1.3 require header_size=227, got ", m_header_size); + // Version 1.0-1.3: use legacy field for point count + m_start_of_waveform_data_packet_record = 0; + m_start_of_first_extended_variable_length_record = 0; + m_number_of_extended_variable_length_records = 0; + m_number_of_point_records = 0; + std::fill(std::begin(m_number_of_points_by_return), std::end(m_number_of_points_by_return), + 0); + } } void write(std::ostream& out_stream) const { diff --git a/src/las_point.hpp b/src/las_point.hpp index 06c2430..c4c361f 100644 --- a/src/las_point.hpp +++ b/src/las_point.hpp @@ -22,7 +22,9 @@ #include #include #include +#include #include +#include #include #include "utilities/macros.hpp" @@ -176,6 +178,20 @@ struct LASPP_PACKED ClassificationByte { } }; +inline float random_float(std::mt19937_64& gen) { + uint32_t raw = static_cast(gen()); + float normalized = + static_cast(raw) / static_cast(std::numeric_limits::max()); + return normalized * 2e6f - 1e6f; +} + +inline double random_double(std::mt19937_64& gen) { + uint64_t raw = gen(); + double normalized = + static_cast(raw) / static_cast(std::numeric_limits::max()); + return normalized * 2e6 - 1e6; +} + struct LASPP_PACKED LASPointFormat0 { int32_t x; int32_t y; @@ -187,6 +203,23 @@ struct LASPP_PACKED LASPointFormat0 { uint8_t user_data; uint16_t point_source_id; + static constexpr int PointFormat = 0; + static constexpr int MinVersion = 2; + + static LASPointFormat0 RandomData(std::mt19937_64& gen) { + LASPointFormat0 point; + point.x = static_cast(gen()); + point.y = static_cast(gen()); + point.z = static_cast(gen()); + point.intensity = static_cast(gen()); + point.bit_byte = static_cast(gen()); + point.classification_byte = static_cast(gen()); + point.scan_angle_rank = static_cast(gen()); + point.user_data = static_cast(gen()); + point.point_source_id = static_cast(gen()); + return point; + } + LASClassification classification() const { return classification_byte.classification; } friend std::ostream& operator<<(std::ostream& os, const LASPointFormat0& point) { @@ -218,6 +251,11 @@ struct LASPP_PACKED GPSTime { int64_t& as_int64() { return gps_time.int64; } + static GPSTime RandomData(std::mt19937_64& gen) { + GPSTime gpst(random_double(gen)); + return gpst; + } + explicit GPSTime(double time) { gps_time.f64 = time; } GPSTime() = default; @@ -230,8 +268,18 @@ struct LASPP_PACKED GPSTime { }; struct LASPP_PACKED LASPointFormat1 : LASPointFormat0, GPSTime { + static constexpr int PointFormat = 1; + static constexpr int MinVersion = 2; + bool operator==(const LASPointFormat1& other) const = default; + static LASPointFormat1 RandomData(std::mt19937_64& gen) { + LASPointFormat1 point; + static_cast(point) = LASPointFormat0::RandomData(gen); + static_cast(point) = GPSTime::RandomData(gen); + return point; + } + friend std::ostream& operator<<(std::ostream& os, const LASPointFormat1& lpf1) { return os << static_cast(lpf1) << static_cast(lpf1) << std::endl; @@ -245,6 +293,14 @@ struct LASPP_PACKED ColorData { bool operator==(const ColorData& other) const = default; + static ColorData RandomData(std::mt19937_64& gen) { + ColorData color; + color.red = static_cast(gen()); + color.green = static_cast(gen()); + color.blue = static_cast(gen()); + return color; + } + friend std::ostream& operator<<(std::ostream& os, const ColorData& color) { os << "RGB: (" << color.red << ", " << color.green << ", " << color.blue << ")" << std::endl; return os; @@ -252,8 +308,18 @@ struct LASPP_PACKED ColorData { }; struct LASPP_PACKED LASPointFormat2 : LASPointFormat0, ColorData { + static constexpr int PointFormat = 2; + static constexpr int MinVersion = 2; + bool operator==(const LASPointFormat2& other) const = default; + static LASPointFormat2 RandomData(std::mt19937_64& gen) { + LASPointFormat2 point; + static_cast(point) = LASPointFormat0::RandomData(gen); + static_cast(point) = ColorData::RandomData(gen); + return point; + } + friend std::ostream& operator<<(std::ostream& os, const LASPointFormat2& lpf2) { return os << static_cast(lpf2) << static_cast(lpf2) << std::endl; @@ -261,8 +327,18 @@ struct LASPP_PACKED LASPointFormat2 : LASPointFormat0, ColorData { }; struct LASPP_PACKED LASPointFormat3 : LASPointFormat1, ColorData { + static constexpr int PointFormat = 3; + static constexpr int MinVersion = 2; + bool operator==(const LASPointFormat3& other) const = default; + static LASPointFormat3 RandomData(std::mt19937_64& gen) { + LASPointFormat3 point; + static_cast(point) = LASPointFormat1::RandomData(gen); + static_cast(point) = ColorData::RandomData(gen); + return point; + } + friend std::ostream& operator<<(std::ostream& os, const LASPointFormat3& lpf3) { return os << static_cast(lpf3) << static_cast(lpf3) << std::endl; @@ -280,6 +356,18 @@ struct LASPP_PACKED WavePacketData { bool operator==(const WavePacketData& other) const = default; + static WavePacketData RandomData(std::mt19937_64& gen) { + WavePacketData wave_packet; + wave_packet.wave_packet_descriptor_index = static_cast(gen()); + wave_packet.byte_offset_to_waveform_data = gen(); + wave_packet.wave_packet_size = static_cast(gen()); + wave_packet.return_point_waveform_location = random_float(gen); + wave_packet.x_t = random_float(gen); + wave_packet.y_t = random_float(gen); + wave_packet.z_t = random_float(gen); + return wave_packet; + } + friend std::ostream& operator<<(std::ostream& os, const WavePacketData& wave_packet) { os << "Wave packet descriptor index: " << static_cast(wave_packet.wave_packet_descriptor_index) << std::endl; @@ -295,8 +383,18 @@ struct LASPP_PACKED WavePacketData { }; struct LASPP_PACKED LASPointFormat4 : LASPointFormat1, WavePacketData { + static constexpr int PointFormat = 4; + static constexpr int MinVersion = 2; + bool operator==(const LASPointFormat4& other) const = default; + static LASPointFormat4 RandomData(std::mt19937_64& gen) { + LASPointFormat4 point; + static_cast(point) = LASPointFormat1::RandomData(gen); + static_cast(point) = WavePacketData::RandomData(gen); + return point; + } + friend std::ostream& operator<<(std::ostream& os, const LASPointFormat4& lpf4) { return os << static_cast(lpf4) << static_cast(lpf4) << std::endl; @@ -304,8 +402,18 @@ struct LASPP_PACKED LASPointFormat4 : LASPointFormat1, WavePacketData { }; struct LASPP_PACKED LASPointFormat5 : LASPointFormat3, WavePacketData { + static constexpr int PointFormat = 5; + static constexpr int MinVersion = 2; + bool operator==(const LASPointFormat5& other) const = default; + static LASPointFormat5 RandomData(std::mt19937_64& gen) { + LASPointFormat5 point; + static_cast(point) = LASPointFormat3::RandomData(gen); + static_cast(point) = WavePacketData::RandomData(gen); + return point; + } + friend std::ostream& operator<<(std::ostream& os, const LASPointFormat5& lpf5) { return os << static_cast(lpf5) << static_cast(lpf5) << std::endl; @@ -325,10 +433,34 @@ struct LASPP_PACKED LASPointFormat6 { uint8_t edge_of_flight_line : 1; LASClassification classification; uint8_t user_data; - uint16_t scan_angle; + int16_t scan_angle; uint16_t point_source_id; double gps_time; + static constexpr int PointFormat = 6; + static constexpr int MinVersion = 4; + + static LASPointFormat6 RandomData(std::mt19937_64& gen) { + LASPointFormat6 point; + point.x = static_cast(gen()); + point.y = static_cast(gen()); + point.z = static_cast(gen()); + point.intensity = static_cast(gen()); + point.return_number = static_cast(gen() & 0x0F); + point.number_of_returns = static_cast(gen() & 0x0F); + point.classification_flags = static_cast(gen() & 0x0F); + point.scanner_channel = static_cast(gen() & 0x03); + point.scan_direction_flag = static_cast(gen() & 0x01); + point.edge_of_flight_line = static_cast(gen() & 0x01); + point.classification = static_cast(static_cast( + gen() % (static_cast(LASClassification::TemporalExclusion) + 1))); + point.user_data = static_cast(gen()); + point.scan_angle = static_cast(gen()); + point.point_source_id = static_cast(gen()); + point.gps_time = random_double(gen); + return point; + } + friend std::ostream& operator<<(std::ostream& os, const LASPointFormat6& point) { os << "X: " << point.x << std::endl; os << "Y: " << point.y << std::endl; @@ -344,6 +476,7 @@ struct LASPP_PACKED LASPointFormat6 { os << "User data: " << static_cast(point.user_data) << std::endl; os << "Scan angle: " << point.scan_angle << std::endl; os << "Point source ID: " << point.point_source_id << std::endl; + os << "GPS time: " << point.gps_time << std::endl; return os; } @@ -351,8 +484,18 @@ struct LASPP_PACKED LASPointFormat6 { }; struct LASPP_PACKED LASPointFormat7 : LASPointFormat6, ColorData { + static constexpr int PointFormat = 7; + static constexpr int MinVersion = 4; + bool operator==(const LASPointFormat7& other) const = default; + static LASPointFormat7 RandomData(std::mt19937_64& gen) { + LASPointFormat7 point; + static_cast(point) = LASPointFormat6::RandomData(gen); + static_cast(point) = ColorData::RandomData(gen); + return point; + } + friend std::ostream& operator<<(std::ostream& os, const LASPointFormat7& lpf7) { return os << static_cast(lpf7) << static_cast(lpf7) << std::endl; @@ -360,9 +503,17 @@ struct LASPP_PACKED LASPointFormat7 : LASPointFormat6, ColorData { }; struct LASPP_PACKED NIRData { + static constexpr int NIRBytes = 2; + bool operator==(const NIRData& other) const = default; uint16_t NIR; + static NIRData RandomData(std::mt19937_64& gen) { + NIRData nir; + nir.NIR = static_cast(gen()); + return nir; + } + friend std::ostream& operator<<(std::ostream& os, const NIRData& nir) { os << "NIR: " << nir.NIR << std::endl; return os; @@ -370,8 +521,18 @@ struct LASPP_PACKED NIRData { }; struct LASPP_PACKED LASPointFormat8 : LASPointFormat7, NIRData { + static constexpr int PointFormat = 8; + static constexpr int MinVersion = 4; + bool operator==(const LASPointFormat8& other) const = default; + static LASPointFormat8 RandomData(std::mt19937_64& gen) { + LASPointFormat8 point; + static_cast(point) = LASPointFormat7::RandomData(gen); + static_cast(point) = NIRData::RandomData(gen); + return point; + } + friend std::ostream& operator<<(std::ostream& os, const LASPointFormat8& lpf8) { return os << static_cast(lpf8) << static_cast(lpf8) << std::endl; @@ -379,8 +540,18 @@ struct LASPP_PACKED LASPointFormat8 : LASPointFormat7, NIRData { }; struct LASPP_PACKED LASPointFormat9 : LASPointFormat6, WavePacketData { + static constexpr int PointFormat = 9; + static constexpr int MinVersion = 4; + bool operator==(const LASPointFormat9& other) const = default; + static LASPointFormat9 RandomData(std::mt19937_64& gen) { + LASPointFormat9 point; + static_cast(point) = LASPointFormat6::RandomData(gen); + static_cast(point) = WavePacketData::RandomData(gen); + return point; + } + friend std::ostream& operator<<(std::ostream& os, const LASPointFormat9& lpf9) { return os << static_cast(lpf9) << static_cast(lpf9) << std::endl; @@ -388,8 +559,19 @@ struct LASPP_PACKED LASPointFormat9 : LASPointFormat6, WavePacketData { }; struct LASPP_PACKED LASPointFormat10 : LASPointFormat9, ColorData, NIRData { + static constexpr int PointFormat = 10; + static constexpr int MinVersion = 4; + bool operator==(const LASPointFormat10& other) const = default; + static LASPointFormat10 RandomData(std::mt19937_64& gen) { + LASPointFormat10 point; + static_cast(point) = LASPointFormat9::RandomData(gen); + static_cast(point) = ColorData::RandomData(gen); + static_cast(point) = NIRData::RandomData(gen); + return point; + } + friend std::ostream& operator<<(std::ostream& os, const LASPointFormat10& lpf10) { return os << static_cast(lpf10) << static_cast(lpf10) << static_cast(lpf10) << std::endl; diff --git a/src/las_reader.hpp b/src/las_reader.hpp index 908759b..b5d3f9b 100644 --- a/src/las_reader.hpp +++ b/src/las_reader.hpp @@ -261,8 +261,6 @@ class LASReader { std::vector compressed_data(total_compressed_size); m_input_stream.seekg( static_cast(header().offset_to_point_data() + compressed_start_offset)); - std::cout << "Reading " << total_compressed_size << " bytes from " << compressed_start_offset - << " to " << compressed_start_offset + total_compressed_size << std::endl; LASPP_CHECK_READ(m_input_stream.read(reinterpret_cast(compressed_data.data()), static_cast(compressed_data.size()))); diff --git a/src/las_writer.hpp b/src/las_writer.hpp index d7c97e6..c3c84d3 100644 --- a/src/las_writer.hpp +++ b/src/las_writer.hpp @@ -116,29 +116,37 @@ class LASWriter { } template - void t_write_points(const std::span& points, std::optional chunk_size) { + void t_write_points(const std::span& points, std::optional chunk_size) { LASPP_ASSERT_EQ(sizeof(PointType), m_header.point_data_record_length()); LASPP_ASSERT_LE(m_stage, WritingStage::POINTS); if (m_header.is_laz_compressed()) { if (m_stage < WritingStage::POINTS) { - LAZSpecialVLRContent laz_vlr_content(LAZCompressor::PointwiseChunked); + LAZSpecialVLRContent laz_vlr_content(std::is_base_of_v + ? LAZCompressor::LayeredChunked + : LAZCompressor::PointwiseChunked); if constexpr (std::is_base_of_v) { laz_vlr_content.add_item_record(LAZItemRecord(LAZItemType::Point10)); } + if constexpr (std::is_base_of_v) { + laz_vlr_content.add_item_record(LAZItemRecord(LAZItemType::Point14)); + } if constexpr (std::is_base_of_v) { laz_vlr_content.add_item_record(LAZItemRecord(LAZItemType::GPSTime11)); } if constexpr (std::is_base_of_v) { - laz_vlr_content.add_item_record(LAZItemRecord(LAZItemType::RGB12)); + if constexpr (std::is_base_of_v) { + laz_vlr_content.add_item_record(LAZItemRecord(LAZItemType::RGB12)); + } else if constexpr (std::is_base_of_v) { + laz_vlr_content.add_item_record(LAZItemRecord(LAZItemType::RGB14)); + } else { + static_assert(!std::is_base_of_v, + "ColorData is only supported alongside point format 0 and 6"); + } } if constexpr (std::is_base_of_v) { laz_vlr_content.add_item_record(LAZItemRecord(LAZItemType::Wavepacket13)); } - if constexpr (std::is_base_of_v) { - LASPP_FAIL("LASPointFormat6-10 is not currently supported in LAZ compression"); - } - std::stringstream laz_vlr_content_stream; laz_vlr_content.write_to(laz_vlr_content_stream); std::vector laz_vlr_content_char( @@ -193,6 +201,7 @@ class LASWriter { static_assert(is_copy_fromable()); copy_if_possible(points_to_write[i], points[i]); + copy_if_possible(points_to_write[i], points[i]); copy_if_possible(points_to_write[i], points[i]); copy_if_possible(points_to_write[i], points[i]); copy_if_possible(points_to_write[i], points[i]); @@ -267,7 +276,8 @@ class LASWriter { public: template - void write_points(const std::span& points, std::optional chunk_size = std::nullopt) { + void write_points(const std::span& points, + std::optional chunk_size = std::nullopt) { if constexpr (std::is_base_of_v || std::is_base_of_v) { t_write_points(points, chunk_size); } else { diff --git a/src/laz/chunktable.hpp b/src/laz/chunktable.hpp index 404e87e..e3d49bf 100644 --- a/src/laz/chunktable.hpp +++ b/src/laz/chunktable.hpp @@ -77,15 +77,20 @@ class LAZChunkTable : LAZChunkTableHeader { ? static_cast(total_n_points - i * constant_chunk_size.value()) : constant_chunk_size.value()); } else { - uint32_t decoded_int = static_cast(n_points_int_decoder.decode_int(decoder)); - uint32_t previous_int = i == 0 ? 0u : static_cast(m_n_points_per_chunk[i - 1]); - m_n_points_per_chunk.push_back(decoded_int + previous_int); + int32_t decoded_delta = n_points_int_decoder.decode_int(decoder); + int64_t previous_value = i == 0 ? 0LL : static_cast(m_n_points_per_chunk[i - 1]); + int64_t current_value = previous_value + decoded_delta; + LASPP_ASSERT_GE(current_value, 0); + m_n_points_per_chunk.push_back(static_cast(current_value)); } m_decompressed_chunk_offsets.push_back( i == 0 ? 0 : m_decompressed_chunk_offsets[i - 1] + m_n_points_per_chunk[i - 1]); - uint32_t decoded_int = static_cast(compressed_size_int_decoder.decode_int(decoder)); - uint32_t previous_int = i == 0 ? 0u : m_compressed_chunk_size[i - 1]; - m_compressed_chunk_size.push_back(decoded_int + previous_int); + + int32_t decoded_delta = compressed_size_int_decoder.decode_int(decoder); + int64_t previous_value = i == 0 ? 0LL : static_cast(m_compressed_chunk_size[i - 1]); + int64_t current_value = previous_value + decoded_delta; + LASPP_ASSERT_GE(current_value, 0); + m_compressed_chunk_size.push_back(static_cast(current_value)); m_compressed_chunk_offsets.push_back( i == 0 ? 8 : m_compressed_chunk_offsets[i - 1] + m_compressed_chunk_size[i - 1]); } diff --git a/src/laz/encoders.hpp b/src/laz/encoders.hpp index 669a63b..492a1cb 100644 --- a/src/laz/encoders.hpp +++ b/src/laz/encoders.hpp @@ -22,10 +22,14 @@ #include "laz/byte_encoder.hpp" #include "laz/gpstime11_encoder.hpp" #include "laz/point10_encoder.hpp" +#include "laz/point14_encoder.hpp" #include "laz/rgb12_encoder.hpp" +#include "laz/rgb14_encoder.hpp" namespace laspp { -typedef std::variant +// FIXME: Inefficient use of memory +typedef std::variant LAZEncoder; -} +} // namespace laspp diff --git a/src/laz/gpstime11_encoder.hpp b/src/laz/gpstime11_encoder.hpp index 2b2b257..b27d095 100644 --- a/src/laz/gpstime11_encoder.hpp +++ b/src/laz/gpstime11_encoder.hpp @@ -26,7 +26,8 @@ #include "utilities/assert.hpp" namespace laspp { -class GPSTime11Encoder { +template +class GeneralGPSTimeEncoder { struct ReferenceFrame { int32_t delta; int32_t counter; @@ -42,8 +43,8 @@ class GPSTime11Encoder { std::array m_reference_frames; MultiInstanceIntegerEncoder<32, 9> m_dgps_time_low_encoder; - SymbolEncoder<516> m_case_encoder; - SymbolEncoder<6> m_case_0delta_encoder; + SymbolEncoder<516 - Point14> m_case_encoder; + SymbolEncoder<6 - Point14> m_case_0delta_encoder; uint_fast8_t m_current_frame; uint_fast8_t m_next_unused_frame; @@ -53,7 +54,10 @@ class GPSTime11Encoder { return m_reference_frames[m_current_frame].prev_gps_time; } - explicit GPSTime11Encoder(GPSTime last_gps_time) : m_current_frame(0), m_next_unused_frame(0) { + static constexpr bool Point14Mode = Point14; + + explicit GeneralGPSTimeEncoder(GPSTime last_gps_time) + : m_current_frame(0), m_next_unused_frame(0) { m_reference_frames[0].prev_gps_time = last_gps_time; } @@ -72,29 +76,33 @@ class GPSTime11Encoder { return 5; } else if (case_delta == 510) { return 6; - } else if (case_delta == 512) { + } else if (case_delta == 512 - Point14) { return 8; } LASPP_FAIL("Unknown case delta: ", case_delta); } + void set_previous_value(const GPSTime& gps_time) { + m_reference_frames[m_current_frame].prev_gps_time = gps_time; + } + GPSTime decode(InStream& in_stream) { uint_fast16_t case_delta; if (m_reference_frames[m_current_frame].delta == 0) { - case_delta = m_case_0delta_encoder.decode_symbol(in_stream); + case_delta = m_case_0delta_encoder.decode_symbol(in_stream) + Point14; if (case_delta >= 3) { m_current_frame = (m_current_frame + case_delta - 2) % 4; return decode(in_stream); } - if (case_delta == 0) { + if (!Point14 && case_delta == 0) { case_delta = 511; } else if (case_delta == 2) { - case_delta = 512; + case_delta = 512 - Point14; } } else { case_delta = m_case_encoder.decode_symbol(in_stream); - if (case_delta >= 513) { - m_current_frame = (m_current_frame + case_delta - 512) % 4; + if (case_delta >= 513 - Point14) { + m_current_frame = (m_current_frame + case_delta - (512 - Point14)) % 4; return decode(in_stream); } } @@ -143,29 +151,32 @@ class GPSTime11Encoder { } return m_reference_frames[m_current_frame].prev_gps_time; } else if (case_delta <= 510) { - int32_t dgps_time_low = - m_dgps_time_low_encoder.decode_int(case_delta == 510 ? 6 : 5, in_stream); - m_reference_frames[m_current_frame].prev_gps_time.as_int64() += - -static_cast(case_delta - 500) * m_reference_frames[m_current_frame].delta + - dgps_time_low; + uint32_t dgps_time_low = static_cast( + m_dgps_time_low_encoder.decode_int(case_delta == 510 ? 6 : 5, in_stream)); + m_reference_frames[m_current_frame].prev_gps_time.as_int64() -= static_cast( + static_cast(case_delta - 500) * + static_cast(m_reference_frames[m_current_frame].delta) - + dgps_time_low); if (case_delta == 510) { m_reference_frames[m_current_frame].counter++; if (m_reference_frames[m_current_frame].counter > 3) { - m_reference_frames[m_current_frame].delta = - -10 * m_reference_frames[m_current_frame].delta + dgps_time_low; + m_reference_frames[m_current_frame].delta = static_cast( + static_cast(-10 * m_reference_frames[m_current_frame].delta) + + dgps_time_low); m_reference_frames[m_current_frame].counter = 0; } } return m_reference_frames[m_current_frame].prev_gps_time; } - } else if (case_delta == 511) { + } else if (!Point14 && case_delta == 511) { return m_reference_frames[m_current_frame].prev_gps_time; } - LASPP_ASSERT_EQ(case_delta, 512, "The final one"); - int32_t dgps_time_low = m_dgps_time_low_encoder.decode_int(8, in_stream); + LASPP_ASSERT_EQ(case_delta, 512 - Point14, "The final one"); + uint32_t dgps_time_low = + static_cast(m_dgps_time_low_encoder.decode_int(8, in_stream)); uint32_t dgps_time = static_cast(raw_decode(in_stream, 32)); uint64_t tmp = (static_cast( - static_cast( + static_cast( m_reference_frames[m_current_frame].prev_gps_time.as_uint64() >> 32) + dgps_time_low) << 32) + @@ -181,32 +192,34 @@ class GPSTime11Encoder { void encode(OutStream& out_stream, GPSTime gps_time) { ReferenceFrame& rf = m_reference_frames[m_current_frame]; if (rf.delta == 0) { - if (rf.prev_gps_time.as_uint64() == gps_time.as_uint64()) { + if (!Point14 && rf.prev_gps_time.as_uint64() == gps_time.as_uint64()) { m_case_0delta_encoder.encode_symbol(out_stream, 0); return; } - int64_t diff = gps_time.as_int64() - rf.prev_gps_time.as_int64(); + int64_t diff = static_cast(gps_time.as_uint64() - rf.prev_gps_time.as_uint64()); if (diff == static_cast(diff)) { int32_t diff_32 = static_cast(diff); - m_case_0delta_encoder.encode_symbol(out_stream, 1); + m_case_0delta_encoder.encode_symbol(out_stream, 1 - Point14); m_dgps_time_low_encoder.encode_int(0, out_stream, diff_32); rf.delta = diff_32; rf.counter = 0; } else { for (size_t i = 0; i < 4; i++) { - int64_t rf_diff = gps_time.as_int64() - m_reference_frames[i].prev_gps_time.as_int64(); + int64_t rf_diff = static_cast(gps_time.as_uint64() - + m_reference_frames[i].prev_gps_time.as_uint64()); if (static_cast(rf_diff) == rf_diff) { - m_case_0delta_encoder.encode_symbol(out_stream, 2 + (4 + i - m_current_frame) % 4); + m_case_0delta_encoder.encode_symbol(out_stream, + 2 - Point14 + (4 + i - m_current_frame) % 4); m_current_frame = static_cast(i); return encode(out_stream, gps_time); } } - m_case_0delta_encoder.encode_symbol(out_stream, 2); + m_case_0delta_encoder.encode_symbol(out_stream, 2 - Point14); m_dgps_time_low_encoder.encode_int( 8, out_stream, - static_cast(gps_time.as_uint64() >> 32) - - static_cast(rf.prev_gps_time.as_uint64() >> 32)); + static_cast(static_cast(gps_time.as_uint64() >> 32) - + static_cast(rf.prev_gps_time.as_uint64() >> 32))); raw_encode(out_stream, static_cast(gps_time.as_uint64()), 32); m_next_unused_frame = (m_next_unused_frame + 1u) % 4; m_current_frame = m_next_unused_frame; @@ -215,7 +228,7 @@ class GPSTime11Encoder { } m_reference_frames[m_current_frame].prev_gps_time = gps_time; } else { - if (rf.prev_gps_time.as_uint64() == gps_time.as_uint64()) { + if (!Point14 && rf.prev_gps_time.as_uint64() == gps_time.as_uint64()) { m_case_encoder.encode_symbol(out_stream, 511); return; } @@ -279,13 +292,13 @@ class GPSTime11Encoder { for (size_t i = 0; i < 4; i++) { int64_t rf_diff = gps_time.as_int64() - m_reference_frames[i].prev_gps_time.as_int64(); if (rf_diff == static_cast(rf_diff)) { - m_case_encoder.encode_symbol(out_stream, 512 + (4 + i - m_current_frame) % 4); + m_case_encoder.encode_symbol(out_stream, 512 - Point14 + (4 + i - m_current_frame) % 4); m_current_frame = static_cast(i); return encode(out_stream, gps_time); } } - m_case_encoder.encode_symbol(out_stream, 512); + m_case_encoder.encode_symbol(out_stream, 512 - Point14); m_dgps_time_low_encoder.encode_int( 8, out_stream, static_cast(gps_time.as_uint64() >> 32) - @@ -301,4 +314,6 @@ class GPSTime11Encoder { } }; +using GPSTime11Encoder = GeneralGPSTimeEncoder<>; + } // namespace laspp diff --git a/src/laz/layered_stream.hpp b/src/laz/layered_stream.hpp new file mode 100644 index 0000000..cbb4968 --- /dev/null +++ b/src/laz/layered_stream.hpp @@ -0,0 +1,125 @@ +/* + * SPDX-FileCopyrightText: (c) 2025 Trailblaze Software, all rights reserved + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * For LGPL2 incompatible licensing or development requests, please contact + * trailblaze.software@gmail.com + */ + +#pragma once + +#include + +#include "stream.hpp" + +namespace laspp { + +template +class AlwaysConstructedArray { + using Storage = std::aligned_storage_t; + std::array storage_{}; + + static T* ptr(Storage& s) noexcept { return std::launder(reinterpret_cast(&s)); } + static const T* ptr(const Storage& s) noexcept { + return std::launder(reinterpret_cast(&s)); + } + + public: + AlwaysConstructedArray() = default; + AlwaysConstructedArray(const AlwaysConstructedArray&) = delete; + AlwaysConstructedArray& operator=(const AlwaysConstructedArray&) = delete; + + ~AlwaysConstructedArray() { + for (std::size_t i = 0; i < N; ++i) std::destroy_at(ptr(storage_[i])); + } + + template + T& construct(std::size_t i, Args&&... args) { + return *std::construct_at(ptr(storage_[i]), std::forward(args)...); + } + + T& operator[](std::size_t i) noexcept { return *ptr(storage_[i]); } + const T& operator[](std::size_t i) const noexcept { return *ptr(storage_[i]); } +}; + +template +class LayeredInStreams { + AlwaysConstructedArray m_layer_stream_buffers; + AlwaysConstructedArray m_layer_streams; + AlwaysConstructedArray m_streams; + std::array m_non_empty{}; + + public: + LayeredInStreams(std::span& layer_sizes, std::span& compressed_layer_data) { + for (std::size_t i = 0; i < N_STREAMS; ++i) { + std::uint32_t layer_size = 0; + std::memcpy(&layer_size, layer_sizes.data(), sizeof(layer_size)); + m_non_empty[i] = layer_size > 0; + layer_sizes = layer_sizes.subspan(sizeof(layer_size)); + m_layer_stream_buffers.construct(i, compressed_layer_data.data(), layer_size); + m_layer_streams.construct(i, &m_layer_stream_buffers[i]); + m_streams.construct(i, m_layer_streams[i]); + compressed_layer_data = compressed_layer_data.subspan(layer_size); + } + } + + bool non_empty(std::size_t i) const noexcept { return m_non_empty[i]; } + + InStream& operator[](std::size_t i) noexcept { return m_streams[i]; } +}; + +template +class LayeredOutStreams { + std::array m_layer_stringstreams; + AlwaysConstructedArray m_streams; + + public: + LayeredOutStreams() { + for (size_t i = 0; i < N_STREAMS; i++) { + m_streams.construct(i, m_layer_stringstreams[i]); + } + } + + OutStream& operator[](size_t i) noexcept { return m_streams[i]; } + + std::array layer_sizes() { + std::array sizes; + for (size_t i = 0; i < N_STREAMS; i++) { + m_streams[i].finalize(); + sizes[i] = static_cast(m_layer_stringstreams[i].str().size()); + } + return sizes; + } + + std::stringstream cb() { + std::stringstream combined; + for (size_t i = 0; i < N_STREAMS; i++) { + combined << std::move(m_layer_stringstreams[i]).rdbuf(); + } + return combined; + } + + std::stringstream combined_stream() { + std::stringstream combined; + auto sizes = layer_sizes(); + for (size_t i = 0; i < N_STREAMS; i++) { + uint32_t layer_size = sizes[i]; + combined.write(reinterpret_cast(&layer_size), sizeof(layer_size)); + } + for (size_t i = 0; i < N_STREAMS; i++) { + combined << std::move(m_layer_stringstreams[i]).rdbuf(); + } + return combined; + } +}; + +} // namespace laspp diff --git a/src/laz/laz_reader.hpp b/src/laz/laz_reader.hpp index e0df107..15e1f1d 100644 --- a/src/laz/laz_reader.hpp +++ b/src/laz/laz_reader.hpp @@ -20,21 +20,50 @@ #include #include #include +#include #include +#include #include "chunktable.hpp" #include "las_point.hpp" #include "laz/byte_encoder.hpp" #include "laz/encoders.hpp" #include "laz/gpstime11_encoder.hpp" +#include "laz/layered_stream.hpp" #include "laz/point10_encoder.hpp" +#include "laz/point14_encoder.hpp" #include "laz/rgb12_encoder.hpp" +#include "laz/rgb14_encoder.hpp" #include "laz/stream.hpp" #include "laz_vlr.hpp" #include "utilities/assert.hpp" namespace laspp { +template +struct has_num_layers : std::false_type {}; + +template +struct has_num_layers< + T, std::void_t>::NUM_LAYERS)>> + : std::true_type {}; + +template +constexpr bool has_num_layers_v = has_num_layers::value; + +template +void copy_from_if_possible(TDest& dest, const TSrc& src) { + if constexpr (is_copy_fromable()) { + copy_from(dest, src); + } else if constexpr (is_copy_assignable()) { + dest = src; + } else if constexpr (std::is_base_of_v>, + TDest>) { + using DecompressedType = std::remove_const_t>; + static_cast(dest) = src; + } +} + class LAZReader { LAZSpecialVLRContent m_special_vlr; std::optional m_chunk_table; @@ -67,75 +96,146 @@ class LAZReader { template std::span decompress_chunk(std::span compressed_data, std::span decompressed_data) { - LASPP_ASSERT(m_chunk_table.has_value()); - std::vector encoders; - for (LAZItemRecord record : m_special_vlr.items_records) { - switch (record.item_type) { - case LAZItemType::Point10: { - encoders.push_back( - LASPointFormat0Encoder(*reinterpret_cast(compressed_data.data()))); - compressed_data = compressed_data.subspan(sizeof(LASPointFormat0)); - break; - } - // case LAZItemType::Point14: { - // encoders.push_back( - // LASPointFormat6Encoder(*reinterpret_cast(compressed_data.data()))); - // compressed_data = compressed_data.subspan(sizeof(LASPointFormat6)); - //} - case LAZItemType::GPSTime11: { - encoders.push_back(GPSTime11Encoder(*reinterpret_cast(compressed_data.data()))); - compressed_data = compressed_data.subspan(sizeof(GPSTime)); - break; - } - case LAZItemType::RGB12: { - encoders.push_back(RGB12Encoder(*reinterpret_cast(compressed_data.data()))); - compressed_data = compressed_data.subspan(sizeof(ColorData)); - break; + { + std::optional context; + for (LAZItemRecord record : m_special_vlr.items_records) { + switch (record.item_type) { + case LAZItemType::Point14: { + encoders.emplace_back(LASPointFormat6Encoder( + *reinterpret_cast(compressed_data.data()))); + context = std::get(encoders.back()).get_active_context(); + compressed_data = compressed_data.subspan(sizeof(LASPointFormat6)); + break; + } + case LAZItemType::RGB14: { + encoders.emplace_back(RGB14Encoder( + *reinterpret_cast(compressed_data.data()), context.value())); + compressed_data = compressed_data.subspan(sizeof(ColorData)); + break; + } + case LAZItemType::Point10: { + encoders.emplace_back(LASPointFormat0Encoder( + *reinterpret_cast(compressed_data.data()))); + compressed_data = compressed_data.subspan(sizeof(LASPointFormat0)); + break; + } + case LAZItemType::GPSTime11: { + encoders.emplace_back( + GPSTime11Encoder(*reinterpret_cast(compressed_data.data()))); + compressed_data = compressed_data.subspan(sizeof(GPSTime)); + break; + } + case LAZItemType::RGB12: { + encoders.emplace_back( + RGB12Encoder(*reinterpret_cast(compressed_data.data()))); + compressed_data = compressed_data.subspan(sizeof(ColorData)); + break; + } + case LAZItemType::Byte: { + std::vector last_bytes(record.item_size); + std::copy_n(compressed_data.data(), record.item_size, last_bytes.begin()); + encoders.emplace_back(BytesEncoder(last_bytes)); + compressed_data = compressed_data.subspan(record.item_size); + break; + } + default: + LASPP_FAIL("Currently unsupported LAZ item type: ", LAZItemType(record.item_type), " (", + static_cast(record.item_type), ")"); } - case LAZItemType::Byte: { - std::vector last_bytes(record.item_size); - std::copy_n(compressed_data.data(), record.item_size, last_bytes.begin()); - encoders.push_back(BytesEncoder(last_bytes)); - compressed_data = compressed_data.subspan(record.item_size); - break; - } - default: - LASPP_FAIL("Currently unsupported LAZ item type: ", LAZItemType(record.item_type), " (", - static_cast(record.item_type), ")"); } } if (m_special_vlr.compressor == LAZCompressor::LayeredChunked) { - uint32_t num_points; - std::copy(compressed_data.begin(), compressed_data.begin() + sizeof(uint32_t), - reinterpret_cast(&num_points)); - compressed_data = compressed_data.subspan(sizeof(uint32_t)); - LASPP_ASSERT_EQ(num_points, decompressed_data.size()); - } + { + uint32_t num_points; + std::memcpy(&num_points, compressed_data.data(), sizeof(num_points)); + compressed_data = compressed_data.subspan(sizeof(uint32_t)); + LASPP_ASSERT_EQ(num_points, decompressed_data.size()); + } - PointerStreamBuffer compressed_buffer(compressed_data.data(), compressed_data.size()); - std::istream compressed_stream(&compressed_buffer); - InStream compressed_in_stream(compressed_stream); - for (size_t i = 0; i < decompressed_data.size(); i++) { - for (LAZEncoder& laz_encoder : encoders) { + static_assert(has_num_layers::value, + "LAZPointFormat6Encoder must have NUM_LAYERS"); + size_t total_n_layers = 0; + for (const LAZEncoder& encoder : encoders) { std::visit( - [&compressed_in_stream, &decompressed_data, &i](auto&& encoder) { - if (i > 0) encoder.decode(compressed_in_stream); - - if constexpr (is_copy_fromable()) { - copy_from(decompressed_data[i], encoder.last_value()); - } else if constexpr (is_copy_assignable()) { - decompressed_data[i] = encoder.last_value(); - } else if constexpr (std::is_base_of_v< - std::remove_reference_t, - T>) { - using DecompressedType = - std::remove_const_t>; - static_cast(decompressed_data[i]) = encoder.last_value(); + [&total_n_layers](auto&& enc) { + if constexpr (has_num_layers_v) { + total_n_layers += std::remove_reference_t::NUM_LAYERS; + } else { + LASPP_FAIL("Cannot use layered decompression with non-layered encoder."); } }, - laz_encoder); + encoder); + } + std::span compressed_layer_data = + compressed_data.subspan(total_n_layers * sizeof(uint32_t)); + + std::vector< + std::variant>, std::unique_ptr>>> + layered_in_streams_for_encoders; + for (const LAZEncoder& encoder : encoders) { + std::visit( + [&compressed_data, &compressed_layer_data, + &layered_in_streams_for_encoders](auto&& enc) { + if constexpr (has_num_layers_v) { + layered_in_streams_for_encoders.emplace_back( + std::make_unique< + LayeredInStreams::NUM_LAYERS>>( + compressed_data, compressed_layer_data)); + } else { + LASPP_FAIL("Cannot use layered decompression with non-layered encoder."); + } + }, + encoder); + } + LASPP_ASSERT_EQ(compressed_layer_data.size(), 0); + for (size_t i = 0; i < decompressed_data.size(); i++) { + std::optional context; + for (size_t encoder_idx = 0; encoder_idx < encoders.size(); encoder_idx++) { + LAZEncoder& laz_encoder = encoders[encoder_idx]; + std::visit( + [&decompressed_data, &i, &layered_in_streams_for_encoders, &encoder_idx, + &context](auto&& encoder) { + if constexpr (has_num_layers_v) { + LayeredInStreams::NUM_LAYERS>& + layered_in_stream = *std::get::NUM_LAYERS>>>( + layered_in_streams_for_encoders[encoder_idx]); + if (i > 0) { + if constexpr (std::is_same_v, + LASPointFormat6Encoder>) { + encoder.decode(layered_in_stream); + context = encoder.get_active_context(); + } else { + encoder.decode(layered_in_stream, context.value()); + } + } + copy_from_if_possible(decompressed_data[i], encoder.last_value()); + } else { + LASPP_FAIL("Cannot use layered decompression with non-layered encoder."); + } + }, + laz_encoder); + } + } + } else { + PointerStreamBuffer compressed_buffer(compressed_data.data(), compressed_data.size()); + std::istream compressed_stream(&compressed_buffer); + InStream compressed_in_stream(compressed_stream); + for (size_t i = 0; i < decompressed_data.size(); i++) { + for (LAZEncoder& laz_encoder : encoders) { + std::visit( + [&compressed_in_stream, &decompressed_data, &i](auto&& encoder) { + if constexpr (has_num_layers_v) { + LASPP_FAIL("Cannot use layered encoder with non-layered compression."); + } else { + if (i > 0) encoder.decode(compressed_in_stream); + copy_from_if_possible(decompressed_data[i], encoder.last_value()); + } + }, + laz_encoder); + } } } return decompressed_data; diff --git a/src/laz/laz_writer.hpp b/src/laz/laz_writer.hpp index 2f51d8f..26bc3d4 100644 --- a/src/laz/laz_writer.hpp +++ b/src/laz/laz_writer.hpp @@ -17,14 +17,22 @@ #pragma once +#include +#include #include #include +#include +#include +#include #include "las_point.hpp" #include "laz/chunktable.hpp" #include "laz/encoders.hpp" +#include "laz/layered_stream.hpp" +#include "laz/laz_reader.hpp" #include "laz/point10_encoder.hpp" #include "laz/rgb12_encoder.hpp" +#include "laz/rgb14_encoder.hpp" #include "laz_vlr.hpp" namespace laspp { @@ -53,71 +61,193 @@ class LAZWriter { LASPP_ASSERT_GT(points.size(), 0); std::stringstream compressed_data; + bool layered_compression = m_special_vlr.compressor == LAZCompressor::LayeredChunked; + std::vector encoders; - for (LAZItemRecord record : m_special_vlr.items_records) { - switch (record.item_type) { - case LAZItemType::Point10: { - LASPointFormat0 point; - if constexpr (is_copy_assignable()) { - point = points[0]; + std::vector< + std::variant>, std::unique_ptr>>> + layered_streams; + std::vector encoder_num_layers; + size_t total_layer_count = 0; + { + std::optional context; + for (LAZItemRecord record : m_special_vlr.items_records) { + switch (record.item_type) { + case LAZItemType::Point10: { + LASPointFormat0 point; + if constexpr (is_copy_assignable()) { + point = points[0]; + } + if (layered_compression) { + LASPP_FAIL("Cannot use Point10 encoder with layered compression"); + } + encoders.emplace_back(LASPointFormat0Encoder(point)); + compressed_data.write(reinterpret_cast(&point), sizeof(LASPointFormat0)); + break; } - encoders.emplace_back(LASPointFormat0Encoder(point)); - compressed_data.write(reinterpret_cast(&point), sizeof(LASPointFormat0)); - break; - } - case LAZItemType::GPSTime11: { - GPSTime gps_time; - if constexpr (is_copy_assignable()) { - gps_time = points[0]; + case LAZItemType::Point14: { + LASPointFormat6 point; + if constexpr (is_copy_assignable()) { + point = points[0]; + } + encoders.emplace_back(LASPointFormat6Encoder(point)); + context = std::get(encoders.back()).get_active_context(); + compressed_data.write(reinterpret_cast(&point), sizeof(LASPointFormat6)); + layered_streams.emplace_back( + std::make_unique>()); + encoder_num_layers.push_back(LASPointFormat6Encoder::NUM_LAYERS); + total_layer_count += LASPointFormat6Encoder::NUM_LAYERS; + break; } - encoders.emplace_back(GPSTime11Encoder(gps_time)); - compressed_data.write(reinterpret_cast(&gps_time), sizeof(GPSTime)); - break; - } - case LAZItemType::RGB12: { - ColorData color_data; - if constexpr (is_copy_assignable()) { - color_data = points[0]; + case LAZItemType::GPSTime11: { + GPSTime gps_time{}; + if constexpr (is_copy_assignable()) { + gps_time = points[0]; + } + if (layered_compression) { + LASPP_FAIL("Cannot use GPSTime11 encoder with layered compression"); + } + encoders.emplace_back(GPSTime11Encoder(gps_time)); + compressed_data.write(reinterpret_cast(&gps_time), sizeof(GPSTime)); + break; } - encoders.emplace_back(RGB12Encoder(color_data)); - compressed_data.write(reinterpret_cast(&color_data), sizeof(ColorData)); - break; - } - case LAZItemType::Byte: { - std::vector bytes(record.item_size); - if constexpr (is_copy_assignable()) { - bytes = points[0]; + case LAZItemType::RGB12: { + ColorData color_data{}; + if constexpr (is_copy_assignable()) { + color_data = points[0]; + } + if (layered_compression) { + LASPP_FAIL("Cannot use RGB12 encoder with layered compression"); + } + encoders.emplace_back(RGB12Encoder(color_data)); + compressed_data.write(reinterpret_cast(&color_data), sizeof(ColorData)); + break; + } + case LAZItemType::RGB14: { + ColorData color_data{}; + if constexpr (is_copy_assignable()) { + color_data = points[0]; + } + encoders.emplace_back(RGB14Encoder(color_data, context.value())); + compressed_data.write(reinterpret_cast(&color_data), sizeof(ColorData)); + layered_streams.emplace_back( + std::make_unique>()); + encoder_num_layers.push_back(RGB14Encoder::NUM_LAYERS); + total_layer_count += RGB14Encoder::NUM_LAYERS; + break; } - LASPP_ASSERT_EQ(record.item_size, bytes.size()); - encoders.emplace_back(BytesEncoder(bytes)); - compressed_data.write(reinterpret_cast(bytes.data()), - static_cast(bytes.size())); - break; + case LAZItemType::Byte: { + std::vector bytes(record.item_size); + if constexpr (is_copy_assignable()) { + bytes = points[0]; + } + LASPP_ASSERT_EQ(record.item_size, bytes.size()); + if (layered_compression) { + LASPP_FAIL("Cannot use Byte encoder with layered compression"); + } + encoders.emplace_back(BytesEncoder(bytes)); + compressed_data.write(reinterpret_cast(bytes.data()), + static_cast(bytes.size())); + break; + } + default: + LASPP_FAIL("Currently unsupported LAZ item type: ", + static_cast(record.item_type)); } - default: - LASPP_FAIL("Currently unsupported LAZ item type: ", - static_cast(record.item_type)); } } + if (layered_compression) { + LASPP_ASSERT_EQ(encoders.size(), layered_streams.size()); + LASPP_ASSERT_EQ(encoders.size(), encoder_num_layers.size()); + } else { + LASPP_ASSERT_EQ(layered_streams.size(), 0); + LASPP_ASSERT_EQ(encoder_num_layers.size(), 0); + } + + std::vector layer_sizes; + std::string layer_payload; { - OutStream compressed_out_stream(compressed_data); - std::vector next_bytes; - for (size_t i = 1; i < points.size(); i++) { - for (LAZEncoder& laz_encoder : encoders) { - std::visit( - [&compressed_out_stream, &points, &i](auto&& encoder) { - if constexpr (is_copy_assignable>, - T>()) { - decltype(encoder.last_value()) value_to_encode = points[i]; - encoder.encode(compressed_out_stream, value_to_encode); - } - }, - laz_encoder); + std::unique_ptr compressed_out_stream; + if (!layered_compression) { + compressed_out_stream = std::make_unique(compressed_data); + } + + { + for (size_t i = 1; i < points.size(); i++) { + std::optional context; + for (size_t encoder_index = 0; encoder_index < encoders.size(); encoder_index++) { + LAZEncoder& laz_encoder = encoders[encoder_index]; + std::visit( + [&](auto&& encoder) { + if constexpr (is_copy_assignable>, + T>()) { + decltype(encoder.last_value()) value_to_encode = points[i]; + using EncoderType = std::decay_t; + if constexpr (std::is_same_v) { + LASPP_ASSERT(layered_compression); + auto& streams = *std::get< + std::unique_ptr>>( + layered_streams[encoder_index]); + encoder.encode(streams, value_to_encode); + context = encoder.get_active_context(); + } else if constexpr (std::is_same_v) { + LASPP_ASSERT(layered_compression); + auto& streams = + *std::get>>( + layered_streams[encoder_index]); + encoder.encode(streams, value_to_encode, context.value()); + } else { + LASPP_ASSERT(!layered_compression); + LASPP_ASSERT(compressed_out_stream != nullptr); + encoder.encode(*compressed_out_stream, value_to_encode); + } + } + }, + laz_encoder); + } } } } + + if (total_layer_count > 0) { + layer_sizes.reserve(total_layer_count); + for (size_t encoder_index = 0; encoder_index < encoders.size(); encoder_index++) { + std::visit( + [&](auto&& encoder) { + if constexpr (has_num_layers_v) { + auto& streams = *std::get::NUM_LAYERS>>>( + layered_streams[encoder_index]); + auto sizes = streams.layer_sizes(); + for (uint32_t size : sizes) { + layer_sizes.push_back(size); + } + layer_payload += streams.cb().str(); + } + }, + encoders[encoder_index]); + } + } + + if (layered_compression) { + LASPP_ASSERT_EQ(total_layer_count, layer_sizes.size()); + std::string seed_data = compressed_data.str(); + compressed_data.str(std::string()); + compressed_data.clear(); + + compressed_data.write(seed_data.data(), static_cast(seed_data.size())); + uint32_t num_points = static_cast(points.size()); + compressed_data.write(reinterpret_cast(&num_points), sizeof(uint32_t)); + for (uint32_t size : layer_sizes) { + compressed_data.write(reinterpret_cast(&size), sizeof(uint32_t)); + } + compressed_data.write(layer_payload.data(), + static_cast(layer_payload.size())); + } else { + LASPP_ASSERT_EQ(total_layer_count, 0); + } return compressed_data; } diff --git a/src/laz/point10_encoder.hpp b/src/laz/point10_encoder.hpp index 8cf7e6d..e6a3e14 100644 --- a/src/laz/point10_encoder.hpp +++ b/src/laz/point10_encoder.hpp @@ -23,15 +23,10 @@ #include "laz/integer_encoder.hpp" #include "laz/stream.hpp" #include "laz/streaming_median.hpp" +#include "utilities/arithmetic.hpp" namespace laspp { -inline int32_t wrapping_int32_add(int32_t a, int32_t b) { - return static_cast(static_cast(a) + static_cast(b)); -} - -inline int32_t wrapping_int32_sub(int32_t a, int32_t b) { return wrapping_int32_add(a, -b); } - template constexpr std::array create_array(const T& val) { std::array arr; @@ -79,63 +74,62 @@ class LASPointFormat0Encoder { [m_last_las_point.bit_byte.return_number]; } - LASPointFormat0 decode(InStream& stream) { - uint_fast16_t changed_values = m_changed_values_encoder.decode_symbol(stream); - if (changed_values) { - if (changed_values & (1 << 5)) { - m_last_las_point.bit_byte = static_cast( - m_bit_byte_encoder[m_last_las_point.bit_byte].decode_symbol(stream)); - - m_m = return_map_m[m_last_las_point.bit_byte.number_of_returns] - [m_last_las_point.bit_byte.return_number]; - } + private: + void decode_changed_values(InStream& stream, uint_fast16_t changed_values) { + if (changed_values & (1 << 5)) { + m_last_las_point.bit_byte = + static_cast(m_bit_byte_encoder[m_last_las_point.bit_byte].decode_symbol(stream)); + m_m = return_map_m[m_last_las_point.bit_byte.number_of_returns] + [m_last_las_point.bit_byte.return_number]; + } - if (changed_values & (1 << 4)) { - if (m_m <= 2) { - m_last_las_point.intensity = static_cast( - m_prev_intensities[m_m] + m_intensity_encoder.decode_int(m_m, stream)); - } else { - m_last_las_point.intensity = - m_prev_intensities[m_m] + - static_cast(m_intensity_encoder.decode_int(3, stream)); - } - m_prev_intensities[m_m] = m_last_las_point.intensity; + if (changed_values & (1 << 4)) { + if (m_m <= 2) { + m_last_las_point.intensity = static_cast( + m_prev_intensities[m_m] + m_intensity_encoder.decode_int(m_m, stream)); } else { - m_last_las_point.intensity = m_prev_intensities[m_m]; + m_last_las_point.intensity = + m_prev_intensities[m_m] + + static_cast(m_intensity_encoder.decode_int(3, stream)); } + m_prev_intensities[m_m] = m_last_las_point.intensity; + } else { + m_last_las_point.intensity = m_prev_intensities[m_m]; + } - if (changed_values & (1 << 3)) { - m_last_las_point.classification_byte = static_cast( - m_classification_encoder[m_last_las_point.classification_byte].decode_symbol(stream)); - } - if (changed_values & (1 << 2)) { - m_last_las_point.scan_angle_rank += static_cast( - m_scan_angle_rank_encoder[m_last_las_point.bit_byte.scan_direction_flag].decode_symbol( - stream)); - } - if (changed_values & (1 << 1)) { - m_last_las_point.user_data = static_cast( - m_user_data_encoder[m_last_las_point.user_data].decode_symbol(stream)); - } - if (changed_values & 1) { - m_last_las_point.point_source_id += - static_cast(m_point_source_id_encoder.decode_int(stream)); - } + if (changed_values & (1 << 3)) { + m_last_las_point.classification_byte = static_cast( + m_classification_encoder[m_last_las_point.classification_byte].decode_symbol(stream)); + } + if (changed_values & (1 << 2)) { + m_last_las_point.scan_angle_rank += static_cast( + m_scan_angle_rank_encoder[m_last_las_point.bit_byte.scan_direction_flag].decode_symbol( + stream)); + } + if (changed_values & (1 << 1)) { + m_last_las_point.user_data = static_cast( + m_user_data_encoder[m_last_las_point.user_data].decode_symbol(stream)); } + if (changed_values & 1) { + m_last_las_point.point_source_id += + static_cast(m_point_source_id_encoder.decode_int(stream)); + } + } + void decode_coordinates(InStream& stream) { // X - int32_t decoded_int = - m_dx_encoder[m_last_las_point.bit_byte.number_of_returns == 1].decode_int(stream); + bool single_return = m_last_las_point.bit_byte.number_of_returns == 1; + int32_t decoded_int = m_dx_encoder[single_return].decode_int(stream); int32_t dx = wrapping_int32_add(decoded_int, m_dx_streamed_median[m_m].get_median()); m_dx_streamed_median[m_m].insert( wrapping_int32_add(decoded_int, m_dx_streamed_median[m_m].get_median())); m_last_las_point.x = wrapping_int32_add(m_last_las_point.x, dx); - uint_fast16_t dx_k = m_dx_encoder[m_last_las_point.bit_byte.number_of_returns == 1].prev_k(); + uint_fast16_t dx_k = m_dx_encoder[single_return].prev_k(); // Y uint32_t dy_instance = (dx_k < 20) ? (dx_k & (~1u)) : 20; - if (m_last_las_point.bit_byte.number_of_returns == 1) { + if (single_return) { dy_instance++; } int32_t dy = wrapping_int32_add(m_dy_encoder[dy_instance].decode_int(stream), @@ -146,7 +140,7 @@ class LASPointFormat0Encoder { // Z uint32_t kxy = static_cast((dx_k + m_dy_encoder[dy_instance].prev_k()) / 2); uint32_t dz_instance = (kxy < 18) ? (kxy & (~1u)) : 18; - if (m_last_las_point.bit_byte.number_of_returns == 1) { + if (single_return) { dz_instance++; } @@ -155,11 +149,20 @@ class LASPointFormat0Encoder { m_last_las_point.bit_byte.return_number)); m_last_las_point.z = wrapping_int32_add(m_prev_dz[l], dz); m_prev_dz[l] = m_last_las_point.z; + } + public: + LASPointFormat0 decode(InStream& stream) { + uint_fast16_t changed_values = m_changed_values_encoder.decode_symbol(stream); + if (changed_values) { + decode_changed_values(stream, changed_values); + } + decode_coordinates(stream); return m_last_las_point; } - void encode(OutStream& out_stream, const LASPointFormat0& las_point) { + private: + uint_fast16_t compute_changed_values(const LASPointFormat0& las_point) { uint_fast16_t changed_values = 0; if (las_point.bit_byte != m_last_las_point.bit_byte) { changed_values |= (1 << 5); @@ -180,62 +183,64 @@ class LASPointFormat0Encoder { if (las_point.point_source_id != m_last_las_point.point_source_id) { changed_values |= 1; } + return changed_values; + } - m_changed_values_encoder.encode_symbol(out_stream, changed_values); - - if (changed_values) { - if (changed_values & (1 << 5)) { - m_bit_byte_encoder[m_last_las_point.bit_byte].encode_symbol(out_stream, las_point.bit_byte); - m_last_las_point.bit_byte = las_point.bit_byte; - } + void encode_changed_values(OutStream& out_stream, uint_fast16_t changed_values, + const LASPointFormat0& las_point) { + if (changed_values & (1 << 5)) { + m_bit_byte_encoder[m_last_las_point.bit_byte].encode_symbol(out_stream, las_point.bit_byte); + m_last_las_point.bit_byte = las_point.bit_byte; + } - if (changed_values & (1 << 4)) { - if (m_m <= 2) { - m_intensity_encoder.encode_int( - m_m, out_stream, static_cast(las_point.intensity - m_prev_intensities[m_m])); - } else { - m_intensity_encoder.encode_int( - 3, out_stream, static_cast(las_point.intensity - m_prev_intensities[m_m])); - } - m_prev_intensities[m_m] = las_point.intensity; - m_last_las_point.intensity = las_point.intensity; + if (changed_values & (1 << 4)) { + int16_t intensity_diff = static_cast(las_point.intensity - m_prev_intensities[m_m]); + if (m_m <= 2) { + m_intensity_encoder.encode_int(m_m, out_stream, intensity_diff); + } else { + m_intensity_encoder.encode_int(3, out_stream, intensity_diff); } + m_prev_intensities[m_m] = las_point.intensity; + m_last_las_point.intensity = las_point.intensity; + } - if (changed_values & (1 << 3)) { - m_classification_encoder[m_last_las_point.classification_byte].encode_symbol( - out_stream, las_point.classification_byte); - m_last_las_point.classification_byte = las_point.classification_byte; - } - if (changed_values & (1 << 2)) { - m_scan_angle_rank_encoder[m_last_las_point.bit_byte.scan_direction_flag].encode_symbol( - out_stream, - static_cast(las_point.scan_angle_rank - m_last_las_point.scan_angle_rank)); - m_last_las_point.scan_angle_rank = las_point.scan_angle_rank; - } - if (changed_values & (1 << 1)) { - m_user_data_encoder[m_last_las_point.user_data].encode_symbol(out_stream, - las_point.user_data); - m_last_las_point.user_data = las_point.user_data; - } - if (changed_values & 1) { - m_point_source_id_encoder.encode_int( - out_stream, las_point.point_source_id - m_last_las_point.point_source_id); - m_last_las_point.point_source_id = las_point.point_source_id; - } + if (changed_values & (1 << 3)) { + m_classification_encoder[m_last_las_point.classification_byte].encode_symbol( + out_stream, las_point.classification_byte); + m_last_las_point.classification_byte = las_point.classification_byte; + } + if (changed_values & (1 << 2)) { + m_scan_angle_rank_encoder[m_last_las_point.bit_byte.scan_direction_flag].encode_symbol( + out_stream, + static_cast(las_point.scan_angle_rank - m_last_las_point.scan_angle_rank)); + m_last_las_point.scan_angle_rank = las_point.scan_angle_rank; + } + if (changed_values & (1 << 1)) { + m_user_data_encoder[m_last_las_point.user_data].encode_symbol(out_stream, + las_point.user_data); + m_last_las_point.user_data = las_point.user_data; } + if (changed_values & 1) { + m_point_source_id_encoder.encode_int( + out_stream, las_point.point_source_id - m_last_las_point.point_source_id); + m_last_las_point.point_source_id = las_point.point_source_id; + } + } + void encode_coordinates(OutStream& out_stream, const LASPointFormat0& las_point) { // X + bool single_return = las_point.bit_byte.number_of_returns == 1; int32_t dx = wrapping_int32_sub(las_point.x, m_last_las_point.x); - m_dx_encoder[las_point.bit_byte.number_of_returns == 1].encode_int( + m_dx_encoder[single_return].encode_int( out_stream, wrapping_int32_sub(dx, m_dx_streamed_median[m_m].get_median())); m_dx_streamed_median[m_m].insert(dx); m_last_las_point.x = las_point.x; - uint_fast16_t dx_k = m_dx_encoder[las_point.bit_byte.number_of_returns == 1].prev_k(); + uint_fast16_t dx_k = m_dx_encoder[single_return].prev_k(); // Y uint32_t dy_instance = (dx_k < 20) ? (dx_k & (~1u)) : 20; - if (las_point.bit_byte.number_of_returns == 1) { + if (single_return) { dy_instance++; } int32_t dy = wrapping_int32_sub(las_point.y, m_last_las_point.y); @@ -257,6 +262,16 @@ class LASPointFormat0Encoder { m_prev_dz[l] = las_point.z; m_last_las_point.z = las_point.z; } + + public: + void encode(OutStream& out_stream, const LASPointFormat0& las_point) { + uint_fast16_t changed_values = compute_changed_values(las_point); + m_changed_values_encoder.encode_symbol(out_stream, changed_values); + if (changed_values) { + encode_changed_values(out_stream, changed_values, las_point); + } + encode_coordinates(out_stream, las_point); + } }; } // namespace laspp diff --git a/src/laz/point14_encoder.hpp b/src/laz/point14_encoder.hpp index 31fea0f..e28ebc4 100644 --- a/src/laz/point14_encoder.hpp +++ b/src/laz/point14_encoder.hpp @@ -17,27 +17,34 @@ #pragma once +#include #include #include "las_point.hpp" +#include "laz/gpstime11_encoder.hpp" #include "laz/integer_encoder.hpp" -#include "laz/stream.hpp" +#include "laz/layered_stream.hpp" #include "laz/streaming_median.hpp" +#include "utilities/arithmetic.hpp" namespace laspp { -inline int32_t wrapping_int32_add(int32_t a, int32_t b) { - return static_cast(static_cast(a) + static_cast(b)); -} - -inline int32_t wrapping_int32_sub(int32_t a, int32_t b) { return wrapping_int32_add(a, -b); } - -template -constexpr std::array create_array(const T& val) { - std::array arr; - arr.fill(val); - return arr; -} +#define LASPP_CHANNEL_RETURNS_LAYER 0 +#define LASPP_Z_LAYER 1 +#define LASPP_CLASSIFICATION_LAYER 2 +#define LASPP_FLAGS_LAYER 3 +#define LASPP_INTENSITY_LAYER 4 +#define LASPP_SCAN_ANGLE_LAYER 5 +#define LASPP_USER_DATA_LAYER 6 +#define LASPP_POINT_SOURCE_LAYER 7 +#define LASPP_GPS_TIME_LAYER 8 + +#define LASPP_POINT14_RETURN_NUMBER_CHANGED_CONTEXT_BITS 0x3 +#define LASPP_POINT14_NUMBER_OF_RETURNS_CHANGED_CONTEXT_BIT 1 << 2 +#define LASPP_POINT14_SCAN_ANGLE_CHANGED_CONTEXT_BIT 1 << 3 +#define LASPP_POINT14_GPS_TIME_CHANGED_CONTEXT_BIT 1 << 4 +#define LASPP_POINT14_POINT_SOURCE_CHANGED_CONTEXT_BIT 1 << 5 +#define LASPP_POINT14_SCANNER_CHANNEL_CHANGED_CONTEXT_BIT 1 << 6 struct LASPointFormat6Context : LASPointFormat6 { bool initialized; @@ -51,6 +58,16 @@ struct LASPointFormat6Context : LASPointFormat6 { std::array, 16> dx_streamed_median; MultiInstanceIntegerEncoder<32, 22> dy_encoder; std::array, 16> dy_streamed_median; + MultiInstanceIntegerEncoder<32, 22> dz_encoder; + std::array last_z; + std::array, 64> classification_encoders; + std::array, 64> flag_encoders; + std::array, 64> user_data_encoders; + MultiInstanceIntegerEncoder<16, 8> intensity_encoder; + std::array last_intensity; + MultiInstanceIntegerEncoder<16, 2> scan_angle_encoder; + IntegerEncoder<16> point_source_id_encoder; + GeneralGPSTimeEncoder gps_time_encoder; uint_fast8_t m; uint_fast8_t l; @@ -58,7 +75,12 @@ struct LASPointFormat6Context : LASPointFormat6 { uint_fast8_t cpr; uint_fast8_t cprgps; - LASPointFormat6Context() : initialized(false) {} + static constexpr int NUM_LAYERS = 9; + using LayerInStreams = LayeredInStreams; + using LayerOutStreams = LayeredOutStreams; + + LASPointFormat6Context() + : initialized(false), gps_time_encoder(GPSTime(0)), m(0), l(0), cpr(0), cprgps(0) {} static constexpr uint8_t number_return_map_6ctx[16][16] = { {0, 1, 2, 3, 4, 5, 3, 4, 4, 5, 5, 5, 5, 5, 5, 5}, @@ -96,7 +118,7 @@ struct LASPointFormat6Context : LASPointFormat6 { {7, 7, 7, 7, 7, 7, 7, 7, 6, 5, 4, 3, 2, 1, 0, 1}, {7, 7, 7, 7, 7, 7, 7, 7, 7, 6, 5, 4, 3, 2, 1, 0}}; - void set_cpr(bool gps_changed = false) { + inline void set_cpr(bool gps_changed = false) { if (return_number == 1) { if (number_of_returns < 2) { cpr = 3; @@ -113,171 +135,470 @@ struct LASPointFormat6Context : LASPointFormat6 { cprgps = static_cast(cpr * 2 + gps_changed); } - void set_m_l() { + inline void set_m_l() { m = number_return_map_6ctx[number_of_returns][return_number]; l = number_return_level_8ctx[number_of_returns][return_number]; } - void initialize(const LASPointFormat6& las_point) { + inline void initialize(const LASPointFormat6& las_point) { static_cast(*this) = las_point; set_cpr(); set_m_l(); + // Copy values to local variables to avoid misaligned reference issues + // with packed structures when passing to std::array::fill() + const int32_t z_local = las_point.z; + const uint16_t intensity_local = las_point.intensity; + last_z.fill(z_local); + last_intensity.fill(intensity_local); + gps_time_encoder.set_previous_value(GPSTime(las_point.gps_time)); initialized = true; } + + inline void decode_return_number(uint_fast16_t return_flag, bool gps_time_change, + LayerInStreams& streams) { + uint8_t last_return_number = return_number; + if (return_flag == 0b01) { + return_number = static_cast((last_return_number + 1) & 0xF); + } else if (return_flag == 0b10) { + return_number = static_cast((last_return_number + 15) & 0xF); + } else if (return_flag == 0b11) { + if (gps_time_change) { + uint_fast16_t decoded_return = + return_number_diff_time_encoder[last_return_number].decode_symbol( + streams[LASPP_CHANNEL_RETURNS_LAYER]); + return_number = static_cast(decoded_return & 0xF); + } else { + uint_fast16_t sym = + d_return_number_same_time_encoder.decode_symbol(streams[LASPP_CHANNEL_RETURNS_LAYER]); + return_number = static_cast((last_return_number + sym + 2) & 0xF); + } + } + } + + inline void decode_number_of_returns(uint_fast16_t changed_values, LayerInStreams& streams) { + if (changed_values & LASPP_POINT14_NUMBER_OF_RETURNS_CHANGED_CONTEXT_BIT) { + uint_fast16_t new_number_of_returns = + n_returns_encoders[number_of_returns].decode_symbol(streams[LASPP_CHANNEL_RETURNS_LAYER]); + number_of_returns = static_cast(new_number_of_returns & 0xF); + } + } + + inline void decode_coordinates(bool gps_time_change, LayerInStreams& streams) { + uint32_t mgps = m * 2u + static_cast(gps_time_change); + bool single_return = number_of_returns == 1; + + int32_t decoded_dx = + dx_encoders[single_return].decode_int(streams[LASPP_CHANNEL_RETURNS_LAYER]); + int32_t dx = wrapping_int32_add(decoded_dx, dx_streamed_median[mgps].get_median()); + dx_streamed_median[mgps].insert(dx); + x = wrapping_int32_add(x, dx); + + uint_fast16_t dx_k = dx_encoders[single_return].prev_k(); + uint32_t dy_instance = (dx_k < 20) ? (dx_k & (~1u)) : 20; + if (single_return) { + dy_instance++; + } + int32_t decoded_dy = dy_encoder[dy_instance].decode_int(streams[LASPP_CHANNEL_RETURNS_LAYER]); + int32_t dy = wrapping_int32_add(decoded_dy, dy_streamed_median[mgps].get_median()); + dy_streamed_median[mgps].insert(dy); + y = wrapping_int32_add(y, dy); + + uint_fast16_t dy_k = dy_encoder[dy_instance].prev_k(); + uint_fast16_t kxy_sum = (dx_k + dy_k) / 2; + uint32_t kxy = static_cast(kxy_sum); + uint32_t dz_instance = (kxy < 18) ? (kxy & (~1u)) : 18; + if (single_return) { + dz_instance++; + } + int32_t decoded_dz = dz_encoder[dz_instance].decode_int(streams[LASPP_Z_LAYER]); + z = wrapping_int32_add(last_z[l], decoded_dz); + last_z[l] = z; + } + + inline void decode_classification(LayerInStreams& streams) { + if (!streams.non_empty(LASPP_CLASSIFICATION_LAYER)) { + return; + } + uint8_t last_classification = static_cast(classification); + uint8_t classification_idx = + static_cast(((last_classification & 0x1F) << 1) + (cpr == 3 ? 1 : 0)); + uint_fast16_t decoded_classification = + classification_encoders[classification_idx].decode_symbol( + streams[LASPP_CLASSIFICATION_LAYER]); + classification = static_cast(decoded_classification); + } + + inline void decode_flags(LayerInStreams& streams) { + if (!streams.non_empty(LASPP_FLAGS_LAYER)) { + return; + } + uint8_t last_flags = static_cast((edge_of_flight_line << 5) | + (scan_direction_flag << 4) | classification_flags); + uint_fast16_t decoded_flags = + flag_encoders[last_flags].decode_symbol(streams[LASPP_FLAGS_LAYER]); + edge_of_flight_line = static_cast((decoded_flags >> 5) & 0x1); + scan_direction_flag = static_cast((decoded_flags >> 4) & 0x1); + classification_flags = static_cast(decoded_flags & 0x0F); + } + + inline void decode_intensity(LayerInStreams& streams) { + if (!streams.non_empty(LASPP_INTENSITY_LAYER)) { + return; + } + int32_t intensity_delta = intensity_encoder[cpr].decode_int(streams[LASPP_INTENSITY_LAYER]); + int32_t new_intensity = static_cast(last_intensity[cprgps]) + intensity_delta; + intensity = static_cast(new_intensity); + last_intensity[cprgps] = intensity; + } + + inline void decode_user_data(LayerInStreams& streams) { + if (!streams.non_empty(LASPP_USER_DATA_LAYER)) { + return; + } + uint8_t user_ctx = user_data / 4; + uint_fast16_t decoded_user_data = + user_data_encoders[user_ctx].decode_symbol(streams[LASPP_USER_DATA_LAYER]); + user_data = static_cast(decoded_user_data); + } + + inline void encode_return_number(uint_fast16_t return_flag, uint8_t last_return_number, + uint_fast16_t rn_diff, bool gps_time_change, + const LASPointFormat6& point, LayerOutStreams& streams) { + uint8_t new_return_number = last_return_number; + if (return_flag == 0b01) { + new_return_number = static_cast((last_return_number + 1) & 0xF); + } else if (return_flag == 0b10) { + new_return_number = static_cast((last_return_number + 15) & 0xF); + } else if (return_flag == 0b11) { + if (gps_time_change) { + return_number_diff_time_encoder[last_return_number].encode_symbol( + streams[LASPP_CHANNEL_RETURNS_LAYER], point.return_number); + new_return_number = point.return_number; + } else { + d_return_number_same_time_encoder.encode_symbol(streams[LASPP_CHANNEL_RETURNS_LAYER], + rn_diff - 2); + new_return_number = static_cast((last_return_number + rn_diff) & 0xF); + } + } + return_number = static_cast(new_return_number & 0xF); + } + + inline void encode_number_of_returns(uint_fast16_t changed_values, uint8_t last_number_of_returns, + const LASPointFormat6& point, LayerOutStreams& streams) { + if (changed_values & LASPP_POINT14_NUMBER_OF_RETURNS_CHANGED_CONTEXT_BIT) { + n_returns_encoders[last_number_of_returns].encode_symbol(streams[LASPP_CHANNEL_RETURNS_LAYER], + point.number_of_returns); + number_of_returns = static_cast(point.number_of_returns & 0xF); + } else { + number_of_returns = static_cast(last_number_of_returns & 0xF); + } + } + + inline void encode_coordinates(bool gps_changed, const LASPointFormat6& point, + LayerOutStreams& streams) { + uint32_t mgps = m * 2u + static_cast(gps_changed); + bool single_return = number_of_returns == 1; + + int32_t dx = wrapping_int32_sub(point.x, x); + dx_encoders[single_return].encode_int( + streams[LASPP_CHANNEL_RETURNS_LAYER], + wrapping_int32_sub(dx, dx_streamed_median[mgps].get_median())); + dx_streamed_median[mgps].insert(dx); + x = point.x; + + uint_fast16_t dx_k = dx_encoders[single_return].prev_k(); + uint32_t dy_instance = (dx_k < 20) ? (dx_k & (~1u)) : 20; + if (single_return) { + dy_instance++; + } + int32_t dy = wrapping_int32_sub(point.y, y); + dy_encoder[dy_instance].encode_int( + streams[LASPP_CHANNEL_RETURNS_LAYER], + wrapping_int32_sub(dy, dy_streamed_median[mgps].get_median())); + dy_streamed_median[mgps].insert(dy); + y = point.y; + + uint_fast16_t dy_k = dy_encoder[dy_instance].prev_k(); + uint_fast16_t kxy_sum = (dx_k + dy_k) / 2; + uint32_t kxy = static_cast(kxy_sum); + uint32_t dz_instance = (kxy < 18) ? (kxy & (~1u)) : 18; + if (single_return) { + dz_instance++; + } + int32_t dz = wrapping_int32_sub(point.z, last_z[l]); + dz_encoder[dz_instance].encode_int(streams[LASPP_Z_LAYER], dz); + last_z[l] = point.z; + z = point.z; + } + + inline void encode_classification(const LASClassification last_classification, + const LASPointFormat6& point, LayerOutStreams& streams) { + uint8_t classification_idx = static_cast( + ((static_cast(last_classification) & 0x1F) << 1) + (cpr == 3 ? 1 : 0)); + classification_encoders[classification_idx].encode_symbol( + streams[LASPP_CLASSIFICATION_LAYER], static_cast(point.classification)); + classification = point.classification; + } + + inline void encode_flags(uint8_t last_flags, const LASPointFormat6& point, + LayerOutStreams& streams) { + flag_encoders[last_flags].encode_symbol( + streams[LASPP_FLAGS_LAYER], + static_cast((point.edge_of_flight_line << 5) | (point.scan_direction_flag << 4) | + point.classification_flags)); + edge_of_flight_line = point.edge_of_flight_line; + scan_direction_flag = point.scan_direction_flag; + classification_flags = point.classification_flags; + } + + inline void encode_intensity(const LASPointFormat6& point, LayerOutStreams& streams) { + int32_t intensity_base = static_cast(last_intensity[cprgps]); + int32_t intensity_diff = static_cast(point.intensity) - intensity_base; + intensity_encoder[cpr].encode_int(streams[LASPP_INTENSITY_LAYER], intensity_diff); + last_intensity[cprgps] = point.intensity; + intensity = point.intensity; + } + + inline void encode_user_data(uint8_t prev_user_data, const LASPointFormat6& point, + LayerOutStreams& streams) { + uint8_t user_ctx = prev_user_data / 4; + user_data_encoders[user_ctx].encode_symbol(streams[LASPP_USER_DATA_LAYER], point.user_data); + user_data = point.user_data; + } }; -class LASPointFormat6Encoder { +template +class LASPointFormat6EncoderBase { std::array m_contexts; uint_fast8_t m_context; + uint_fast8_t m_external_context; public: + static constexpr int NUM_LAYERS = LASPointFormat6Context::NUM_LAYERS; + using LayerInStreams = LayeredInStreams; + using LayerOutStreams = LayeredOutStreams; + using EncodedType = LASPointFormat6; const LASPointFormat6& last_value() const { return m_contexts[m_context]; } - explicit LASPointFormat6Encoder(const LASPointFormat6& initial_las_point) { + explicit LASPointFormat6EncoderBase(const LASPointFormat6& initial_las_point) { m_context = initial_las_point.scanner_channel; + m_external_context = m_context; m_contexts[m_context].initialize(initial_las_point); } - LASPointFormat6 decode(InStream& stream) { - uint_fast16_t changed_values = - m_contexts[m_context].changed_values_encoders[m_contexts[m_context].cprgps].decode_symbol( - stream); - if (changed_values & (1 << 6)) { - uint_fast16_t d_scanner_channel = - 1 + m_contexts[m_context].d_scanner_channel_encoder.decode_symbol(stream); - uint_fast16_t new_scanner_channel = - (m_contexts[m_context].scanner_channel + d_scanner_channel) % 4; - if (!m_contexts[new_scanner_channel].initialized) { - m_contexts[new_scanner_channel].initialize(m_contexts[m_context]); - } - m_context = static_cast(new_scanner_channel); + uint8_t get_active_context() const { return m_external_context; } + + private: + inline void handle_scanner_channel_context_change(LASPointFormat6Context& prev_context, + uint_fast16_t changed_values, + LayerInStreams& streams) { + if (!(changed_values & LASPP_POINT14_SCANNER_CHANNEL_CHANGED_CONTEXT_BIT)) { + // Workaround for issue with context setting in V3: https://github.com/LASzip/LASzip/issues/74 + m_external_context = 0; + return; } - LASPointFormat6Context& context = m_contexts[m_context]; + uint_fast16_t d_scanner_channel = + prev_context.d_scanner_channel_encoder.decode_symbol(streams[LASPP_CHANNEL_RETURNS_LAYER]); + uint_fast8_t new_scanner_channel = + static_cast((m_context + d_scanner_channel + 1) % 4); + if (!m_contexts[new_scanner_channel].initialized) { + m_contexts[new_scanner_channel].initialize(prev_context); + m_contexts[new_scanner_channel].scanner_channel = + static_cast(new_scanner_channel & 0x3); + } + m_context = new_scanner_channel; + m_external_context = m_context; + } - if (changed_values) { - if (changed_values & (1 << 2)) { - context.number_of_returns = - static_cast( - context.n_returns_encoders[context.number_of_returns].decode_symbol(stream)) & - 0xf; - } + public: + inline LASPointFormat6 decode(LayerInStreams& streams) { + LASPointFormat6Context& prev_context = m_contexts[m_context]; + uint_fast16_t changed_values = + prev_context.changed_values_encoders[prev_context.cprgps].decode_symbol( + streams[LASPP_CHANNEL_RETURNS_LAYER]); + bool gps_time_changed = + static_cast(changed_values & LASPP_POINT14_GPS_TIME_CHANGED_CONTEXT_BIT); - if (changed_values & (0b11)) { - uint_fast16_t d_return_number; - if (changed_values & 0b1 && changed_values & 0b10) { - d_return_number = 2 + context.d_return_number_same_time_encoder.decode_symbol(stream); - } else if (changed_values & 0b1) { - d_return_number = 1; - } else { - d_return_number = 15; - } - context.return_number = (context.return_number + d_return_number) % 16; - } + handle_scanner_channel_context_change(prev_context, changed_values, streams); - context.set_m_l(); - } + LASPointFormat6Context& context = m_contexts[m_context]; - uint32_t mgps = context.m * 2u + static_cast(bool(changed_values & (1 << 4))); - int32_t decoded_int = context.dx_encoders[context.number_of_returns == 1].decode_int(stream); + context.decode_number_of_returns(changed_values, streams); - int32_t dx = wrapping_int32_add(decoded_int, context.dx_streamed_median[mgps].get_median()); - context.dx_streamed_median[mgps].insert(dx); - context.x = wrapping_int32_add(context.x, dx); + uint_fast16_t return_flag = changed_values & LASPP_POINT14_RETURN_NUMBER_CHANGED_CONTEXT_BITS; + context.decode_return_number(return_flag, gps_time_changed, streams); - uint_fast16_t dx_k = context.dx_encoders[mgps].prev_k(); + context.set_cpr(gps_time_changed); + context.set_m_l(); - // Y - uint32_t dy_instance = (dx_k < 20) ? (dx_k & (~1u)) : 20; - if (context.number_of_returns == 1) { - dy_instance++; + if (changed_values & LASPP_POINT14_POINT_SOURCE_CHANGED_CONTEXT_BIT) { + int32_t diff = context.point_source_id_encoder.decode_int(streams[LASPP_POINT_SOURCE_LAYER]); + context.point_source_id = + static_cast(static_cast(context.point_source_id) + diff); } - int32_t dy = wrapping_int32_add(context.dy_encoder[dy_instance].decode_int(stream), - context.dy_streamed_median[context.m].get_median()); - context.y = wrapping_int32_add(context.y, dy); - context.dy_streamed_median[context.m].insert(dy); + + if (gps_time_changed) { + GPSTime new_time = context.gps_time_encoder.decode(streams[LASPP_GPS_TIME_LAYER]); + context.gps_time = static_cast(new_time); + } + + if (changed_values & LASPP_POINT14_SCAN_ANGLE_CHANGED_CONTEXT_BIT) { + int16_t prev_scan_angle = context.scan_angle; + int16_t diff = static_cast( + context.scan_angle_encoder[gps_time_changed].decode_int(streams[LASPP_SCAN_ANGLE_LAYER])); + context.scan_angle = prev_scan_angle + diff; + } + + context.decode_classification(streams); + context.decode_coordinates(gps_time_changed, streams); + context.decode_flags(streams); + context.decode_intensity(streams); + context.decode_user_data(streams); return context; } - void encode(OutStream& stream, const LASPointFormat6& point) { - uint_fast16_t d_scanner_channel = - ((point.scanner_channel - m_contexts[m_context].scanner_channel) + 4u) % 4u; - - uint_fast8_t old_cprgps = m_contexts[m_context].cprgps; + private: + inline uint_fast16_t compute_changed_values(const LASPointFormat6& point, bool scanner_changed, + bool point_source_change, bool gps_time_change, + bool scan_angle_change, + uint8_t last_number_of_returns, + uint8_t last_return_number) { uint_fast16_t changed_values = 0; - if (d_scanner_channel) { - changed_values |= (1 << 6); - if (!m_contexts[point.scanner_channel].initialized) { - m_contexts[point.scanner_channel].initialize(m_contexts[m_context]); - } - m_context = static_cast(point.scanner_channel); + if (scanner_changed) { + changed_values |= LASPP_POINT14_SCANNER_CHANNEL_CHANGED_CONTEXT_BIT; } - LASPointFormat6Context& context = m_contexts[m_context]; - if (point.point_source_id != context.point_source_id) { - changed_values |= (1 << 5); + if (point_source_change) { + changed_values |= LASPP_POINT14_POINT_SOURCE_CHANGED_CONTEXT_BIT; } - if (point.gps_time != context.gps_time) { - changed_values |= (1 << 4); + if (gps_time_change) { + changed_values |= LASPP_POINT14_GPS_TIME_CHANGED_CONTEXT_BIT; } - if (point.scan_angle != context.scan_angle) { - changed_values |= (1 << 3); + if (scan_angle_change) { + changed_values |= LASPP_POINT14_SCAN_ANGLE_CHANGED_CONTEXT_BIT; } - if (point.number_of_returns != context.number_of_returns) { - changed_values |= (1 << 2); + if (point.number_of_returns != last_number_of_returns) { + changed_values |= LASPP_POINT14_NUMBER_OF_RETURNS_CHANGED_CONTEXT_BIT; } - if (point.return_number != context.return_number) { - uint_fast16_t d_return_number = (point.return_number - context.return_number + 16u) % 16u; - if (d_return_number == 1) { + uint_fast16_t rn_diff = (point.return_number - last_return_number + 16u) % 16u; + if (rn_diff != 0) { + if (rn_diff == 1) { changed_values |= 0b1; - } else if (d_return_number == 15) { + } else if (rn_diff == 15) { changed_values |= 0b10; } else { changed_values |= 0b11; } } - context.changed_values_encoders[old_cprgps].encode_symbol(stream, changed_values); + return changed_values; + } - if (changed_values) { - if (changed_values & (1 << 6)) { - context.d_scanner_channel_encoder.encode_symbol(stream, d_scanner_channel - 1); - } - if (changed_values & (1 << 2)) { - context.n_returns_encoders[context.number_of_returns].encode_symbol( - stream, point.number_of_returns); - context.number_of_returns = point.number_of_returns; - } - if (changed_values & 0b1 && changed_values & 0b10) { - uint_fast16_t d_return_number = (point.return_number - context.return_number + 16u) % 16u; - context.d_return_number_same_time_encoder.encode_symbol(stream, d_return_number - 2); - context.return_number = point.return_number; - } + inline void handle_encode_scanner_change(LASPointFormat6Context& prev_context, + const LASPointFormat6& point, + uint_fast8_t target_context_idx, + LayerOutStreams& streams) { + uint_fast16_t delta = (point.scanner_channel - prev_context.scanner_channel + 4u) % 4u; + prev_context.d_scanner_channel_encoder.encode_symbol(streams[LASPP_CHANNEL_RETURNS_LAYER], + delta - 1); + if (!m_contexts[target_context_idx].initialized) { + m_contexts[target_context_idx].initialize(prev_context); + m_contexts[target_context_idx].scanner_channel = + static_cast(target_context_idx & 0x3); + } + m_context = target_context_idx; + m_external_context = m_context; + } + + inline LASPointFormat6Context* get_reference_context(LASPointFormat6Context& prev_context, + uint_fast8_t target_context_idx, + bool scanner_changed) { + if (scanner_changed && m_contexts[target_context_idx].initialized) { + return &m_contexts[target_context_idx]; + } + return &prev_context; + } + + inline void encode_metadata_fields(LASPointFormat6Context& context, const LASPointFormat6& point, + LASPointFormat6Context& reference_context, bool gps_changed, + LayerOutStreams& streams) { + bool point_source_change = point.point_source_id != reference_context.point_source_id; + if (point_source_change) { + int32_t diff = static_cast(point.point_source_id) - + static_cast(reference_context.point_source_id); + context.point_source_id_encoder.encode_int(streams[LASPP_POINT_SOURCE_LAYER], diff); + } + context.point_source_id = point.point_source_id; - context.set_m_l(); + bool gps_time_change = point.gps_time != reference_context.gps_time; + if (gps_time_change) { + context.gps_time_encoder.encode(streams[LASPP_GPS_TIME_LAYER], GPSTime(point.gps_time)); + } + context.gps_time = point.gps_time; + + bool scan_angle_change = point.scan_angle != reference_context.scan_angle; + if (scan_angle_change) { + int16_t prev_angle = reference_context.scan_angle; + int16_t next_angle = point.scan_angle; + int32_t diff = static_cast(next_angle) - static_cast(prev_angle); + context.scan_angle_encoder[gps_changed].encode_int(streams[LASPP_SCAN_ANGLE_LAYER], diff); } + context.scan_angle = point.scan_angle; + } - uint32_t mgps = context.m * 2u + static_cast(bool(changed_values & (1 << 4))); + public: + inline void encode(LayerOutStreams& streams, const LASPointFormat6& point) { + LASPointFormat6Context& prev_context = m_contexts[m_context]; + uint_fast8_t target_context_idx = point.scanner_channel; + bool scanner_changed = target_context_idx != m_context; - // X - int32_t dx = wrapping_int32_sub(point.x, context.x); - context.dx_encoders[context.number_of_returns == 1].encode_int( - stream, wrapping_int32_sub(dx, context.dx_streamed_median[mgps].get_median())); - context.dx_streamed_median[mgps].insert(dx); - context.x = point.x; + LASPointFormat6Context* reference_context = + get_reference_context(prev_context, target_context_idx, scanner_changed); - uint_fast16_t dx_k = context.dx_encoders[context.number_of_returns == 1].prev_k(); + bool point_source_change = point.point_source_id != reference_context->point_source_id; + bool gps_time_change = point.gps_time != reference_context->gps_time; + bool scan_angle_change = point.scan_angle != reference_context->scan_angle; + uint8_t last_number_of_returns = reference_context->number_of_returns; + uint8_t last_return_number = reference_context->return_number; - // Y - uint32_t dy_instance = (dx_k < 20) ? (dx_k & (~1u)) : 20; - if (context.number_of_returns == 1) { - dy_instance++; + uint_fast16_t changed_values = + compute_changed_values(point, scanner_changed, point_source_change, gps_time_change, + scan_angle_change, last_number_of_returns, last_return_number); + + prev_context.changed_values_encoders[prev_context.cprgps].encode_symbol( + streams[LASPP_CHANNEL_RETURNS_LAYER], changed_values); + + if (scanner_changed) { + handle_encode_scanner_change(prev_context, point, target_context_idx, streams); + } else { + // Workaround for issue with context setting in V3: + m_external_context = Version == 3 ? 0 : m_context; } - int32_t dy = wrapping_int32_sub(point.y, context.y); - context.dy_encoder[dy_instance].encode_int( - stream, wrapping_int32_sub(dy, context.dy_streamed_median[mgps].get_median())); - context.y = point.y; - context.dy_streamed_median[mgps].insert(dy); + + LASPointFormat6Context& context = m_contexts[m_context]; + context.encode_number_of_returns(changed_values, last_number_of_returns, point, streams); + + uint_fast16_t return_flag = changed_values & LASPP_POINT14_RETURN_NUMBER_CHANGED_CONTEXT_BITS; + uint_fast16_t rn_diff = (point.return_number - last_return_number + 16u) % 16u; + context.encode_return_number(return_flag, last_return_number, rn_diff, gps_time_change, point, + streams); + + bool gps_time_changed = gps_time_change; + context.set_cpr(gps_time_changed); + context.set_m_l(); + + encode_metadata_fields(context, point, *reference_context, gps_time_changed, streams); + context.encode_classification(reference_context->classification, point, streams); + uint8_t last_flags = static_cast((reference_context->edge_of_flight_line << 5) | + (reference_context->scan_direction_flag << 4) | + reference_context->classification_flags); + context.encode_flags(last_flags, point, streams); + context.encode_intensity(point, streams); + context.encode_user_data(reference_context->user_data, point, streams); + context.encode_coordinates(gps_time_changed, point, streams); } }; +using LASPointFormat6Encoder = LASPointFormat6EncoderBase<>; + } // namespace laspp diff --git a/src/laz/rgb12_encoder.hpp b/src/laz/rgb12_encoder.hpp index 3846ad7..4a73e12 100644 --- a/src/laz/rgb12_encoder.hpp +++ b/src/laz/rgb12_encoder.hpp @@ -17,11 +17,10 @@ #pragma once -#include - #include "las_point.hpp" #include "laz/stream.hpp" #include "laz/symbol_encoder.hpp" +#include "utilities/arithmetic.hpp" namespace laspp { class RGB12Encoder { @@ -35,31 +34,63 @@ class RGB12Encoder { SymbolEncoder<256> m_blue_low_encoder; SymbolEncoder<256> m_blue_high_encoder; - static uint8_t clamp(uint8_t value, int delta) { - if (delta + value > 255) { - return 255; - } else if (delta + value < 0) { - return 0; - } - return static_cast(value + delta); - } - public: using EncodedType = ColorData; const EncodedType& last_value() const { return m_last_value; } explicit RGB12Encoder(ColorData initial_color_data) : m_last_value(initial_color_data) {} - ColorData decode(InStream& in_stream) { - uint_fast16_t changed_values = m_changed_values_encoder.decode_symbol(in_stream); - uint8_t red_low = static_cast(m_last_value.red); - uint8_t red_high = static_cast(m_last_value.red >> 8); + private: + void decode_red_channels(InStream& in_stream, uint_fast16_t changed_values, uint8_t& red_low, + uint8_t& red_high) { if (changed_values & 1) { red_low += static_cast(m_red_low_encoder.decode_symbol(in_stream)); } if (changed_values & (1 << 1)) { red_high += static_cast(m_red_high_encoder.decode_symbol(in_stream)); } + } + + void decode_green_blue_channels(InStream& in_stream, uint_fast16_t changed_values, int d_red_low, + int d_red_high) { + uint8_t green_low = static_cast(m_last_value.green); + uint8_t green_high = static_cast(m_last_value.green >> 8); + uint8_t blue_low = static_cast(m_last_value.blue); + uint8_t blue_high = static_cast(m_last_value.blue >> 8); + + if (changed_values & (1 << 2)) { + green_low = clamp(green_low, d_red_low) + + static_cast(m_green_low_encoder.decode_symbol(in_stream)); + } + if (changed_values & (1 << 3)) { + green_high = clamp(green_high, d_red_high) + + static_cast(m_green_high_encoder.decode_symbol(in_stream)); + } + if (changed_values & (1 << 4)) { + int d_green_low = green_low - static_cast(m_last_value.green); + int d = (d_red_low + d_green_low) / 2; + blue_low = + clamp(blue_low, d) + static_cast(m_blue_low_encoder.decode_symbol(in_stream)); + } + if (changed_values & (1 << 5)) { + int d_green_high = green_high - static_cast(m_last_value.green >> 8); + int d = (d_red_high + d_green_high) / 2; + blue_high = + clamp(blue_high, d) + static_cast(m_blue_high_encoder.decode_symbol(in_stream)); + } + + m_last_value.green = green_low | static_cast(green_high << 8); + m_last_value.blue = blue_low | static_cast(blue_high << 8); + } + + public: + ColorData decode(InStream& in_stream) { + uint_fast16_t changed_values = m_changed_values_encoder.decode_symbol(in_stream); + uint8_t red_low = static_cast(m_last_value.red); + uint8_t red_high = static_cast(m_last_value.red >> 8); + + decode_red_channels(in_stream, changed_values, red_low, red_high); + if (changed_values & (1 << 6)) { m_last_value.red = red_low | static_cast(red_high << 8); m_last_value.green = m_last_value.red; @@ -67,50 +98,19 @@ class RGB12Encoder { } else { int d_red_low = red_low - static_cast(m_last_value.red); int d_red_high = red_high - static_cast(m_last_value.red >> 8); - uint8_t green_low = static_cast(m_last_value.green); - uint8_t green_high = static_cast(m_last_value.green >> 8); - uint8_t blue_low = static_cast(m_last_value.blue); - uint8_t blue_high = static_cast(m_last_value.blue >> 8); - if (changed_values & (1 << 2)) { - green_low = clamp(green_low, d_red_low) + - static_cast(m_green_low_encoder.decode_symbol(in_stream)); - } - if (changed_values & (1 << 3)) { - green_high = clamp(green_high, d_red_high) + - static_cast(m_green_high_encoder.decode_symbol(in_stream)); - } - if (changed_values & (1 << 4)) { - int d_green_low = green_low - static_cast(m_last_value.green); - int d = (d_red_low + d_green_low) / 2; - blue_low = - clamp(blue_low, d) + static_cast(m_blue_low_encoder.decode_symbol(in_stream)); - } - if (changed_values & (1 << 5)) { - int d_green_high = green_high - static_cast(m_last_value.green >> 8); - int d = (d_red_high + d_green_high) / 2; - blue_high = clamp(blue_high, d) + - static_cast(m_blue_high_encoder.decode_symbol(in_stream)); - } + decode_green_blue_channels(in_stream, changed_values, d_red_low, d_red_high); m_last_value.red = red_low | static_cast(red_high << 8); - m_last_value.green = green_low | static_cast(green_high << 8); - m_last_value.blue = blue_low | static_cast(blue_high << 8); } return m_last_value; } - void encode(OutStream& out_stream, ColorData color_data) { + private: + uint_fast16_t compute_changed_values(uint8_t red_low, uint8_t red_high, uint8_t green_low, + uint8_t green_high, uint8_t blue_low, uint8_t blue_high, + int d_red_low, int d_red_high) { uint_fast16_t changed_values = 0; - uint8_t red_low = static_cast(color_data.red); - uint8_t red_high = static_cast(color_data.red >> 8); - int d_red_low = red_low - static_cast(m_last_value.red); - int d_red_high = red_high - static_cast(m_last_value.red >> 8); - uint8_t green_low = static_cast(color_data.green); - uint8_t green_high = static_cast(color_data.green >> 8); - uint8_t blue_low = static_cast(color_data.blue); - uint8_t blue_high = static_cast(color_data.blue >> 8); - if (d_red_low != 0) { changed_values |= 1; } @@ -134,6 +134,46 @@ class RGB12Encoder { changed_values |= (1 << 5); } } + return changed_values; + } + + void encode_green_blue_channels(OutStream& out_stream, uint_fast16_t changed_values, + uint8_t green_low, uint8_t green_high, uint8_t blue_low, + uint8_t blue_high, int d_red_low, int d_red_high) { + if (changed_values & (1 << 2)) { + uint8_t base = clamp(static_cast(m_last_value.green), d_red_low); + m_green_low_encoder.encode_symbol(out_stream, static_cast(green_low - base)); + } + if (changed_values & (1 << 3)) { + uint8_t base = clamp(static_cast(m_last_value.green >> 8), d_red_high); + m_green_high_encoder.encode_symbol(out_stream, static_cast(green_high - base)); + } + if (changed_values & (1 << 4)) { + int d = (d_red_low + (green_low - static_cast(m_last_value.green))) / 2; + uint8_t base = clamp(static_cast(m_last_value.blue), d); + m_blue_low_encoder.encode_symbol(out_stream, static_cast(blue_low - base)); + } + if (changed_values & (1 << 5)) { + int d = (d_red_high + (green_high - static_cast(m_last_value.green >> 8))) / 2; + uint8_t base = clamp(static_cast(m_last_value.blue >> 8), d); + m_blue_high_encoder.encode_symbol(out_stream, static_cast(blue_high - base)); + } + } + + public: + void encode(OutStream& out_stream, ColorData color_data) { + uint8_t red_low = static_cast(color_data.red); + uint8_t red_high = static_cast(color_data.red >> 8); + int d_red_low = red_low - static_cast(m_last_value.red); + int d_red_high = red_high - static_cast(m_last_value.red >> 8); + uint8_t green_low = static_cast(color_data.green); + uint8_t green_high = static_cast(color_data.green >> 8); + uint8_t blue_low = static_cast(color_data.blue); + uint8_t blue_high = static_cast(color_data.blue >> 8); + + uint_fast16_t changed_values = compute_changed_values( + red_low, red_high, green_low, green_high, blue_low, blue_high, d_red_low, d_red_high); + m_changed_values_encoder.encode_symbol(out_stream, changed_values); if (changed_values & 1) { m_red_low_encoder.encode_symbol(out_stream, static_cast(d_red_low)); @@ -142,24 +182,8 @@ class RGB12Encoder { m_red_high_encoder.encode_symbol(out_stream, static_cast(d_red_high)); } if (!(changed_values & (1 << 6))) { - if (changed_values & (1 << 2)) { - uint8_t base = clamp(static_cast(m_last_value.green), d_red_low); - m_green_low_encoder.encode_symbol(out_stream, static_cast(green_low - base)); - } - if (changed_values & (1 << 3)) { - uint8_t base = clamp(static_cast(m_last_value.green >> 8), d_red_high); - m_green_high_encoder.encode_symbol(out_stream, static_cast(green_high - base)); - } - if (changed_values & (1 << 4)) { - int d = (d_red_low + (green_low - static_cast(m_last_value.green))) / 2; - uint8_t base = clamp(static_cast(m_last_value.blue), d); - m_blue_low_encoder.encode_symbol(out_stream, static_cast(blue_low - base)); - } - if (changed_values & (1 << 5)) { - int d = (d_red_high + (green_high - static_cast(m_last_value.green >> 8))) / 2; - uint8_t base = clamp(static_cast(m_last_value.blue >> 8), d); - m_blue_high_encoder.encode_symbol(out_stream, static_cast(blue_high - base)); - } + encode_green_blue_channels(out_stream, changed_values, green_low, green_high, blue_low, + blue_high, d_red_low, d_red_high); } m_last_value = color_data; } diff --git a/src/laz/rgb14_encoder.hpp b/src/laz/rgb14_encoder.hpp new file mode 100644 index 0000000..06191c1 --- /dev/null +++ b/src/laz/rgb14_encoder.hpp @@ -0,0 +1,255 @@ +/* + * SPDX-FileCopyrightText: (c) 2025 Trailblaze Software, all rights reserved + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * For LGPL2 incompatible licensing or development requests, please contact + * trailblaze.software@gmail.com + */ + +#pragma once + +#include +#include + +#include "las_point.hpp" +#include "laz/layered_stream.hpp" +#include "laz/stream.hpp" +#include "laz/symbol_encoder.hpp" +#include "utilities/arithmetic.hpp" + +namespace laspp { + +class RGB14Encoder { + struct Context { + bool initialized; + ColorData last_value; + SymbolEncoder<1 << 7> changed_values_encoder; + SymbolEncoder<256> red_low_encoder; + SymbolEncoder<256> red_high_encoder; + SymbolEncoder<256> green_low_encoder; + SymbolEncoder<256> green_high_encoder; + SymbolEncoder<256> blue_low_encoder; + SymbolEncoder<256> blue_high_encoder; + + Context() : initialized(false) {} + }; + + std::array m_contexts; + uint8_t m_active_context; + uint8_t m_last_value_context; + + Context& ensure_context(uint8_t idx) { + if (!m_contexts[idx].initialized) { + LASPP_ASSERT(m_contexts[m_active_context].initialized, + "Cannot switch to uninitialized context when no initialized context exists."); + m_contexts[idx].last_value = m_contexts[m_active_context].last_value; + m_contexts[idx].initialized = true; + } + m_active_context = idx; + return m_contexts[idx]; + } + + public: + static constexpr int NUM_LAYERS = 1; + + using EncodedType = ColorData; + + const EncodedType& last_value() const { return m_contexts[m_last_value_context].last_value; } + + explicit RGB14Encoder(ColorData initial_color_data, uint8_t context) : m_active_context(context) { + m_contexts[context].last_value = initial_color_data; + m_contexts[context].initialized = true; + m_last_value_context = context; + } + + private: + void decode_red_channels(Context& context, InStream& in_stream, uint_fast16_t changed_values, + uint8_t& red_low, uint8_t& red_high) { + if (changed_values & 1) { + uint8_t delta = static_cast(context.red_low_encoder.decode_symbol(in_stream)); + red_low = static_cast(red_low + delta); + } + if (changed_values & (1 << 1)) { + uint8_t delta = static_cast(context.red_high_encoder.decode_symbol(in_stream)); + red_high = static_cast(red_high + delta); + } + } + + void decode_green_blue_channels(Context& context, InStream& in_stream, + uint_fast16_t changed_values, ColorData& last_value, + uint8_t red_low, uint8_t red_high) { + uint8_t green_low = static_cast(last_value.green); + uint8_t green_high = static_cast(last_value.green >> 8); + uint8_t blue_low = static_cast(last_value.blue); + uint8_t blue_high = static_cast(last_value.blue >> 8); + + int d_red_low = red_low - static_cast(last_value.red); + int d_red_high = red_high - static_cast(last_value.red >> 8); + + if (changed_values & (1 << 2)) { + uint8_t base = clamp(green_low, d_red_low); + uint8_t delta = static_cast(context.green_low_encoder.decode_symbol(in_stream)); + green_low = static_cast((base + delta) & 0xFF); + } + if (changed_values & (1 << 4)) { + int d_green_low = green_low - static_cast(last_value.green); + int d = (d_red_low + d_green_low) / 2; + uint8_t base = clamp(blue_low, d); + uint8_t delta = static_cast(context.blue_low_encoder.decode_symbol(in_stream)); + blue_low = static_cast((base + delta) & 0xFF); + } + if (changed_values & (1 << 3)) { + uint8_t base = clamp(green_high, d_red_high); + uint8_t delta = static_cast(context.green_high_encoder.decode_symbol(in_stream)); + green_high = static_cast((base + delta) & 0xFF); + } + if (changed_values & (1 << 5)) { + int d_green_high = green_high - static_cast(last_value.green >> 8); + int d = (d_red_high + d_green_high) / 2; + uint8_t base = clamp(blue_high, d); + uint8_t delta = static_cast(context.blue_high_encoder.decode_symbol(in_stream)); + blue_high = static_cast((base + delta) & 0xFF); + } + + last_value.green = static_cast( + (static_cast(green_low) | (static_cast(green_high) << 8))); + last_value.blue = static_cast( + (static_cast(blue_low) | (static_cast(blue_high) << 8))); + } + + public: + ColorData decode(LayeredInStreams& in_streams, uint8_t context_idx) { + InStream& in_stream = in_streams[0]; + // Yet another cursed bug in LASzip where the last item + // is not switched to the new context + m_last_value_context = m_contexts[context_idx].initialized ? m_active_context : context_idx; + ColorData& last_value = m_contexts[m_last_value_context].last_value; + Context& context = ensure_context(context_idx); + + uint_fast16_t changed_values = context.changed_values_encoder.decode_symbol(in_stream); + + uint8_t red_low = static_cast(last_value.red); + uint8_t red_high = static_cast(last_value.red >> 8); + + decode_red_channels(context, in_stream, changed_values, red_low, red_high); + + if (changed_values & (1 << 6)) { + decode_green_blue_channels(context, in_stream, changed_values, last_value, red_low, red_high); + last_value.red = static_cast( + (static_cast(red_low) | (static_cast(red_high) << 8))); + } else { + last_value.red = static_cast( + (static_cast(red_low) | (static_cast(red_high) << 8))); + last_value.green = last_value.red; + last_value.blue = last_value.red; + } + + return last_value; + } + + private: + uint_fast16_t compute_changed_values(const ColorData& last_value, uint8_t red_low, + uint8_t red_high, uint8_t green_low, uint8_t green_high, + uint8_t blue_low, uint8_t blue_high, int d_red_low, + int d_red_high) { + uint_fast16_t changed_values = 0; + + if (d_red_low != 0) { + changed_values |= 1; + } + if (d_red_high != 0) { + changed_values |= (1 << 1); + } + if (green_low != red_low || green_high != red_high || blue_low != red_low || + blue_high != red_high) { + changed_values |= (1 << 6); + if (green_low != static_cast(last_value.green)) { + changed_values |= (1 << 2); + } + if (green_high != static_cast(last_value.green >> 8)) { + changed_values |= (1 << 3); + } + if (blue_low != static_cast(last_value.blue)) { + changed_values |= (1 << 4); + } + if (blue_high != static_cast(last_value.blue >> 8)) { + changed_values |= (1 << 5); + } + } + return changed_values; + } + + void encode_green_blue_channels(Context& context, OutStream& out_stream, + uint_fast16_t changed_values, const ColorData& last_value, + uint8_t green_low, uint8_t green_high, uint8_t blue_low, + uint8_t blue_high, int d_red_low, int d_red_high) { + if (changed_values & (1 << 2)) { + uint8_t base = clamp(static_cast(last_value.green), d_red_low); + context.green_low_encoder.encode_symbol(out_stream, static_cast(green_low - base)); + } + if (changed_values & (1 << 4)) { + int d = (d_red_low + (green_low - static_cast(last_value.green))) / 2; + uint8_t base = clamp(static_cast(last_value.blue), d); + context.blue_low_encoder.encode_symbol(out_stream, static_cast(blue_low - base)); + } + if (changed_values & (1 << 3)) { + uint8_t base = clamp(static_cast(last_value.green >> 8), d_red_high); + context.green_high_encoder.encode_symbol(out_stream, static_cast(green_high - base)); + } + if (changed_values & (1 << 5)) { + int d = (d_red_high + (green_high - static_cast(last_value.green >> 8))) / 2; + uint8_t base = clamp(static_cast(last_value.blue >> 8), d); + context.blue_high_encoder.encode_symbol(out_stream, static_cast(blue_high - base)); + } + } + + public: + void encode(LayeredOutStreams& out_streams, ColorData color_data, + uint8_t context_idx) { + OutStream& out_stream = out_streams[0]; + // Yet another cursed bug in LASzip where the last item + // is not switched to the new context + ColorData& last_value = m_contexts[context_idx].initialized + ? m_contexts[m_active_context].last_value + : m_contexts[context_idx].last_value; + Context& context = ensure_context(context_idx); + + uint8_t red_low = static_cast(color_data.red); + uint8_t red_high = static_cast(color_data.red >> 8); + int d_red_low = red_low - static_cast(last_value.red); + int d_red_high = red_high - static_cast(last_value.red >> 8); + uint8_t green_low = static_cast(color_data.green); + uint8_t green_high = static_cast(color_data.green >> 8); + uint8_t blue_low = static_cast(color_data.blue); + uint8_t blue_high = static_cast(color_data.blue >> 8); + + uint_fast16_t changed_values = + compute_changed_values(last_value, red_low, red_high, green_low, green_high, blue_low, + blue_high, d_red_low, d_red_high); + + context.changed_values_encoder.encode_symbol(out_stream, changed_values); + if (changed_values & 1) { + context.red_low_encoder.encode_symbol(out_stream, static_cast(d_red_low)); + } + if (changed_values & (1 << 1)) { + context.red_high_encoder.encode_symbol(out_stream, static_cast(d_red_high)); + } + if (changed_values & (1 << 6)) { + encode_green_blue_channels(context, out_stream, changed_values, last_value, green_low, + green_high, blue_low, blue_high, d_red_low, d_red_high); + } + + last_value = color_data; + } +}; + +} // namespace laspp diff --git a/src/laz/stream.hpp b/src/laz/stream.hpp index a867ada..5fcab86 100644 --- a/src/laz/stream.hpp +++ b/src/laz/stream.hpp @@ -234,6 +234,7 @@ class InStream : StreamVariables { class OutStream : StreamVariables { std::iostream& m_stream; + bool m_finalized; void finalize_stream() { bool write_two_bytes; @@ -260,12 +261,19 @@ class OutStream : StreamVariables { } public: - explicit OutStream(std::iostream& stream) : m_stream(stream) { + explicit OutStream(std::iostream& stream) : m_stream(stream), m_finalized(false) { m_base = 0; m_length = std::numeric_limits::max(); } - ~OutStream() { finalize_stream(); } + ~OutStream() { finalize(); } + + void finalize() { + if (!m_finalized) { + finalize_stream(); + m_finalized = true; + } + } uint32_t length() const { return m_length; } @@ -290,6 +298,7 @@ class OutStream : StreamVariables { } void update_range(uint32_t lower, uint32_t upper) { + LASPP_ASSERT(!m_finalized); if ((static_cast(m_base) + static_cast(lower)) >= (static_cast(1) << 32)) { propogate_carry(); diff --git a/src/laz/tests/test_gpstime11_encoder.cpp b/src/laz/tests/test_gpstime11_encoder.cpp index fd2e95d..7399145 100644 --- a/src/laz/tests/test_gpstime11_encoder.cpp +++ b/src/laz/tests/test_gpstime11_encoder.cpp @@ -20,28 +20,31 @@ #include "laz/gpstime11_encoder.hpp" #include "utilities/assert.hpp" -int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { +template +void run_gpstime_test() { { std::stringstream encoded_stream; { laspp::OutStream ostream(encoded_stream); - laspp::GPSTime11Encoder encoder(laspp::GPSTime(0)); + GPSTimeEncoder encoder(laspp::GPSTime(0)); encoder.encode(ostream, laspp::GPSTime(0.0)); encoder.encode(ostream, laspp::GPSTime(1.0)); encoder.encode(ostream, laspp::GPSTime(2.0)); + encoder.encode(ostream, laspp::GPSTime(3.0)); encoder.encode(ostream, laspp::GPSTime(1.0)); encoder.encode(ostream, laspp::GPSTime(0.0)); encoder.encode(ostream, laspp::GPSTime(2.0)); } - LASPP_ASSERT_EQ(encoded_stream.str().size(), 22); + LASPP_ASSERT_EQ(encoded_stream.str().size(), GPSTimeEncoder::Point14Mode ? 32 : 29); { laspp::InStream instream(encoded_stream); - laspp::GPSTime11Encoder encoder(laspp::GPSTime(0)); + GPSTimeEncoder encoder(laspp::GPSTime(0)); LASPP_ASSERT_EQ(encoder.decode(instream), 0.0); LASPP_ASSERT_EQ(encoder.decode(instream), 1.0); LASPP_ASSERT_EQ(encoder.decode(instream), 2.0); + LASPP_ASSERT_EQ(encoder.decode(instream), 3.0); LASPP_ASSERT_EQ(encoder.decode(instream), 1.0); LASPP_ASSERT_EQ(encoder.decode(instream), 0.0); LASPP_ASSERT_EQ(encoder.decode(instream), 2.0); @@ -66,7 +69,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { values[1][i] = static_cast(i % 5); } } - compressed_sizes.push_back(6895); + compressed_sizes.push_back(GPSTimeEncoder::Point14Mode ? 6894 : 6895); values.emplace_back(1000); for (size_t i = 0; i < values.back().size(); i++) { @@ -76,7 +79,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { values[2][i] = 0; } } - compressed_sizes.push_back(900); + compressed_sizes.push_back(GPSTimeEncoder::Point14Mode ? 934 : 900); values.emplace_back(1000); for (size_t i = 0; i < values.back().size(); i++) { @@ -90,7 +93,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { values[3][i] = 15666123123123123123. * static_cast(i) + 1e34; } } - compressed_sizes.push_back(1102); + compressed_sizes.push_back(GPSTimeEncoder::Point14Mode ? 1199 : 1102); values.emplace_back(); { @@ -114,7 +117,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { std::stringstream encoded_stream; { laspp::OutStream ostream(encoded_stream); - laspp::GPSTime11Encoder encoder(laspp::GPSTime(12)); + GPSTimeEncoder encoder(laspp::GPSTime(12)); for (auto value : vec) { encoder.encode(ostream, laspp::GPSTime(value)); } @@ -124,7 +127,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { { laspp::InStream instream(encoded_stream); - laspp::GPSTime11Encoder encoder(laspp::GPSTime(12)); + GPSTimeEncoder encoder(laspp::GPSTime(12)); LASPP_ASSERT_EQ(encoder.last_value(), laspp::GPSTime(12)); for (auto value : vec) { LASPP_ASSERT_EQ(encoder.decode(instream), value); @@ -133,6 +136,10 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { } } } +} +int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { + run_gpstime_test(); + run_gpstime_test>(); return 0; } diff --git a/src/laz/tests/test_laszip_interop.cpp b/src/laz/tests/test_laszip_interop.cpp new file mode 100644 index 0000000..7c6c405 --- /dev/null +++ b/src/laz/tests/test_laszip_interop.cpp @@ -0,0 +1,629 @@ +/* + * SPDX-FileCopyrightText: (c) 2025 Trailblaze Software, all rights reserved + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * For LGPL2 incompatible licensing or development requests, please contact + * trailblaze.software@gmail.com + */ + +// Windows compatibility: prevent min/max macros and byte typedef conflicts +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#endif + +#include + +// Undefine Windows byte typedef if it exists to avoid conflicts with std::byte +#ifdef _WIN32 +#ifdef byte +#undef byte +#endif +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "las_point.hpp" +#include "las_reader.hpp" +#include "las_writer.hpp" +#include "laszipper.hpp" +#include "laz/laz_reader.hpp" +#include "laz/laz_vlr.hpp" +#include "utilities/assert.hpp" + +using namespace laspp; + +namespace { + +constexpr laszip_U32 ChunkSize = 204; + +template +std::vector generate_random_points(size_t n_points, uint32_t seed = 42) { + std::vector points; + points.reserve(n_points); + std::mt19937_64 gen(seed); + for (size_t i = 0; i < n_points; i++) { + points.emplace_back(PointT::RandomData(gen)); + } + return points; +} + +template +void populate_laszip_format0_fields(const PointT& point, laszip_point& las_point) { + unsigned char return_num = static_cast(point.bit_byte.return_number); + unsigned char num_returns = static_cast(point.bit_byte.number_of_returns); + las_point.return_number = return_num & 0x7; + las_point.number_of_returns = num_returns & 0x7; + las_point.scan_direction_flag = point.bit_byte.scan_direction_flag; + las_point.edge_of_flight_line = point.bit_byte.edge_of_flight_line; + las_point.classification = static_cast(point.classification_byte.classification); + las_point.synthetic_flag = point.classification_byte.synthetic; + las_point.keypoint_flag = point.classification_byte.key_point; + las_point.withheld_flag = point.classification_byte.withheld; + las_point.scan_angle_rank = static_cast(point.scan_angle_rank); + las_point.user_data = point.user_data; + las_point.point_source_ID = point.point_source_id; + las_point.extended_point_type = 0; +} + +template +void populate_laszip_format6_fields(const PointT& point, laszip_point& las_point) { + las_point.return_number = static_cast(std::min(point.return_number, 7)) & 0x7; + las_point.number_of_returns = + static_cast(std::min(point.number_of_returns, 7)) & 0x7; + las_point.scan_direction_flag = point.scan_direction_flag; + las_point.edge_of_flight_line = point.edge_of_flight_line; + las_point.synthetic_flag = point.classification_flags & 0x1; + las_point.keypoint_flag = (point.classification_flags >> 1) & 0x1; + las_point.withheld_flag = (point.classification_flags >> 2) & 0x1; + las_point.extended_point_type = 1; + las_point.extended_classification_flags = point.classification_flags; + las_point.extended_classification = static_cast(point.classification); + las_point.extended_return_number = point.return_number; + las_point.extended_number_of_returns = point.number_of_returns; + las_point.extended_scanner_channel = point.scanner_channel; + las_point.extended_scan_angle = static_cast(point.scan_angle); + las_point.scan_angle_rank = + static_cast(std::clamp(static_cast(point.scan_angle) / 512, -128, 127)); + las_point.classification = static_cast(point.classification) & 0x1F; + las_point.user_data = point.user_data; + las_point.point_source_ID = point.point_source_id; + las_point.gps_time = point.gps_time; +} + +template +void populate_laszip_point(const PointT& point, laszip_point& las_point) { + las_point.X = point.x; + las_point.Y = point.y; + las_point.Z = point.z; + las_point.intensity = point.intensity; + las_point.extra_bytes = nullptr; + + if constexpr (std::is_base_of_v) { + populate_laszip_format0_fields(point, las_point); + } + + if constexpr (std::is_base_of_v) { + las_point.gps_time = static_cast(static_cast(point)); + } + + if constexpr (std::is_base_of_v) { + populate_laszip_format6_fields(point, las_point); + } + + if constexpr (std::is_base_of_v) { + las_point.rgb[0] = point.red; + las_point.rgb[1] = point.green; + las_point.rgb[2] = point.blue; + } +} + +// Conversion functions from laszip_point to PointT +LASPointFormat0 convert_laszip_to_format0(const laszip_point& laszip_pt) { + LASPointFormat0 pt; + pt.x = laszip_pt.X; + pt.y = laszip_pt.Y; + pt.z = laszip_pt.Z; + pt.intensity = laszip_pt.intensity; + pt.bit_byte.return_number = static_cast(laszip_pt.return_number & 0x07); + pt.bit_byte.number_of_returns = static_cast(laszip_pt.number_of_returns & 0x07); + pt.bit_byte.scan_direction_flag = static_cast(laszip_pt.scan_direction_flag); + pt.bit_byte.edge_of_flight_line = static_cast(laszip_pt.edge_of_flight_line); + pt.classification_byte.classification = + static_cast(laszip_pt.classification & 0x1F); + pt.classification_byte.synthetic = static_cast(laszip_pt.synthetic_flag); + pt.classification_byte.key_point = static_cast(laszip_pt.keypoint_flag); + pt.classification_byte.withheld = static_cast(laszip_pt.withheld_flag); + pt.scan_angle_rank = static_cast(static_cast(laszip_pt.scan_angle_rank)); + pt.user_data = laszip_pt.user_data; + pt.point_source_id = laszip_pt.point_source_ID; + return pt; +} + +LASPointFormat1 convert_laszip_to_format1(const laszip_point& laszip_pt) { + LASPointFormat1 pt; + static_cast(pt) = convert_laszip_to_format0(laszip_pt); + pt.gps_time.f64 = laszip_pt.gps_time; + return pt; +} + +LASPointFormat6 convert_laszip_to_format6(const laszip_point& laszip_pt) { + LASPointFormat6 pt; + pt.x = laszip_pt.X; + pt.y = laszip_pt.Y; + pt.z = laszip_pt.Z; + pt.intensity = laszip_pt.intensity; + pt.return_number = static_cast(laszip_pt.extended_return_number & 0x0F); + pt.number_of_returns = static_cast(laszip_pt.extended_number_of_returns & 0x0F); + pt.classification_flags = static_cast(laszip_pt.extended_classification_flags & 0x0F); + pt.scanner_channel = static_cast(laszip_pt.extended_scanner_channel & 0x03); + pt.scan_direction_flag = static_cast(laszip_pt.scan_direction_flag); + pt.edge_of_flight_line = static_cast(laszip_pt.edge_of_flight_line); + pt.classification = static_cast(laszip_pt.extended_classification); + pt.user_data = laszip_pt.user_data; + pt.scan_angle = laszip_pt.extended_scan_angle; + pt.point_source_id = laszip_pt.point_source_ID; + pt.gps_time = laszip_pt.gps_time; + return pt; +} + +LASPointFormat7 convert_laszip_to_format7(const laszip_point& laszip_pt) { + LASPointFormat7 pt; + static_cast(pt) = convert_laszip_to_format6(laszip_pt); + pt.red = laszip_pt.rgb[0]; + pt.green = laszip_pt.rgb[1]; + pt.blue = laszip_pt.rgb[2]; + return pt; +} + +template +PointT convert_laszip_point(const laszip_point& laszip_pt) { + if constexpr (std::is_same_v) { + return convert_laszip_to_format0(laszip_pt); + } else if constexpr (std::is_same_v) { + return convert_laszip_to_format1(laszip_pt); + } else if constexpr (std::is_same_v) { + return convert_laszip_to_format6(laszip_pt); + } else if constexpr (std::is_same_v) { + return convert_laszip_to_format7(laszip_pt); + } else { + LASPP_FAIL("Unsupported point format for conversion"); + } +} + +template +void populate_header_basic_fields(laszip_header& header, laszip_U64 total_points) { + header.version_major = 1; + header.version_minor = PointT::MinVersion; + header.header_size = (header.version_minor >= 4) ? 375 : 227; + header.offset_to_point_data = header.header_size; + header.point_data_format = PointT::PointFormat; + header.point_data_record_length = static_cast(sizeof(PointT)); + + header.number_of_point_records = static_cast( + std::min(total_points, std::numeric_limits::max())); + header.extended_number_of_point_records = total_points; + + std::fill(std::begin(header.number_of_points_by_return), + std::end(header.number_of_points_by_return), 0); + std::fill(std::begin(header.extended_number_of_points_by_return), + std::end(header.extended_number_of_points_by_return), 0); + + const double scale = 0.27; + header.x_scale_factor = scale; + header.y_scale_factor = scale; + header.z_scale_factor = scale; + header.x_offset = 146; + header.y_offset = -25.4; + header.z_offset = 512; +} + +template +void populate_header_bounds(const std::vector& points, laszip_header& header) { + int32_t min_x = std::numeric_limits::max(); + int32_t max_x = std::numeric_limits::lowest(); + int32_t min_y = min_x; + int32_t max_y = max_x; + int32_t min_z = min_x; + int32_t max_z = max_x; + for (const PointT& point : points) { + const int32_t x = point.x; + const int32_t y = point.y; + const int32_t z = point.z; + min_x = std::min(min_x, x); + min_y = std::min(min_y, y); + min_z = std::min(min_z, z); + max_x = std::max(max_x, x); + max_y = std::max(max_y, y); + max_z = std::max(max_z, z); + + auto increment_return_counters = [&](uint8_t return_number, uint8_t number_of_returns) { + if (return_number == 0 || number_of_returns == 0) { + return; + } + uint8_t basic_index = std::min(return_number, 5); + header.number_of_points_by_return[basic_index - 1]++; + uint8_t extended_index = std::min(return_number, 15); + header.extended_number_of_points_by_return[extended_index - 1]++; + }; + + if constexpr (std::is_base_of_v) { + increment_return_counters(point.bit_byte.return_number, point.bit_byte.number_of_returns); + } + if constexpr (std::is_base_of_v) { + increment_return_counters(point.return_number, point.number_of_returns); + } + } + header.min_x = header.x_offset + min_x * header.x_scale_factor; + header.max_x = header.x_offset + max_x * header.x_scale_factor; + header.min_y = header.y_offset + min_y * header.y_scale_factor; + header.max_y = header.y_offset + max_y * header.y_scale_factor; + header.min_z = header.z_offset + min_z * header.z_scale_factor; + header.max_z = header.z_offset + max_z * header.z_scale_factor; +} + +template +void populate_header(const std::vector& points, laszip_header& header) { + std::memset(&header, 0, sizeof(laszip_header)); + laszip_U64 total_points = static_cast(points.size()); + populate_header_basic_fields(header, total_points); + populate_header_bounds(points, header); + + std::string system_identifier = "laspp-tests"; + std::string generating_software = "laspp-tests"; + std::snprintf(header.system_identifier, sizeof(header.system_identifier), "%s", + system_identifier.c_str()); + std::snprintf(header.generating_software, sizeof(header.generating_software), "%s", + generating_software.c_str()); +} + +class TempFile { + public: + explicit TempFile(const std::string& prefix) { + auto base_dir = std::filesystem::temp_directory_path() / "laspp_laszip_tests"; + std::filesystem::create_directories(base_dir); + auto timestamp = std::chrono::steady_clock::now().time_since_epoch().count(); + path_ = base_dir / (prefix + "_" + std::to_string(timestamp) + ".laz"); + } + + ~TempFile() { + std::error_code ec; + std::filesystem::remove(path_, ec); + } + + const std::filesystem::path& path() const { return path_; } + + private: + std::filesystem::path path_; +}; + +template +void write_points_with_laszip(const std::vector& points, const std::filesystem::path& path, + bool request_native_extension) { + laszip_POINTER writer; + LASPP_ASSERT_EQ(laszip_create(&writer), 0); + + // Configure native extension or compatibility mode + // Note: compatibility mode (format 6+ stored as format 0-5 + + // extra bytes) is not currently supported by the LAS++ reader, + // which only supports native extension mode + if (request_native_extension) { + LASPP_ASSERT_EQ(laszip_request_native_extension(writer, 1), 0); + } else if constexpr (PointT::PointFormat > 5) { + LASPP_ASSERT_EQ(laszip_request_compatibility_mode(writer, 1), 0); + } + + laszip_header* header; + LASPP_ASSERT_EQ(laszip_get_header_pointer(writer, &header), 0); + populate_header(points, *header); + LASPP_ASSERT_EQ(laszip_set_header(writer, header), 0); + + LASPP_ASSERT_EQ(laszip_set_chunk_size(writer, ChunkSize), 0); + + LASPP_ASSERT_EQ(laszip_open_writer(writer, path.string().c_str(), 1), 0); + + laszip_point* las_point; + LASPP_ASSERT_EQ(laszip_get_point_pointer(writer, &las_point), 0); + + for (size_t idx = 0; idx < points.size(); idx++) { + const PointT& point = points[idx]; + populate_laszip_point(point, *las_point); + LASPP_ASSERT_EQ(laszip_set_point(writer, las_point), 0); + LASPP_ASSERT_EQ(laszip_write_point(writer), 0, "Failed to write point ", idx); + } + + LASPP_ASSERT_EQ(laszip_close_writer(writer), 0); + LASPP_ASSERT_EQ(laszip_destroy(writer), 0); +} + +// Write points with LAS++ writer +template +void write_points_with_laspp(const std::vector& points, const std::filesystem::path& path) { + std::fstream file(path, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc); + LASPP_ASSERT(file.is_open(), "Failed to open file for writing"); + + // Enable LAZ compression by setting bit 128 in the point format + uint8_t compressed_point_format = PointT::PointFormat | 128; + LASWriter writer(file, compressed_point_format); + + // Verify compression is enabled + LASPP_ASSERT(writer.header().is_laz_compressed(), "File should be LAZ compressed"); + + // Set scale factors and offsets (LASWriter handles point counts and bounds automatically) + const double scale = 0.27; + writer.header().transform().m_scale_factors.x() = scale; + writer.header().transform().m_scale_factors.y() = scale; + writer.header().transform().m_scale_factors.z() = scale; + writer.header().transform().m_offsets.x() = 146; + writer.header().transform().m_offsets.y() = -25.4; + writer.header().transform().m_offsets.z() = 512; + + writer.write_points(std::span(points.data(), points.size()), ChunkSize); +} + +// Read points with laszip API +template +std::vector read_points_with_laszip(const std::filesystem::path& path) { + laszip_POINTER reader; + LASPP_ASSERT_EQ(laszip_create(&reader), 0, "Failed to create laszip reader"); + + laszip_BOOL is_compressed = 0; + LASPP_ASSERT_EQ(laszip_open_reader(reader, path.string().c_str(), &is_compressed), 0, + "Failed to open file with laszip"); + + laszip_header* header; + LASPP_ASSERT_EQ(laszip_get_header_pointer(reader, &header), 0, "Failed to get laszip header"); + + laszip_U64 num_points = header->extended_number_of_point_records; + if (num_points == 0) { + num_points = header->number_of_point_records; + } + + laszip_point* las_point; + LASPP_ASSERT_EQ(laszip_get_point_pointer(reader, &las_point), 0, + "Failed to get laszip point pointer"); + + // Verify point format matches what we expect + LASPP_ASSERT_EQ(header->point_data_format, PointT::PointFormat, + "Point format mismatch: header says ", header->point_data_format, + " but expected ", PointT::PointFormat); + + std::vector decoded_points; + decoded_points.reserve(num_points); + + for (laszip_U64 i = 0; i < num_points; ++i) { + laszip_I32 error = laszip_read_point(reader); + LASPP_ASSERT_EQ(error, 0, "Failed to read point ", i, " laszip error: ", error); + + decoded_points.push_back(convert_laszip_point(*las_point)); + } + + LASPP_ASSERT_EQ(laszip_close_reader(reader), 0, "Failed to close laszip reader"); + LASPP_ASSERT_EQ(laszip_destroy(reader), 0, "Failed to destroy laszip reader"); + + return decoded_points; +} + +// Test roundtrip: write with LASzip, read with LAS++ reader +template +void run_laszip_file_roundtrip(size_t n_points, bool request_native_extension) { + auto points = generate_random_points(n_points); + TempFile temp_file("laszip_roundtrip"); + + write_points_with_laszip(points, temp_file.path(), request_native_extension); + + // Read with LAS++ reader + std::ifstream laz_file(temp_file.path(), std::ios::binary); + LASPP_ASSERT(laz_file.is_open(), "Failed to open file for reading"); + LASReader reader(laz_file); + + LASPP_ASSERT_EQ(reader.header().num_points(), points.size(), + "Point count mismatch in LAS++ reader header"); + + std::vector decoded(points.size()); + auto decoded_span = reader.read_chunks(std::span(decoded), {0, reader.num_chunks()}); + LASPP_ASSERT_EQ(decoded_span.size(), points.size(), "Decoded point count mismatch"); + + // Verify all points match + for (size_t i = 0; i < points.size(); ++i) { + LASPP_ASSERT_EQ(decoded_span[i], points[i], "Point ", i, " mismatch in roundtrip test"); + } +} + +// Test roundtrip: write with LAS++, read with laszip +template +void run_laspp_file_roundtrip(size_t n_points) { + auto points = generate_random_points(n_points); + TempFile temp_file("laspp_roundtrip"); + + write_points_with_laspp(points, temp_file.path()); + + // First verify the file was written correctly by reading with LAS++ + { + std::ifstream laz_file(temp_file.path(), std::ios::binary); + LASPP_ASSERT(laz_file.is_open(), "Failed to open file for reading with LAS++"); + LASReader reader(laz_file); + LASPP_ASSERT_EQ(reader.header().num_points(), points.size(), + "Point count mismatch in LAS++ reader header"); + std::vector laspp_decoded(points.size()); + auto laspp_decoded_span = + reader.read_chunks(std::span(laspp_decoded), {0, reader.num_chunks()}); + LASPP_ASSERT_EQ(laspp_decoded_span.size(), points.size(), "LAS++ decoded point count mismatch"); + + // Verify LAS++ can read its own file + for (size_t i = 0; i < points.size(); ++i) { + LASPP_ASSERT_EQ(laspp_decoded_span[i], points[i], "Point ", i, + " mismatch when LAS++ reads its own file"); + } + } + + // Now read with laszip + std::vector decoded_points = read_points_with_laszip(temp_file.path()); + + LASPP_ASSERT_EQ(decoded_points.size(), points.size(), "Decoded point count mismatch"); + + // Verify all points match + for (size_t i = 0; i < points.size(); ++i) { + LASPP_ASSERT_EQ(decoded_points[i], points[i], "Point ", i, " mismatch in LAS++ roundtrip test"); + } +} + +// Test roundtrip using LAS++ internal compression (no file I/O) +template +void run_laszip_internal_roundtrip(size_t n_points) { + auto points = generate_random_points(n_points); + + LASzip laszip; + LASPP_ASSERT(laszip.setup( + PointT::PointFormat, sizeof(PointT), + PointT::PointFormat >= 6 ? LASZIP_COMPRESSOR_LAYERED_CHUNKED : LASZIP_COMPRESSOR_CHUNKED)); + + unsigned char* vlr_bytes = nullptr; + int vlr_size = 0; + LASPP_ASSERT(laszip.pack(vlr_bytes, vlr_size)); + + std::stringstream compressed_stream(std::ios::in | std::ios::out | std::ios::binary); + LASzipper zipper; + LASPP_ASSERT(zipper.open(compressed_stream, &laszip)); + + if (PointT::PointFormat < 6) { + PointT point_buffer{}; + std::vector point_items(laszip.num_items); + size_t offset = 0; + for (unsigned int i = 0; i < laszip.num_items; i++) { + point_items[i] = reinterpret_cast(&point_buffer) + offset; + offset += laszip.items[i].size; + } + LASPP_ASSERT_EQ(offset, sizeof(PointT)); + for (const PointT& point : points) { + std::memcpy(&point_buffer, &point, sizeof(point_buffer)); + LASPP_ASSERT(zipper.write(point_items.data())); + } + } else { + laszip_point laszip_point_instance; + std::vector point_items(laszip.num_items); + for (unsigned int i = 0; i < laszip.num_items; i++) { + if (laszip.items[i].type == LASitem::POINT14) { + point_items[i] = reinterpret_cast(&laszip_point_instance); + continue; + } + if constexpr (std::is_base_of_v) { + if (laszip.items[i].type == LASitem::RGB14) { + point_items[i] = reinterpret_cast(&laszip_point_instance.rgb); + continue; + } + } + LASPP_FAIL("Unexpected item type in LASzip items for point format >= 6"); + } + for (const PointT& point : points) { + populate_laszip_point(point, laszip_point_instance); + LASPP_ASSERT(zipper.write(point_items.data())); + } + } + LASPP_ASSERT(zipper.close()); + + std::string compressed = compressed_stream.str(); + LASPP_ASSERT_GE(compressed.size(), sizeof(uint64_t)); + + uint64_t chunk_table_offset = 0; + std::memcpy(&chunk_table_offset, compressed.data(), sizeof(chunk_table_offset)); + LASPP_ASSERT(chunk_table_offset >= sizeof(uint64_t)); + LASPP_ASSERT_LE(chunk_table_offset, compressed.size()); + + std::vector chunk_data(chunk_table_offset - sizeof(uint64_t)); + std::copy(compressed.begin() + sizeof(uint64_t), + compressed.begin() + static_cast(chunk_table_offset), + reinterpret_cast(chunk_data.data())); + + std::string vlr_string(reinterpret_cast(vlr_bytes), static_cast(vlr_size)); + std::stringstream vlr_stream(vlr_string); + LAZSpecialVLRContent laz_vlr(vlr_stream); + + std::vector decoded(points.size()); + LAZReader laz_reader(laz_vlr); + laz_reader.decompress_chunk(std::span(chunk_data), std::span(decoded)); + + for (size_t i = 0; i < points.size(); i++) { + LASPP_ASSERT_EQ(decoded[i], points[i], "Point ", i, " mismatch in internal roundtrip"); + } +} + +} // namespace + +int main() { + constexpr size_t SmallPointCount = 128; + constexpr size_t MediumPointCount = 500; + constexpr size_t LargePointCount = 5000; + + // Format 0 tests - basic point format + run_laszip_internal_roundtrip(256); + run_laszip_file_roundtrip(1, false); + run_laszip_file_roundtrip(SmallPointCount, false); + run_laszip_file_roundtrip(MediumPointCount, false); + run_laspp_file_roundtrip(1); + run_laspp_file_roundtrip(SmallPointCount); + run_laspp_file_roundtrip(MediumPointCount); + + // Format 1 tests - with GPS time + run_laszip_internal_roundtrip(1000); + run_laszip_file_roundtrip(1, false); + run_laszip_file_roundtrip(SmallPointCount, false); + run_laszip_file_roundtrip(MediumPointCount, false); + run_laszip_file_roundtrip(LargePointCount, false); + run_laspp_file_roundtrip(1); + run_laspp_file_roundtrip(SmallPointCount); + run_laspp_file_roundtrip(MediumPointCount); + run_laspp_file_roundtrip(LargePointCount); + + // Format 6 tests - extended point format with native extension + // Note: compatibility mode (request_native_extension=false) + // is not supported by LAS++ reader - it only supports native + // extension mode (Point14 items) + run_laszip_internal_roundtrip(1000); + run_laszip_file_roundtrip(1, true); + run_laszip_file_roundtrip(SmallPointCount, true); + run_laszip_file_roundtrip(MediumPointCount, true); + run_laszip_file_roundtrip(LargePointCount, true); + run_laspp_file_roundtrip(1); + run_laspp_file_roundtrip(SmallPointCount); + run_laspp_file_roundtrip(MediumPointCount); + run_laspp_file_roundtrip(LargePointCount); + + // Format 7 tests - extended point format with RGB colors + run_laszip_internal_roundtrip(1000); + run_laszip_file_roundtrip(1, true); + run_laszip_file_roundtrip(SmallPointCount, true); + run_laszip_file_roundtrip(MediumPointCount, true); + run_laszip_file_roundtrip(LargePointCount, true); + run_laspp_file_roundtrip(1); + run_laspp_file_roundtrip(SmallPointCount); + run_laspp_file_roundtrip(MediumPointCount); + run_laspp_file_roundtrip(LargePointCount); + + return 0; +} diff --git a/src/laz/tests/test_laz_io.cpp b/src/laz/tests/test_laz_io.cpp index b52e534..41961f3 100644 --- a/src/laz/tests/test_laz_io.cpp +++ b/src/laz/tests/test_laz_io.cpp @@ -51,23 +51,11 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char *argv[]) { std::stringstream stream; std::unique_ptr laz_special_vlr; - std::mt19937 gen(0); - gen.seed(42); + std::mt19937_64 gen(42); std::vector points; points.reserve(100); for (size_t i = 0; i < 100; i++) { - LASPointFormat1 point; - point.x = static_cast(gen()); - point.y = static_cast(gen()); - point.z = static_cast(gen()); - point.intensity = static_cast(gen()); - point.bit_byte = static_cast(gen()); - point.classification_byte = static_cast(gen()); - point.scan_angle_rank = static_cast(gen()); - point.user_data = static_cast(gen()); - point.point_source_id = static_cast(gen()); - point.gps_time.f64 = static_cast(gen()); - points.push_back(point); + points.push_back(LASPointFormat1::RandomData(gen)); } { @@ -78,7 +66,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char *argv[]) { laz_special_vlr = std::make_unique(writer.special_vlr()); } - LASPP_ASSERT_EQ(stream.str().size(), 2136); + LASPP_ASSERT_EQ(stream.str().size(), 2138); { LAZReader reader(*laz_special_vlr); @@ -124,26 +112,11 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char *argv[]) { std::stringstream stream; std::unique_ptr laz_special_vlr; - std::mt19937 gen(0); - gen.seed(42); + std::mt19937_64 gen(42); std::vector points; points.reserve(200); for (size_t i = 0; i < 200; i++) { - LASPointFormat3 point; - point.x = static_cast(gen()); - point.y = static_cast(gen()); - point.z = static_cast(gen()); - point.intensity = static_cast(gen()); - point.bit_byte = static_cast(gen()); - point.classification_byte = static_cast(gen()); - point.scan_angle_rank = static_cast(gen()); - point.user_data = static_cast(gen()); - point.point_source_id = static_cast(gen()); - point.gps_time.f64 = static_cast(gen()); - point.red = static_cast(gen()); - point.green = static_cast(gen()); - point.blue = static_cast(gen()); - points.push_back(point); + points.push_back(LASPointFormat3::RandomData(gen)); } { @@ -160,7 +133,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char *argv[]) { laz_special_vlr = std::make_unique(writer.special_vlr()); } - LASPP_ASSERT_EQ(stream.str().size(), 7083); + LASPP_ASSERT_EQ(stream.str().size(), 7213); { LAZReader reader(*laz_special_vlr); @@ -187,5 +160,44 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char *argv[]) { } } } + { + std::stringstream stream; + std::unique_ptr laz_special_vlr; + + std::mt19937_64 gen(2024); + std::vector points; + points.reserve(150); + for (size_t i = 0; i < 150; i++) { + points.push_back(LASPointFormat7::RandomData(gen)); + } + + { + LAZWriter writer(stream, LAZCompressor::LayeredChunked); + writer.special_vlr().add_item_record(LAZItemRecord(LAZItemType::Point14)); + writer.special_vlr().add_item_record(LAZItemRecord(LAZItemType::RGB14)); + + writer.write_chunk(std::span(points)); + laz_special_vlr = std::make_unique(writer.special_vlr()); + } + + LASPP_ASSERT_EQ(stream.str().size(), 5929); + + { + LAZReader reader(*laz_special_vlr); + reader.read_chunk_table(stream, points.size()); + std::vector decompressed_points(points.size()); + + size_t compressed_size = reader.chunk_table().compressed_chunk_size(0); + std::vector compressed_chunk(compressed_size); + stream.seekg(static_cast(reader.chunk_table().chunk_offset(0))); + stream.read(reinterpret_cast(compressed_chunk.data()), + static_cast(compressed_size)); + reader.decompress_chunk(compressed_chunk, std::span(decompressed_points)); + + for (size_t i = 0; i < points.size(); i++) { + LASPP_ASSERT_EQ(decompressed_points[i], points[i]); + } + } + } return 0; } diff --git a/src/laz/tests/test_point10_encoder.cpp b/src/laz/tests/test_point10_encoder.cpp index e9e0f8a..4531296 100644 --- a/src/laz/tests/test_point10_encoder.cpp +++ b/src/laz/tests/test_point10_encoder.cpp @@ -60,7 +60,6 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { .user_data = 32, .point_source_id = 1}); std::mt19937 gen(0); - gen.seed(0); for (size_t i = points.size(); i < 1000; i++) { const LASPointFormat0& prev = points.back(); LASPointFormat0 next = prev; diff --git a/src/laz/tests/test_point14_encoder.cpp b/src/laz/tests/test_point14_encoder.cpp index 1d8e759..0d7604a 100644 --- a/src/laz/tests/test_point14_encoder.cpp +++ b/src/laz/tests/test_point14_encoder.cpp @@ -15,14 +15,19 @@ * trailblaze.software@gmail.com */ +#include +#include #include #include +#include #include +#include #include +#include #include "las_point.hpp" +#include "laz/layered_stream.hpp" #include "laz/point14_encoder.hpp" -#include "laz/stream.hpp" using namespace laspp; @@ -30,52 +35,63 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { { std::vector points; points.reserve(1000); - LASPointFormat6 first_point; + LASPointFormat6 first_point{}; first_point.x = 0; first_point.y = 0; first_point.z = 0; first_point.intensity = 0; - first_point.return_number = 0; - first_point.number_of_returns = 0; + first_point.return_number = 1; + first_point.number_of_returns = 1; + first_point.classification_flags = 0; + first_point.scanner_channel = 0; first_point.scan_direction_flag = 0; first_point.edge_of_flight_line = 0; first_point.classification = LASClassification::OverlapPoints; - first_point.scan_angle = 0; first_point.user_data = 0; + first_point.scan_angle = 0; first_point.point_source_id = 0; - first_point.gps_time = 0; + first_point.gps_time = 0.0; points.push_back(first_point); - std::mt19937 gen(0); - gen.seed(0); - for (size_t i = points.size(); i < 1000; i++) { - const LASPointFormat6& prev = points.back(); - LASPointFormat6 next = prev; - // uint8_t changed = gen() % 128; - next.x = static_cast(gen()); - next.y = static_cast(gen()); - next.z = static_cast(gen()); - points.emplace_back(next); + + std::mt19937_64 gen(0); + + for (size_t i = 1; i < 1000; i++) { + points.emplace_back(LASPointFormat6::RandomData(gen)); } - std::stringstream encoded_stream; + std::string combined_data; + { - laspp::OutStream ostream(encoded_stream); - LASPointFormat6Encoder encoder(points.front()); - for (LASPointFormat6& point : points) { - encoder.encode(ostream, point); + LayeredOutStreams out_streams; + { + std::unique_ptr encoder = + std::make_unique(points.front()); + for (size_t i = 1; i < points.size(); i++) { + encoder->encode(out_streams, points[i]); + } } + + std::stringstream combined_stream = out_streams.combined_stream(); + combined_data = combined_stream.str(); } - // LASPP_ASSERT_EQ(encoded_stream.str().size(), 17377); - // - //{ - // laspp::InStream instream(encoded_stream); - // LASPointFormat6Encoder encoder(points.back()); - // for (LASPointFormat6& point : points) { - // LASPP_ASSERT_EQ(encoder.decode(instream), point); - // LASPP_ASSERT_EQ(encoder.last_value(), point); - //} - //} + LASPP_ASSERT_EQ(combined_data.size(), 30852); + + std::span size_span(reinterpret_cast(combined_data.data()), + LASPointFormat6Encoder::NUM_LAYERS * sizeof(uint32_t)); + std::span data_span( + reinterpret_cast(combined_data.data()) + size_span.size(), + combined_data.size() - size_span.size()); + + LayeredInStreams in_streams(size_span, data_span); + + std::unique_ptr decoder = + std::make_unique(points.front()); + LASPP_ASSERT_EQ(decoder->last_value(), points[0]); + for (size_t i = 1; i < points.size(); i++) { + LASPP_ASSERT_EQ(decoder->decode(in_streams), points[i]); + LASPP_ASSERT_EQ(decoder->last_value(), points[i]); + } } return 0; diff --git a/src/laz/tests/test_rgb12_encoder.cpp b/src/laz/tests/test_rgb12_encoder.cpp index e8f0695..0acd125 100644 --- a/src/laz/tests/test_rgb12_encoder.cpp +++ b/src/laz/tests/test_rgb12_encoder.cpp @@ -47,8 +47,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { { std::stringstream encoded_stream; std::vector random_color_data; - std::mt19937 gen(0); - gen.seed(42); + std::mt19937 gen(42); for (size_t i = 0; i < 1000; i++) { random_color_data.emplace_back(laspp::ColorData{static_cast(gen()), static_cast(gen()), diff --git a/src/tests/test_reader.cpp b/src/tests/test_reader.cpp index 73c6ac5..6c00acd 100644 --- a/src/tests/test_reader.cpp +++ b/src/tests/test_reader.cpp @@ -41,7 +41,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { writer.header().transform() = Transform({1, 1, 1}, {0, 0, 0}); - writer.write_points(std::span(points)); + writer.write_points(std::span(points)); LASPP_ASSERT_THROWS(writer.write_vlr(LASVLR(), std::vector()), std::runtime_error); } @@ -91,9 +91,9 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { } writer.write_vlr(LASVLR(), std::vector(0)); - LASPP_ASSERT_THROWS(writer.write_points(std::span(points_bad)), + LASPP_ASSERT_THROWS(writer.write_points(std::span(points_bad)), std::runtime_error); - writer.write_points(std::span(points)); + writer.write_points(std::span(points)); LASPP_ASSERT_THROWS(writer.write_vlr(LASVLR(), std::vector()), std::runtime_error); } @@ -137,7 +137,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { } writer.write_vlr(LASVLR(), std::vector(0)); - writer.write_points(std::span(points)); + writer.write_points(std::span(points)); LASPP_ASSERT_THROWS(writer.write_vlr(LASVLR(), std::vector()), std::runtime_error); } @@ -184,9 +184,9 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { for (LASWriter* writer : {&las_writer, &laz_writer}) { writer->write_vlr(LASVLR(), std::vector(0)); - writer->write_points(std::span(points).subspan(0, 20)); - writer->write_points(std::span(points).subspan(20, 61)); - writer->write_points(std::span(points).subspan(81, 19)); + writer->write_points(std::span(points).subspan(0, 20)); + writer->write_points(std::span(points).subspan(20, 61)); + writer->write_points(std::span(points).subspan(81, 19)); LASPP_ASSERT_THROWS(writer->write_vlr(LASVLR(), std::vector()), std::runtime_error); diff --git a/src/utilities/arithmetic.hpp b/src/utilities/arithmetic.hpp new file mode 100644 index 0000000..696b864 --- /dev/null +++ b/src/utilities/arithmetic.hpp @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: (c) 2025 Trailblaze Software, all rights reserved + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * For LGPL2 incompatible licensing or development requests, please contact + * trailblaze.software@gmail.com + */ + +#pragma once + +#include + +namespace laspp { + +inline int32_t wrapping_int32_add(int32_t a, int32_t b) { + return static_cast(static_cast(a) + static_cast(b)); +} + +inline int32_t wrapping_int32_sub(int32_t a, int32_t b) { return wrapping_int32_add(a, -b); } + +inline uint8_t clamp(uint8_t value, int delta) { + if (delta > 255 - value) { + return 255; + } else if (value < -delta) { + return 0; + } + return static_cast(value + static_cast(delta)); +} + +} // namespace laspp