From ac1847be526063d68b77ba0b362a0c5617635e38 Mon Sep 17 00:00:00 2001 From: Chris Parker Date: Sat, 11 Apr 2026 00:16:28 -0500 Subject: [PATCH 01/27] Introduces unproject() lookup tables --- .gitignore | 5 + AGENTS.md | 44 + CLAUDE.md | 45 +- README.md | 33 + cpp_runtime/unproject_lut.cpp | 753 ++++++++++++++ cpp_runtime/unproject_lut.hpp | 109 ++ docs/unproject_lut.md | 159 +++ examples/unproject_lut_mo.py | 363 +++++++ mkdocs.yml | 2 + src/lensboy/__init__.py | 2 + src/lensboy/analysis/__init__.py | 41 +- src/lensboy/analysis/plots.py | 181 ++++ src/lensboy/analysis/unproject_lut.py | 1003 +++++++++++++++++++ src/lensboy/camera_models/__init__.py | 4 + src/lensboy/camera_models/base_model.py | 39 + src/lensboy/camera_models/opencv.py | 28 +- src/lensboy/camera_models/unproject_lut.py | 1055 ++++++++++++++++++++ tests/test_unproject_lut.py | 912 +++++++++++++++++ uv.lock | 79 +- 19 files changed, 4794 insertions(+), 63 deletions(-) create mode 100644 AGENTS.md create mode 100644 cpp_runtime/unproject_lut.cpp create mode 100644 cpp_runtime/unproject_lut.hpp create mode 100644 docs/unproject_lut.md create mode 100755 examples/unproject_lut_mo.py create mode 100644 src/lensboy/analysis/unproject_lut.py create mode 100644 src/lensboy/camera_models/unproject_lut.py create mode 100644 tests/test_unproject_lut.py diff --git a/.gitignore b/.gitignore index 6bd1daa..37a16c7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,15 @@ build/ dist/ wheels/ *.egg-info +**/__marimo__/ +site/ # Virtual environments .venv +# Mac system files +**/.DS_Store + .cache **/*.so diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..65c8168 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,44 @@ +Don't go too deep without exposing and sanity checking your approach with the user. +Reveal your thinking and processing as much as possible! + +Don't worry about linting errors if `ruff check --fix` and `ruff format` will fix them. +But do not run any of the linter checks on the terminal yourself, I'll handle that. + +# Docstrings + +Write docstrings for public functions/methods. + +For functions, they should be on this format + +``` +""" + + + +Args: + arg1: + arg2: + +Returns: + + +""" +``` + +Never say types in descriptions, as they are already annotated. Mention the shape of all numpy arrays. +Try not to duplicate information inside the docstring too much, and don't just repeat the variable names. + +# Stubs + +Never modify the stubs. They are autogenerated, and your changes will just be overwritten. + +# Comments + +Keep comments minimal, only add comments when the code needs explaining why it's doing something. + +# Plots + +Follow the color scheme of the existing plots in the plots.py file. +Making the type checker happy is less important in the plots if the plotting library APIs make it hard to make the types work. In this case the usage of `type: ignore` is fine. + +All plots should be exported, and if applicable, have a matching method on the CalibrationResult object in calibrate.py. diff --git a/CLAUDE.md b/CLAUDE.md index 65c8168..eef4bd2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,44 +1 @@ -Don't go too deep without exposing and sanity checking your approach with the user. -Reveal your thinking and processing as much as possible! - -Don't worry about linting errors if `ruff check --fix` and `ruff format` will fix them. -But do not run any of the linter checks on the terminal yourself, I'll handle that. - -# Docstrings - -Write docstrings for public functions/methods. - -For functions, they should be on this format - -``` -""" - - - -Args: - arg1: - arg2: - -Returns: - - -""" -``` - -Never say types in descriptions, as they are already annotated. Mention the shape of all numpy arrays. -Try not to duplicate information inside the docstring too much, and don't just repeat the variable names. - -# Stubs - -Never modify the stubs. They are autogenerated, and your changes will just be overwritten. - -# Comments - -Keep comments minimal, only add comments when the code needs explaining why it's doing something. - -# Plots - -Follow the color scheme of the existing plots in the plots.py file. -Making the type checker happy is less important in the plots if the plotting library APIs make it hard to make the types work. In this case the usage of `type: ignore` is fine. - -All plots should be exported, and if applicable, have a matching method on the CalibrationResult object in calibrate.py. +@AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md index d80cd1b..3d8f158 100644 --- a/README.md +++ b/README.md @@ -88,3 +88,36 @@ pip install lensboy Spline models use B-spline grids instead of polynomial coefficients, so they can fit lenses that OpenCV's model can't. This approach is inspired by [mrcal](https://mrcal.secretsauce.net/). The calibrated model converts to a pinhole model with undistortion maps, so you can use it with any standard pinhole pipeline. + +## Runtime unproject LUTs + +If you want a compact runtime cache of `normalize_points()`, `lensboy` can export a +single `.unproject_LUT` file with a human-readable text header followed by a binary +payload: + +```python +import lensboy as lb +from lensboy.analysis import UnprojectLUTAnalyzer + +model = lb.OpenCV.load("camera.json") +lut = model.get_unproject_lut( + pixel_stride=32, + storage_encoding="float32_xy", +) +lut.save("camera.unproject_LUT") + +runtime_lut = lb.UnprojectLUT.load("camera.unproject_LUT") +rays = runtime_lut.normalize_points( + pixel_coords, + interpolation="bilinear", + bounds="strict", +) + +analyzer = UnprojectLUTAnalyzer(runtime_lut) +report = analyzer.estimate_accuracy(interpolations="bilinear") +heatmap = analyzer.compute_error_heatmap(interpolation="bilinear") +``` + +See the [unproject LUT guide](https://robertleoj.github.io/lensboy/unproject_lut.html) +for the file format, size/error tradeoffs, and C++ runtime usage. There is also a runnable +[Marimo example](examples/unproject_lut_mo.py). diff --git a/cpp_runtime/unproject_lut.cpp b/cpp_runtime/unproject_lut.cpp new file mode 100644 index 0000000..65a333d --- /dev/null +++ b/cpp_runtime/unproject_lut.cpp @@ -0,0 +1,753 @@ +#include "unproject_lut.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace lensboy { +namespace { + +constexpr char const* k_format_name = "lensboy_unproject_LUT"; +constexpr int k_format_version = 1; +constexpr char const* k_header_end_marker = "END_HEADER"; +constexpr char const* k_payload_layout = "row_major_interleaved_xy"; +constexpr char const* k_payload_endianness = "little"; +constexpr std::size_t k_max_header_bytes = 512 * 1024 * 1024; + +std::string trim( + std::string const& text +) { + std::size_t const first = text.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) { + return ""; + } + std::size_t const last = text.find_last_not_of(" \t\r\n"); + return text.substr(first, last - first + 1); +} + +double quiet_nan() { + return std::numeric_limits::quiet_NaN(); +} + +bool is_finite( + PixelXY const& value +) { + return std::isfinite(value.xy[0]) and std::isfinite(value.xy[1]); +} + +uint16_t read_little_endian_16( + char const* data +) { + return static_cast(static_cast(data[0])) | + (static_cast(static_cast(data[1])) << 8); +} + +uint32_t read_little_endian_32( + char const* data +) { + return static_cast(static_cast(data[0])) | + (static_cast(static_cast(data[1])) << 8) | + (static_cast(static_cast(data[2])) << 16) | + (static_cast(static_cast(data[3])) << 24); +} + +uint64_t read_little_endian_64( + char const* data +) { + uint64_t value = 0; + for (int i = 0; i < 8; ++i) { + value |= static_cast(static_cast(data[i])) + << (8 * i); + } + return value; +} + +double float16_to_double( + uint16_t bits +) { + uint16_t const sign = static_cast((bits >> 15) & 0x1); + uint16_t const exponent = static_cast((bits >> 10) & 0x1F); + uint16_t const mantissa = static_cast(bits & 0x03FF); + + if (exponent == 0) { + if (mantissa == 0) { + return sign == 0 ? 0.0 : -0.0; + } + double const fraction = static_cast(mantissa) / 1024.0; + double const magnitude = std::ldexp(fraction, -14); + return sign == 0 ? magnitude : -magnitude; + } + + if (exponent == 31) { + if (mantissa == 0) { + return sign == 0 ? std::numeric_limits::infinity() + : -std::numeric_limits::infinity(); + } + return quiet_nan(); + } + + double const fraction = 1.0 + static_cast(mantissa) / 1024.0; + double const magnitude = std::ldexp(fraction, static_cast(exponent) - 15); + return sign == 0 ? magnitude : -magnitude; +} + +double decode_float32( + char const* data +) { + uint32_t const bits = read_little_endian_32(data); + float value = 0.0f; + std::memcpy(&value, &bits, sizeof(value)); + return static_cast(value); +} + +double decode_float64( + char const* data +) { + uint64_t const bits = read_little_endian_64(data); + double value = 0.0; + std::memcpy(&value, &bits, sizeof(value)); + return value; +} + +double decode_payload_value( + char const* data, + std::string const& storage_encoding +) { + if (storage_encoding == "float16_xy") { + return float16_to_double(read_little_endian_16(data)); + } + if (storage_encoding == "float32_xy") { + return decode_float32(data); + } + if (storage_encoding == "float64_xy") { + return decode_float64(data); + } + throw std::runtime_error("Unsupported storage encoding: " + storage_encoding); +} + +std::size_t payload_item_size( + std::string const& storage_encoding +) { + if (storage_encoding == "float16_xy") { + return 2; + } + if (storage_encoding == "float32_xy") { + return 4; + } + if (storage_encoding == "float64_xy") { + return 8; + } + throw std::runtime_error("Unsupported storage encoding: " + storage_encoding); +} + +std::array parse_pair_of_ints( + std::string const& text, + std::string const& field_name +) { + std::array values{}; + std::stringstream stream(text); + std::string part; + for (int i = 0; i < 2; ++i) { + if (not std::getline(stream, part, ',')) { + throw std::runtime_error( + field_name + " must contain exactly 2 comma-separated values." + ); + } + values[i] = std::stoi(trim(part)); + } + if (std::getline(stream, part, ',')) { + throw std::runtime_error( + field_name + " must contain exactly 2 comma-separated values." + ); + } + return values; +} + +std::array parse_quad_of_doubles( + std::string const& text, + std::string const& field_name +) { + std::array values{}; + std::stringstream stream(text); + std::string part; + for (int i = 0; i < 4; ++i) { + if (not std::getline(stream, part, ',')) { + throw std::runtime_error( + field_name + " must contain exactly 4 comma-separated values." + ); + } + values[i] = std::stod(trim(part)); + } + if (std::getline(stream, part, ',')) { + throw std::runtime_error( + field_name + " must contain exactly 4 comma-separated values." + ); + } + return values; +} + +std::array parse_pair_of_doubles( + std::string const& text, + std::string const& field_name +) { + std::array values{}; + std::stringstream stream(text); + std::string part; + for (int i = 0; i < 2; ++i) { + if (not std::getline(stream, part, ',')) { + throw std::runtime_error( + field_name + " must contain exactly 2 comma-separated values." + ); + } + values[i] = std::stod(trim(part)); + } + if (std::getline(stream, part, ',')) { + throw std::runtime_error( + field_name + " must contain exactly 2 comma-separated values." + ); + } + return values; +} + +std::string parse_optional_string( + std::string const& text +) { + if (text == "not_computed") { + return ""; + } + return text; +} + +std::array catmull_rom_weights( + double t +) { + double const t2 = t * t; + double const t3 = t2 * t; + return { + -0.5 * t + t2 - 0.5 * t3, + 1.0 - 2.5 * t2 + 1.5 * t3, + 0.5 * t + 2.0 * t2 - 1.5 * t3, + -0.5 * t2 + 0.5 * t3, + }; +} + +PixelXY add_scaled( + PixelXY const& a, + PixelXY const& b, + double scale +) { + return { + a.xy[0] + b.xy[0] * scale, + a.xy[1] + b.xy[1] * scale, + }; +} + +std::string_view append_string_view( + std::vector& storage, + std::string const& text +) { + if (text.empty()) { + return {}; + } + std::size_t const offset = storage.size(); + storage.insert(storage.end(), text.begin(), text.end()); + storage.push_back('\0'); + return std::string_view(storage.data() + offset, text.size()); +} + +void normalize_ray( + double ray[3] +) { + double const norm = std::sqrt( + ray[0] * ray[0] + + ray[1] * ray[1] + + ray[2] * ray[2] + ); + if (norm == 0.0 or not std::isfinite(norm)) { + throw std::runtime_error("Cannot normalize a non-finite or zero-length ray."); + } + ray[0] /= norm; + ray[1] /= norm; + ray[2] /= norm; +} + +UnprojectLUTQueryResult invalid_result() { + return UnprojectLUTQueryResult{}; +} + +} // namespace + +UnprojectLUT::UnprojectLUT( + UnprojectLUTMetadata metadata, + std::vector xy_grid, + std::vector string_storage +) : string_storage_(std::move(string_storage)), + metadata_(std::move(metadata)), + xy_grid_(std::move(xy_grid)) { + if (metadata_.image_width == 0 or metadata_.image_height == 0) { + throw std::runtime_error("Image dimensions must be positive."); + } + if (metadata_.grid_width == 0 or metadata_.grid_height == 0) { + throw std::runtime_error("Grid dimensions must be positive."); + } + if ( + metadata_.grid_x_max < metadata_.grid_x_min or + metadata_.grid_y_max < metadata_.grid_y_min + ) { + throw std::runtime_error("Grid extents must be ordered from min to max."); + } + if (xy_grid_.size() != metadata_.grid_width * metadata_.grid_height * 2) { + throw std::runtime_error("xy_grid size does not match metadata."); + } + + if (metadata_.grid_width <= 1 or metadata_.grid_x_max == metadata_.grid_x_min) { + grid_scale_x_ = 0.0; + } else { + grid_scale_x_ = static_cast(metadata_.grid_width - 1) / + (metadata_.grid_x_max - metadata_.grid_x_min); + } + if ( + metadata_.grid_height <= 1 or + metadata_.grid_y_max == metadata_.grid_y_min + ) { + grid_scale_y_ = 0.0; + } else { + grid_scale_y_ = static_cast(metadata_.grid_height - 1) / + (metadata_.grid_y_max - metadata_.grid_y_min); + } +} + +UnprojectLUT UnprojectLUT::load( + std::string_view const path +) { + std::ifstream file(std::string(path), std::ios::binary); + if (not file) { + throw std::runtime_error("Failed to open LUT file: " + std::string(path)); + } + + std::unordered_map header; + std::string line; + std::size_t header_bytes = 0; + while (std::getline(file, line)) { + header_bytes += line.size() + 1; + if (header_bytes > k_max_header_bytes) { + throw std::runtime_error("Header exceeds the maximum supported size."); + } + std::string const trimmed = trim(line); + if (trimmed == k_header_end_marker) { + break; + } + + std::size_t const colon = trimmed.find(':'); + if (colon == std::string::npos) { + throw std::runtime_error( + "Invalid header line. Expected 'key: value'." + ); + } + + std::string const key = trim(trimmed.substr(0, colon)); + std::string const value = trim(trimmed.substr(colon + 1)); + if (key.empty()) { + throw std::runtime_error("Header keys must be non-empty."); + } + if (header.find(key) != header.end()) { + throw std::runtime_error("Duplicate header key: " + key); + } + header[key] = value; + } + + if (not file or trim(line) != k_header_end_marker) { + throw std::runtime_error("Reached end of file before END_HEADER."); + } + + auto const require_field = [&header](std::string const& key) -> std::string const& { + auto const it = header.find(key); + if (it == header.end()) { + throw std::runtime_error("Missing required header field: " + key); + } + return it->second; + }; + + for (auto const& [key, _value] : header) { + bool const is_removed_field = + key == "error_report_mode" or + key == "error_report_max_depth" or + key == "error_report_min_cell_size" or + key.rfind("estimated_max_angular_error_", 0) == 0 or + key.rfind("estimated_median_angular_error_", 0) == 0; + if (is_removed_field) { + throw std::runtime_error( + "This runtime-only .unproject_LUT format does not support legacy " + "error-report header fields." + ); + } + } + + if (require_field("format") != k_format_name) { + throw std::runtime_error("Unsupported LUT format."); + } + int const format_version = std::stoi(require_field("format_version")); + if (format_version != k_format_version) { + throw std::runtime_error("Unsupported LUT format_version."); + } + if (require_field("payload_layout") != k_payload_layout) { + throw std::runtime_error("Unsupported payload_layout."); + } + if (require_field("payload_endianness") != k_payload_endianness) { + throw std::runtime_error("Unsupported payload_endianness."); + } + std::streampos const payload_offset = file.tellg(); + if (payload_offset < 0) { + throw std::runtime_error("Failed to determine payload offset."); + } + std::size_t const payload_offset_bytes = static_cast(payload_offset); + std::size_t const declared_payload_offset_bytes = static_cast( + std::stoull(require_field("payload_offset_bytes")) + ); + if (payload_offset_bytes != declared_payload_offset_bytes) { + throw std::runtime_error("payload_offset_bytes does not match payload position."); + } + + std::array const image_size = + parse_pair_of_ints(require_field("image_size_wh"), "image_size_wh"); + std::array const grid_size = + parse_pair_of_ints(require_field("grid_size_wh"), "grid_size_wh"); + std::array const extents = + parse_quad_of_doubles(require_field("grid_extents_xy"), "grid_extents_xy"); + std::array const grid_stride = + parse_pair_of_doubles(require_field("grid_stride_xy"), "grid_stride_xy"); + double const expected_grid_stride_x = + grid_size[0] <= 1 ? 0.0 : (extents[1] - extents[0]) / static_cast(grid_size[0] - 1); + double const expected_grid_stride_y = + grid_size[1] <= 1 ? 0.0 : (extents[3] - extents[2]) / static_cast(grid_size[1] - 1); + if (std::abs(grid_stride[0] - expected_grid_stride_x) > 1e-12 or + std::abs(grid_stride[1] - expected_grid_stride_y) > 1e-12) { + throw std::runtime_error("grid_stride_xy does not match grid_extents_xy and grid_size_wh."); + } + + std::string const storage_encoding = require_field("storage_encoding"); + std::size_t const item_size = payload_item_size(storage_encoding); + std::size_t const expected_payload_bytes = + static_cast(grid_size[0]) * + static_cast(grid_size[1]) * 2 * item_size; + + std::vector payload( + (std::istreambuf_iterator(file)), + std::istreambuf_iterator() + ); + if (payload.size() != expected_payload_bytes) { + throw std::runtime_error("Unexpected payload size."); + } + + std::vector xy_grid( + static_cast(grid_size[0]) * + static_cast(grid_size[1]) * 2 + ); + for (std::size_t i = 0; i < xy_grid.size(); ++i) { + xy_grid[i] = + decode_payload_value(payload.data() + i * item_size, storage_encoding); + if (not std::isfinite(xy_grid[i])) { + throw std::runtime_error("Payload contains non-finite values."); + } + } + + std::string const default_interpolation = require_field("default_interpolation"); + std::string const default_bounds = require_field("default_bounds"); + std::string const source_model_type = + parse_optional_string(require_field("source_model_type")); + std::string const source_model_spec_json = + parse_optional_string(require_field("source_model_spec_json")); + std::string const source_model_spec_json_sha256 = + parse_optional_string(require_field("source_model_spec_json_sha256")); + std::string const lensboy_version = require_field("lensboy_version"); + + std::vector string_storage; + string_storage.reserve( + storage_encoding.size() + + default_interpolation.size() + + default_bounds.size() + + source_model_type.size() + + source_model_spec_json.size() + + source_model_spec_json_sha256.size() + + lensboy_version.size() + + 8 + ); + + UnprojectLUTMetadata metadata; + metadata.image_width = static_cast(image_size[0]); + metadata.image_height = static_cast(image_size[1]); + metadata.grid_width = static_cast(grid_size[0]); + metadata.grid_height = static_cast(grid_size[1]); + metadata.grid_x_min = extents[0]; + metadata.grid_x_max = extents[1]; + metadata.grid_y_min = extents[2]; + metadata.grid_y_max = extents[3]; + metadata.grid_stride_x = grid_stride[0]; + metadata.grid_stride_y = grid_stride[1]; + metadata.storage_encoding = append_string_view(string_storage, storage_encoding); + metadata.default_interpolation = append_string_view( + string_storage, + default_interpolation + ); + metadata.default_bounds = append_string_view( + string_storage, + default_bounds + ); + metadata.source_model_type = append_string_view( + string_storage, + source_model_type + ); + metadata.source_model_spec_json = append_string_view( + string_storage, + source_model_spec_json + ); + metadata.source_model_spec_json_sha256 = append_string_view( + string_storage, + source_model_spec_json_sha256 + ); + metadata.lensboy_version = append_string_view( + string_storage, + lensboy_version + ); + metadata.payload_offset_bytes = payload_offset_bytes; + + return UnprojectLUT( + std::move(metadata), + std::move(xy_grid), + std::move(string_storage) + ); +} + +UnprojectLUTMetadata const& UnprojectLUT::metadata() const noexcept { + return metadata_; +} + +std::size_t UnprojectLUT::flat_index( + std::size_t x, + std::size_t y +) const noexcept { + return (y * metadata_.grid_width + x) * 2; +} + +PixelXY UnprojectLUT::sample_node( + std::size_t x, + std::size_t y +) const noexcept { + std::size_t const idx = flat_index(x, y); + return {{xy_grid_[idx], xy_grid_[idx + 1]}}; +} + +double UnprojectLUT::grid_coordinate_x( + double pixel_x +) const noexcept { + return (pixel_x - metadata_.grid_x_min) * grid_scale_x_; +} + +double UnprojectLUT::grid_coordinate_y( + double pixel_y +) const noexcept { + return (pixel_y - metadata_.grid_y_min) * grid_scale_y_; +} + +UnprojectLUTQueryResult UnprojectLUT::query( + double pixel_x, + double pixel_y, + InterpolationMode interpolation, + BoundsMode bounds, + bool const normalize +) const { + bool const inside = pixel_x >= metadata_.grid_x_min and + pixel_x <= metadata_.grid_x_max and + pixel_y >= metadata_.grid_y_min and + pixel_y <= metadata_.grid_y_max; + + if (bounds == BoundsMode::kStrict and not inside) { + return invalid_result(); + } + + double sample_x = pixel_x; + double sample_y = pixel_y; + if (bounds == BoundsMode::kClamp) { + sample_x = std::clamp(sample_x, metadata_.grid_x_min, metadata_.grid_x_max); + sample_y = std::clamp(sample_y, metadata_.grid_y_min, metadata_.grid_y_max); + } + + double const gx = grid_coordinate_x(sample_x); + double const gy = grid_coordinate_y(sample_y); + + PixelXY xy = {{quiet_nan(), quiet_nan()}}; + + auto const bilinear_xy = [this, gx, gy, bounds]() -> PixelXY { + long long ix0 = 0; + long long iy0 = 0; + double tx = 0.0; + double ty = 0.0; + + if (metadata_.grid_width > 1) { + double gx_work = gx; + if (bounds != BoundsMode::kExtrapolate) { + gx_work = std::clamp( + gx_work, + 0.0, + static_cast(metadata_.grid_width - 1) + ); + } + ix0 = static_cast(std::floor(gx_work)); + ix0 = std::clamp( + ix0, + 0, + static_cast(metadata_.grid_width) - 2 + ); + tx = gx_work - static_cast(ix0); + } + + if (metadata_.grid_height > 1) { + double gy_work = gy; + if (bounds != BoundsMode::kExtrapolate) { + gy_work = std::clamp( + gy_work, + 0.0, + static_cast(metadata_.grid_height - 1) + ); + } + iy0 = static_cast(std::floor(gy_work)); + iy0 = std::clamp( + iy0, + 0, + static_cast(metadata_.grid_height) - 2 + ); + ty = gy_work - static_cast(iy0); + } + + std::size_t const x0 = static_cast(ix0); + std::size_t const x1 = + std::min(x0 + 1, metadata_.grid_width - 1); + std::size_t const y0 = static_cast(iy0); + std::size_t const y1 = + std::min(y0 + 1, metadata_.grid_height - 1); + + PixelXY const v00 = sample_node(x0, y0); + PixelXY const v10 = sample_node(x1, y0); + PixelXY const v01 = sample_node(x0, y1); + PixelXY const v11 = sample_node(x1, y1); + + PixelXY const top = {{ + v00.xy[0] * (1.0 - tx) + v10.xy[0] * tx, + v00.xy[1] * (1.0 - tx) + v10.xy[1] * tx, + }}; + PixelXY const bottom = {{ + v01.xy[0] * (1.0 - tx) + v11.xy[0] * tx, + v01.xy[1] * (1.0 - tx) + v11.xy[1] * tx, + }}; + return {{ + top.xy[0] * (1.0 - ty) + bottom.xy[0] * ty, + top.xy[1] * (1.0 - ty) + bottom.xy[1] * ty, + }}; + }; + + if (interpolation == InterpolationMode::kNearest) { + long long const ix = std::llround(gx); + long long const iy = std::llround(gy); + std::size_t const sample_ix = static_cast( + std::clamp(ix, 0, static_cast(metadata_.grid_width) - 1) + ); + std::size_t const sample_iy = static_cast( + std::clamp( + iy, + 0, + static_cast(metadata_.grid_height) - 1 + ) + ); + xy = sample_node(sample_ix, sample_iy); + } else if (interpolation == InterpolationMode::kBilinear) { + xy = bilinear_xy(); + } else if (interpolation == InterpolationMode::kBicubic) { + if (metadata_.grid_width < 4 or metadata_.grid_height < 4) { + xy = bilinear_xy(); + } else { + double const gx_work = bounds == BoundsMode::kExtrapolate + ? gx + : std::clamp(gx, 0.0, static_cast(metadata_.grid_width - 1)); + double const gy_work = bounds == BoundsMode::kExtrapolate + ? gy + : std::clamp(gy, 0.0, static_cast(metadata_.grid_height - 1)); + + long long const anchor_x = static_cast(std::floor(gx_work)); + long long const anchor_y = static_cast(std::floor(gy_work)); + bool const has_full_support = + anchor_x >= 1 and + anchor_x <= static_cast(metadata_.grid_width) - 3 and + anchor_y >= 1 and + anchor_y <= static_cast(metadata_.grid_height) - 3; + if (not has_full_support) { + xy = bilinear_xy(); + } else { + double const tx = gx_work - static_cast(anchor_x); + double const ty = gy_work - static_cast(anchor_y); + + std::array const wx = catmull_rom_weights(tx); + std::array const wy = catmull_rom_weights(ty); + PixelXY accum = {{0.0, 0.0}}; + for (int j = 0; j < 4; ++j) { + std::size_t const sample_y_idx = + static_cast(anchor_y + j - 1); + PixelXY row = {{0.0, 0.0}}; + for (int i = 0; i < 4; ++i) { + std::size_t const sample_x_idx = + static_cast(anchor_x + i - 1); + PixelXY const node = sample_node( + sample_x_idx, + sample_y_idx + ); + row = add_scaled(row, node, wx[i]); + } + accum = add_scaled(accum, row, wy[j]); + } + xy = accum; + } + } + } else { + throw std::runtime_error("Unreachable interpolation mode."); + } + + if (not is_finite(xy)) { + throw std::runtime_error("Query produced non-finite values."); + } + + UnprojectLUTQueryResult result; + result.valid = true; + result.ray[0] = xy.xy[0]; + result.ray[1] = xy.xy[1]; + result.ray[2] = 1.0; + if (normalize) { + normalize_ray(result.ray); + } + return result; +} + +std::vector UnprojectLUT::query( + std::vector const& pixels, + InterpolationMode interpolation, + BoundsMode bounds, + bool const normalize +) const { + std::vector results; + results.reserve(pixels.size()); + for (PixelXY const& pixel : pixels) { + results.push_back( + query(pixel.xy[0], pixel.xy[1], interpolation, bounds, normalize) + ); + } + return results; +} + +} // namespace lensboy diff --git a/cpp_runtime/unproject_lut.hpp b/cpp_runtime/unproject_lut.hpp new file mode 100644 index 0000000..8c88d5b --- /dev/null +++ b/cpp_runtime/unproject_lut.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include +#include + +namespace lensboy { + +enum class InterpolationMode { + kNearest, + kBilinear, + kBicubic, +}; + +enum class BoundsMode { + kStrict, + kClamp, + kExtrapolate, +}; + +struct PixelXY { + double xy[2] = {0.0, 0.0}; +}; + +struct UnprojectLUTMetadata { + std::size_t image_width = 0; + std::size_t image_height = 0; + std::size_t grid_width = 0; + std::size_t grid_height = 0; + double grid_x_min = 0.0; + double grid_x_max = 0.0; + double grid_y_min = 0.0; + double grid_y_max = 0.0; + double grid_stride_x = 0.0; + double grid_stride_y = 0.0; + std::string_view storage_encoding; + std::string_view default_interpolation; + std::string_view default_bounds; + std::string_view source_model_type; + std::string_view source_model_spec_json; + std::string_view source_model_spec_json_sha256; + std::string_view lensboy_version; + std::size_t payload_offset_bytes = 0; +}; + +struct UnprojectLUTQueryResult { + bool valid = false; + double ray[3] = { + std::numeric_limits::quiet_NaN(), + std::numeric_limits::quiet_NaN(), + std::numeric_limits::quiet_NaN(), + }; +}; + +class UnprojectLUT { + public: + static UnprojectLUT load(std::string_view path); + + UnprojectLUTMetadata const& metadata() const noexcept; + + UnprojectLUTQueryResult query( + double pixel_x, + double pixel_y, + InterpolationMode interpolation = InterpolationMode::kBilinear, + BoundsMode bounds = BoundsMode::kStrict, + bool normalize = true + ) const; + + std::vector query( + std::vector const& pixels, + InterpolationMode interpolation = InterpolationMode::kBilinear, + BoundsMode bounds = BoundsMode::kStrict, + bool normalize = true + ) const; + + private: + UnprojectLUT( + UnprojectLUTMetadata metadata, + std::vector xy_grid, + std::vector string_storage + ); + + std::size_t flat_index( + std::size_t x, + std::size_t y + ) const noexcept; + + PixelXY sample_node( + std::size_t x, + std::size_t y + ) const noexcept; + + double grid_coordinate_x( + double pixel_x + ) const noexcept; + + double grid_coordinate_y( + double pixel_y + ) const noexcept; + + std::vector string_storage_; + UnprojectLUTMetadata metadata_; + std::vector xy_grid_; + double grid_scale_x_ = 0.0; + double grid_scale_y_ = 0.0; +}; + +} // namespace lensboy diff --git a/docs/unproject_lut.md b/docs/unproject_lut.md new file mode 100644 index 0000000..bd694d0 --- /dev/null +++ b/docs/unproject_lut.md @@ -0,0 +1,159 @@ +# Unproject LUT Guide + +`lensboy` can export a camera model's `normalize_points()` field into a regular-grid lookup table for fast runtime queries. + +The `.unproject_LUT` file format is intentionally split into two concerns: + +- `UnprojectLUT` is the runtime object. It handles grid construction, file I/O, and fast lookup. +- `UnprojectLUTAnalyzer` is the analysis object. It reconstructs the exact source model from the embedded model spec and computes accuracy summaries or error heatmaps on demand. + +This keeps the runtime file format compact while still making later analysis possible. + +## Build a runtime LUT + +```python +import lensboy as lb + +model = lb.OpenCV.load("camera.json") +lut = model.get_unproject_lut( + pixel_stride=32, + storage_encoding="float32_xy", +) +lut.save("camera.unproject_LUT") +``` + +You can control the LUT size in two ways: + +- `pixel_stride=...` chooses an approximate spacing in image pixels between cached samples. +- `storage_encoding=...` chooses the on-disk precision: + - `float64_xy` + - `float32_xy` + - `float16_xy` + +The LUT stores only `x` and `y`. The queried ray is always reconstructed as `[x, y, 1]`. + +## Load and query the LUT + +```python +runtime_lut = lb.UnprojectLUT.load("camera.unproject_LUT") + +rays = runtime_lut.normalize_points( + pixel_coords, + interpolation="bilinear", + bounds="strict", +) +``` + +Supported runtime interpolation modes: + +- `nearest` +- `bilinear` +- `bicubic` + +Supported bounds modes: + +- `strict` +- `clamp` +- `extrapolate` + +`bilinear` is the default. `strict` is the default bounds behavior. + +## Analyze accuracy later + +```python +from lensboy.analysis import UnprojectLUTAnalyzer + +analyzer = UnprojectLUTAnalyzer(runtime_lut) + +report = analyzer.estimate_accuracy( + interpolations=("nearest", "bilinear"), +) +heatmap = analyzer.compute_error_heatmap(interpolation="bilinear") +heatmap.save("camera_bilinear_error_heatmap.npz") +``` + +The accuracy report contains: + +- `max_angular_error_mdeg` +- `median_angular_error_mdeg` +- the interpolation modes that were analyzed +- the adaptive estimator settings that produced the result + +The analyzer requires `source_model_spec_json` to be present in the LUT. Standard `lensboy` camera models write that field automatically. + +## Plot a heatmap + +```python +from lensboy.analysis import plot_unproject_lut_error_heatmap + +fig = plot_unproject_lut_error_heatmap( + heatmap, + angular_unit="mdeg", + return_figure=True, +) +``` + +The plot helper accepts either: + +- an in-memory `UnprojectLUTErrorHeatmap` +- a saved `.npz` heatmap path + +## File format + +The `.unproject_LUT` file starts with an ASCII header followed by a binary payload. The payload offset is written into the header itself, so the header length can grow as needed. + +The header looks like this: + +```text +format: lensboy_unproject_LUT +payload_offset_bytes: 1306 +format_version: 1 +lensboy_version: 3.0.1 +source_model_type: opencv +source_model_spec_json_sha256: e31fc9944f7ad2a787a38a65331bf873baeeed68fc881f6e237e3b6a5a3e9811 +image_size_wh: 3088, 2064 +grid_size_wh: 98, 66 +grid_extents_xy: 0, 3087, 0, 2063 +grid_stride_xy: 31.824742268041238, 31.738461538461539 +storage_encoding: float32_xy +default_interpolation: bilinear +default_bounds: strict +payload_layout: row_major_interleaved_xy +payload_endianness: little +source_model_spec_json: {"cx":1514.1042261000703,"cy":1076.8896307961334,"distortion_coeffs":[...],"fx":1354.51232559645,"fy":1354.318019481934,"image_height":2064,"image_width":3088,"lensboy-version":"3.0.1","type":"opencv"} +END_HEADER +``` + +Notes: + +- `grid_stride_xy` is derived from the extents and grid size, so it may be fractional. +- `source_model_spec_json` is stored in canonical JSON form. +- The binary payload is little-endian, row-major, interleaved `x/y`. +- The runtime format does not store accuracy reports. Those are computed separately by `UnprojectLUTAnalyzer`. + +## Standalone C++ runtime + +The repository ships a small standalone runtime in: + +- `cpp_runtime/unproject_lut.hpp` +- `cpp_runtime/unproject_lut.cpp` + +You can copy those two files into another project and load/query the LUT there: + +```cpp +#include "unproject_lut.hpp" + +auto lut = lensboy::UnprojectLUT::load("camera.unproject_LUT"); +auto result = lut.query( + 1280.0, + 720.0, + lensboy::InterpolationMode::kBilinear, + lensboy::BoundsMode::kStrict +); + +if (result.valid) { + do_stuff(result.ray); +} +``` + +By default, `query()` returns a unit-length ray. Pass `normalize=false` if you want the stored LUT convention of `[x, y, 1]`. diff --git a/examples/unproject_lut_mo.py b/examples/unproject_lut_mo.py new file mode 100755 index 0000000..54c9037 --- /dev/null +++ b/examples/unproject_lut_mo.py @@ -0,0 +1,363 @@ +#!/usr/bin/env -S uv run --with marimo marimo -y edit --no-sandbox +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "lensboy>=3.0.1", +# "marimo>=0.23.0", +# "matplotlib==3.10.8", +# "numpy==2.4.4", +# ] +# /// + +import marimo + +__generated_with = "0.23.1" +app = marimo.App(width="medium") + +with app.setup(hide_code=True): + from pathlib import Path + from types import SimpleNamespace + + import marimo as mo + import numpy as np + + import lensboy as lb + from lensboy.analysis import ( + UnprojectLUTAnalyzer, + UnprojectLUTErrorHeatmap, + plot_unproject_lut_error_heatmap, + ) + + +@app.cell(hide_code=True) +def _(): + mo.md(""" + # Unproject LUT (LookUp Table) example + + The function which goes from pixel location to 3D ray is called `unproject()`. It is usually not possible to compute in closed form, so it must be iteratively approximated. Unfortunately, this process is slow, which can be a problem for some applications. + + Lensboy allows you to precompute these values once ahead of time, for fast lookup during operation. The runtime LUT stays compact and fast, while the accuracy analysis is available separately on demand. + + This notebook builds a `.unproject_LUT` file from the bundled OpenCV test + calibration, saves it to `examples/generated/`, reloads it, and compares the + cached rays against the exact camera model. + + The controls intentionally stay in a moderate range so the notebook remains + interactive. If you want a denser cache, edit the values in the cells below. + """) + return + + +@app.cell +def _(): + repo_root = Path(__file__).resolve().parents[1] + data_dir = repo_root / "data" / "test_datasets" + output_dir = repo_root / "examples" / "generated" + output_dir.mkdir(parents=True, exist_ok=True) + return data_dir, output_dir, repo_root + + +@app.cell(hide_code=True) +def _(model): + mo.md(f""" + ## Source camera model + + Using a OpenCV calibration that comes with Lensboy for example purposes: + + - `{model = !r}` + """) + return + + +@app.cell +def _(data_dir): + model = lb.OpenCV.load(data_dir / "opencv.json") + return (model,) + + +@app.cell(hide_code=True) +def _(): + mo.md(r""" + ## Compute the lookup table + + Use the drop-down widget below to provide paramters for how the LUT is created and used. + """) + return + + +@app.cell(hide_code=True) +def _(): + pixel_stride_widget = mo.ui.dropdown( + options=[ + "1", + "2", + "3", + "4", + "6", + "8", + "12", + "16", + "24", + "32", + "48", + "64", + "96", + "128", + ], + value="64", + label="Approximate pixel stride", + ) + storage_encoding_widget = mo.ui.dropdown( + options=["float64_xy", "float32_xy", "float16_xy"], + value="float32_xy", + label="Storage encoding", + ) + num_workers_widget = mo.ui.slider( + start=1, + step=1, + stop=64, + value=8, + label="Number of workers", + show_value=True, + debounce=True, + ) + controls_ui = mo.vstack( + [ + pixel_stride_widget, + storage_encoding_widget, + num_workers_widget, + ] + ) + controls_ui + return num_workers_widget, pixel_stride_widget, storage_encoding_widget + + +@app.cell(hide_code=True) +def _(num_workers_widget, pixel_stride_widget, storage_encoding_widget): + controls = SimpleNamespace( + pixel_stride=float(pixel_stride_widget.value), + storage_encoding=storage_encoding_widget.value, + num_workers=num_workers_widget.value, + ) + return (controls,) + + +@app.cell +def _(controls, model): + # Be patient! This may take a while. + lut = model.get_unproject_lut( + pixel_stride=controls.pixel_stride, + storage_encoding=controls.storage_encoding, + num_workers=controls.num_workers, + ) + return (lut,) + + +@app.cell(hide_code=True) +def _(): + mo.md(r""" + ## Save the lookup-table to disk + """) + return + + +@app.cell +def _(controls, lut, output_dir): + lut_filename = f"opencv_stride_{int(controls.pixel_stride)}_{controls.storage_encoding}.unproject_LUT" + lut_path = output_dir / lut_filename + + lut.save(lut_path) + return (lut_path,) + + +@app.cell(hide_code=True) +def _(): + mo.md(r""" + ## Load the lookup-table from disk + """) + return + + +@app.cell +def _(lut_path): + loaded = lb.UnprojectLUT.load(lut_path) + return (loaded,) + + +@app.cell(hide_code=True) +def _(loaded, lut_path, repo_root): + mo.md(rf""" + ## Loaded file header data + + Read `{lut_path.relative_to(repo_root)}`. + + - total size: `{loaded.total_bytes / 1024:.1f} KiB` + - payload size: `{loaded.payload_bytes / 1024:.1f} KiB` + - grid size: `{loaded.grid_width} x {loaded.grid_height}` + - full file header: + ```text + {loaded.header_preview()} + ``` + """) + return + + +@app.cell +def _(loaded): + analyzer = UnprojectLUTAnalyzer(loaded) + return (analyzer,) + + +@app.cell(hide_code=True) +def _(): + interpolation_widget = mo.ui.dropdown( + options=["nearest", "bilinear", "bicubic"], + value="bilinear", + label="Query interpolation mode", + ) + analyzer_ui = mo.vstack( + [ + interpolation_widget, + ] + ) + analyzer_ui + return (interpolation_widget,) + + +@app.cell(hide_code=True) +def _(interpolation_widget): + interpolation_mode = interpolation_widget.value + return (interpolation_mode,) + + +@app.cell(hide_code=True) +def _(accuracy_report, interpolation_mode, sample_accuracy): + mo.md(f""" + ## Accuracy on a dense sample + + Queried `{sample_accuracy.sample_count}` evenly spaced pixels with + `{interpolation_mode}` interpolation. + + - sample grid: `{sample_accuracy.sample_grid_width} x {sample_accuracy.sample_grid_height}` + - max observed angular error on this sample: `{sample_accuracy.max_angular_error_mdeg:.3f} milli degrees` + - mean angular error on this sample: `{sample_accuracy.mean_angular_error_mdeg:.3f} milli degrees` + - analyzer-estimated max for `{interpolation_mode}`: `{accuracy_report.max_angular_error_mdeg[interpolation_mode]:.3f} milli degrees` + - analyzer-estimated median for `{interpolation_mode}`: `{accuracy_report.median_angular_error_mdeg[interpolation_mode]:.3f} milli degrees` + """) + return + + +@app.cell +def _(analyzer, interpolation_mode): + accuracy_report = analyzer.estimate_accuracy(interpolations=interpolation_mode) + sample_accuracy = analyzer.sample_accuracy_grid( + interpolation=interpolation_mode, + target_sample_count=2500, + ) + return accuracy_report, sample_accuracy + + +@app.cell(hide_code=True) +def _(heatmap_path, interpolation_mode): + mo.md(f""" + ## Error heatmap + + Computed the heatmap on demand from the loaded LUT, exported `{heatmap_path.name}`, + reloaded it from disk, and plotted the per-cell maximum angular error for + `{interpolation_mode}` interpolation in `milli degrees`. + + The cyan arrows show the direction of the local peak `x/y` + interpolation error within each sampled cell. + """) + return + + +@app.cell +def _(analyzer, controls, interpolation_mode, output_dir): + heatmap_filename = f"opencv_stride_{int(controls.pixel_stride)}_{controls.storage_encoding}_{interpolation_mode}_error_heatmap.npz" + heatmap_path = output_dir / heatmap_filename + + heatmap = analyzer.compute_error_heatmap(interpolation=interpolation_mode) + heatmap.save(heatmap_path) + loaded_heatmap = UnprojectLUTErrorHeatmap.load(heatmap_path) + fig = plot_unproject_lut_error_heatmap( + loaded_heatmap, + angular_unit="mdeg", + figsize=(7.8, 5.3), + return_figure=True, + ) + mo.mpl.interactive(fig) + return (heatmap_path,) + + +@app.cell(hide_code=True) +def _(bounds_demo_pixels, clamp_rays, strict_rays, valid_mask): + mo.md(f""" + ## Bounds behavior + + `strict` keeps queries safe by flagging pixels outside the LUT domain. + + - demo pixels: + `{np.array2string(bounds_demo_pixels, precision=2, separator=", ")}` + - strict valid mask: + `{np.array2string(valid_mask, separator=", ")}` + - first strict ray: + `{np.array2string(strict_rays[0], precision=5, separator=", ")}` + - first clamped ray: + `{np.array2string(clamp_rays[0], precision=5, separator=", ")}` + """) + return + + +@app.cell +def _(loaded, model): + bounds_demo_pixels = np.array( + [ + [-20.0, 50.0], + [0.0, 0.0], + [model.image_width - 1, model.image_height - 1], + [model.image_width + 20.0, model.image_height / 2.0], + ] + ) + strict_rays, valid_mask = loaded.normalize_points( + bounds_demo_pixels, + bounds="strict", + return_valid_mask=True, + ) + clamp_rays = loaded.normalize_points(bounds_demo_pixels, bounds="clamp") + return bounds_demo_pixels, clamp_rays, strict_rays, valid_mask + + +@app.cell(hide_code=True) +def _(): + mo.md(r""" + ## C++ runtime + + The same file can be loaded from the standalone runtime in `cpp_runtime/`. Simply copy-paste those files into your project and use them like so: + + ```cpp + #include "unproject_lut.hpp" + + auto lut = lensboy::UnprojectLUT::load("camera_runtime.unproject_LUT"); + auto result = lut.query( + 1280.0, + 720.0, + lensboy::InterpolationMode::kBilinear, + lensboy::BoundsMode::kStrict + ); + + if (result.valid) { + do_stuff(result.ray); + } + ``` + """) + return + + +@app.cell +def _(): + return + + +if __name__ == "__main__": + app.run() diff --git a/mkdocs.yml b/mkdocs.yml index 333cfc3..3257378 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,4 +34,6 @@ extra_javascript: - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js nav: + - Home: index.md - Calibration Guide: calibration_guide.md + - Unproject LUT: unproject_lut.md diff --git a/src/lensboy/__init__.py b/src/lensboy/__init__.py index 95212c1..790a635 100644 --- a/src/lensboy/__init__.py +++ b/src/lensboy/__init__.py @@ -10,6 +10,7 @@ from lensboy.camera_models.opencv import OpenCV, OpenCVConfig from lensboy.camera_models.pinhole_remapped import PinholeRemapped from lensboy.camera_models.pinhole_splined import PinholeSplined, PinholeSplinedConfig +from lensboy.camera_models.unproject_lut import UnprojectLUT from lensboy.common_targets.charuco import extract_frames_from_charuco from lensboy.geometry.pose import Pose @@ -25,6 +26,7 @@ "PinholeRemapped", "PinholeSplinedConfig", "PinholeSplined", + "UnprojectLUT", "extract_frames_from_charuco", "Pose", "TargetWarp", diff --git a/src/lensboy/analysis/__init__.py b/src/lensboy/analysis/__init__.py index 2acbb5f..b11a146 100644 --- a/src/lensboy/analysis/__init__.py +++ b/src/lensboy/analysis/__init__.py @@ -1,3 +1,17 @@ +from lensboy.analysis.unproject_lut import ( + UnprojectLUTAccuracyReport, + UnprojectLUTAnalyzer, + UnprojectLUTErrorHeatmap, + UnprojectLUTSampleAccuracy, +) + +__all__ = [ + "UnprojectLUTAccuracyReport", + "UnprojectLUTAnalyzer", + "UnprojectLUTErrorHeatmap", + "UnprojectLUTSampleAccuracy", +] + try: from lensboy.analysis.plots import ( draw_points, @@ -5,17 +19,18 @@ plot_distortion_grid, plot_projection_diff, plot_undistortion, + plot_unproject_lut_error_heatmap, + ) +except ImportError: + pass +else: + __all__.extend( + [ + "draw_points", + "plot_detection_coverage", + "plot_distortion_grid", + "plot_projection_diff", + "plot_unproject_lut_error_heatmap", + "plot_undistortion", + ] ) -except ImportError as e: - raise ImportError( - "The analysis module requires extra dependencies. " - "Install them with: pip install lensboy[analysis]" - ) from e - -__all__ = [ - "draw_points", - "plot_detection_coverage", - "plot_distortion_grid", - "plot_projection_diff", - "plot_undistortion", -] diff --git a/src/lensboy/analysis/plots.py b/src/lensboy/analysis/plots.py index 04868c3..981de6a 100644 --- a/src/lensboy/analysis/plots.py +++ b/src/lensboy/analysis/plots.py @@ -1,5 +1,6 @@ from __future__ import annotations +from pathlib import Path from typing import Literal import cv2 @@ -2149,6 +2150,186 @@ def _add_masked_line( return None +def plot_unproject_lut_error_heatmap( + heatmap: UnprojectLUTErrorHeatmap | Path | str, + *, + title: str | None = None, + angular_unit: Literal["deg", "mdeg", "udeg", "rad", "mrad", "urad"] = "mdeg", + show_directions: bool = True, + arrow_grid: int = 28, + arrow_scale: float = 0.5, + cmap_name: str = "inferno", + figsize: tuple[float, float] = (8.5, 6.0), + return_figure: bool = False, +) -> Figure | None: + """Plot an unproject LUT error heatmap. + + Draws the per-cell maximum angular error as a heatmap and can overlay the + direction of the local peak x/y interpolation error in each cell. + + Args: + heatmap: In-memory heatmap object or path to a saved heatmap archive. + title: Plot title. Uses the archive interpolation mode when omitted. + angular_unit: Angular units for the heatmap color scale. + show_directions: Whether to draw the error-direction arrows. + arrow_grid: Approximate maximum number of arrows along the longer heatmap axis. + arrow_scale: Arrow length as a fraction of the spacing between drawn arrows. + cmap_name: Matplotlib colormap name for the heatmap. + figsize: Figure size in inches as ``(width, height)``. + return_figure: If True, return the figure instead of calling ``plt.show()``. + + Returns: + The figure if ``return_figure`` is True, otherwise None. + """ + from lensboy.analysis.unproject_lut import UnprojectLUTErrorHeatmap + + if isinstance(heatmap, (str, Path)): + heatmap_obj = UnprojectLUTErrorHeatmap.load(heatmap) + else: + heatmap_obj = heatmap + + x_edges = np.asarray(heatmap_obj.cell_x_edges, dtype=np.float64).copy() + y_edges = np.asarray(heatmap_obj.cell_y_edges, dtype=np.float64).copy() + max_angular_error_deg = np.asarray( + heatmap_obj.max_angular_error_deg, dtype=np.float64 + ).copy() + error_direction_xy = np.asarray( + heatmap_obj.error_direction_xy, dtype=np.float64 + ).copy() + interpolation = heatmap_obj.interpolation + + angular_unit_scales = { + "deg": (1.0, "degrees"), + "mdeg": (1.0e3, "milli degrees"), + "udeg": (1.0e6, "micro degrees"), + "rad": (np.pi / 180.0, "radians"), + "mrad": (np.pi / 180.0 * 1.0e3, "milli radians"), + "urad": (np.pi / 180.0 * 1.0e6, "micro radians"), + } + if angular_unit not in angular_unit_scales: + raise ValueError( + "angular_unit must be one of " + f"{tuple(angular_unit_scales)}, got {angular_unit!r}." + ) + unit_scale, unit_label = angular_unit_scales[angular_unit] + max_angular_error = max_angular_error_deg * unit_scale + + if max_angular_error_deg.ndim != 2: + raise ValueError( + "max_angular_error_deg must have shape (H, W), " + f"got {max_angular_error_deg.shape}." + ) + if error_direction_xy.shape != (*max_angular_error_deg.shape, 2): + raise ValueError( + "error_direction_xy must have shape (H, W, 2), " + f"got {error_direction_xy.shape}." + ) + heatmap_height, heatmap_width = max_angular_error_deg.shape + valid_x_edge_sizes = {1} if heatmap_width == 1 else {heatmap_width + 1} + valid_y_edge_sizes = {1} if heatmap_height == 1 else {heatmap_height + 1} + if x_edges.ndim != 1 or x_edges.size not in valid_x_edge_sizes: + raise ValueError( + f"cell_x_edges must have size {sorted(valid_x_edge_sizes)}, " + f"got {x_edges.shape} for heatmap width {heatmap_width}." + ) + if y_edges.ndim != 1 or y_edges.size not in valid_y_edge_sizes: + raise ValueError( + f"cell_y_edges must have size {sorted(valid_y_edge_sizes)}, " + f"got {y_edges.shape} for heatmap height {heatmap_height}." + ) + + bg = "#111111" + fg = "white" + accent = "#00d4ff" + + fig, ax = plt.subplots(figsize=figsize) + fig.patch.set_facecolor(bg) + ax.set_facecolor(bg) + ax.tick_params(colors=fg) + ax.xaxis.label.set_color(fg) + ax.yaxis.label.set_color(fg) + ax.title.set_color(fg) + for spine in ax.spines.values(): + spine.set_color(fg) + + x_extent_min = float(x_edges[0]) + x_extent_max = float(x_edges[-1]) + y_extent_min = float(y_edges[0]) + y_extent_max = float(y_edges[-1]) + if x_extent_min == x_extent_max: + x_extent_min -= 0.5 + x_extent_max += 0.5 + if y_extent_min == y_extent_max: + y_extent_min -= 0.5 + y_extent_max += 0.5 + + im = ax.imshow( + max_angular_error, + cmap=cmap_name, + extent=[x_extent_min, x_extent_max, y_extent_max, y_extent_min], # type: ignore[arg-type] + aspect="equal", + ) + cbar: Colorbar = fig.colorbar(im, ax=ax, shrink=0.8, fraction=0.035, pad=0.02) + cbar.set_label(f"max angular error [{unit_label}]", color=fg) + cbar.ax.tick_params(colors=fg) + + if show_directions: + if x_edges.size > 1: + x_centers = 0.5 * (x_edges[:-1] + x_edges[1:]) + x_cell_size = float(np.min(np.abs(np.diff(x_edges)))) + else: + x_centers = x_edges + x_cell_size = 1.0 + if y_edges.size > 1: + y_centers = 0.5 * (y_edges[:-1] + y_edges[1:]) + y_cell_size = float(np.min(np.abs(np.diff(y_edges)))) + else: + y_centers = y_edges + y_cell_size = 1.0 + + center_x, center_y = np.meshgrid(x_centers, y_centers, indexing="xy") + quiver_stride = max( + 1, + int(np.ceil(max(max_angular_error_deg.shape) / max(int(arrow_grid), 1))), + ) + arrow_spacing = quiver_stride * min(x_cell_size, y_cell_size) + arrow_length = arrow_scale * arrow_spacing + quiver_slice = ( + slice(None, None, quiver_stride), + slice(None, None, quiver_stride), + ) + quiver_mask = max_angular_error_deg[quiver_slice] > 0.0 + + ax.quiver( + center_x[quiver_slice][quiver_mask], + center_y[quiver_slice][quiver_mask], + error_direction_xy[..., 0][quiver_slice][quiver_mask] * arrow_length, + error_direction_xy[..., 1][quiver_slice][quiver_mask] * arrow_length, + color=accent, + angles="xy", + scale_units="xy", + scale=1.0, + width=0.0022, + headwidth=3, + headlength=3.5, + headaxislength=3, + ) + + if title is None: + title = f"Per-cell max error heatmap ({interpolation})" + ax.set_title(f"{title} [{unit_label}]") + ax.set_xlabel("x [px]") + ax.set_ylabel("y [px]") + ax.set_xlim(x_extent_min, x_extent_max) + ax.set_ylim(y_extent_max, y_extent_min) + + plt.tight_layout() + if return_figure: + return fig + plt.show() + return None + + def _plot_per_image_rms( frame_diagnostics: list[lb.FrameDiagnostics | None], *, diff --git a/src/lensboy/analysis/unproject_lut.py b/src/lensboy/analysis/unproject_lut.py new file mode 100644 index 0000000..6155b7a --- /dev/null +++ b/src/lensboy/analysis/unproject_lut.py @@ -0,0 +1,1003 @@ +from __future__ import annotations + +import json +from collections.abc import Callable +from dataclasses import dataclass, field +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import numpy as np + +from lensboy.camera_models.unproject_lut import InterpolationMode, UnprojectLUT + +if TYPE_CHECKING: + from matplotlib.figure import Figure + + from lensboy.camera_models.base_model import CameraModel + +_SUPPORTED_INTERPOLATIONS: tuple[InterpolationMode, ...] = ( + "nearest", + "bilinear", + "bicubic", +) +_DEFAULT_ERROR_MAX_DEPTH = 2 +_DEFAULT_ERROR_MIN_CELL_SIZE = 0.5 +_ANGULAR_ERROR_MDEG_SCALE = 1.0e3 + + +def _validate_target_sample_count(target_sample_count: int) -> int: + resolved = int(target_sample_count) + if resolved <= 0: + raise ValueError("target_sample_count must be positive.") + return resolved + + +def _validate_interpolation_mode(interpolation: str) -> InterpolationMode: + if interpolation not in _SUPPORTED_INTERPOLATIONS: + raise ValueError( + f"Unsupported interpolation mode {interpolation!r}. " + f"Expected one of {_SUPPORTED_INTERPOLATIONS}." + ) + return interpolation # type: ignore[return-value] + + +def _validate_error_mode(mode: str) -> str: + if mode != "adaptive": + raise ValueError(f"Unsupported error mode {mode!r}.") + return mode + + +def _normalize_interpolations( + interpolations: InterpolationMode + | tuple[InterpolationMode, ...] + | list[InterpolationMode], +) -> tuple[InterpolationMode, ...]: + if isinstance(interpolations, str): + raw_items = [interpolations] + else: + raw_items = list(interpolations) + + normalized: list[InterpolationMode] = [] + seen: set[str] = set() + for item in raw_items: + mode = _validate_interpolation_mode(item) + if mode in seen: + continue + normalized.append(mode) + seen.add(mode) + if len(normalized) == 0: + raise ValueError("interpolations must be non-empty.") + return tuple(normalized) + + +def _serialize_source_model_spec(source_model_spec: dict[str, Any] | None) -> str | None: + if source_model_spec is None: + return None + return json.dumps( + source_model_spec, + separators=(",", ":"), + sort_keys=True, + ensure_ascii=True, + ) + + +def _make_camera_model_from_spec(source_model_spec: dict[str, Any]) -> CameraModel: + model_type = source_model_spec.get("type") + if model_type == "opencv": + from lensboy.camera_models.opencv import OpenCV + + return OpenCV.from_json(source_model_spec) + if model_type == "pinhole_splined": + from lensboy.camera_models.pinhole_splined import PinholeSplined + + return PinholeSplined.from_json(source_model_spec) + raise ValueError(f"Unsupported source_model_spec type {model_type!r}.") + + +def _make_normalize_points_fn( + source_model_spec_json: str, +) -> Callable[[np.ndarray], np.ndarray]: + source_model_spec = json.loads(source_model_spec_json) + model_type = source_model_spec.get("type") + if model_type == "pinhole_remapped": + fx = float(source_model_spec["fx"]) + fy = float(source_model_spec["fy"]) + cx = float(source_model_spec["cx"]) + cy = float(source_model_spec["cy"]) + + def normalize_points(pixel_coords: np.ndarray) -> np.ndarray: + pts = np.asarray(pixel_coords, dtype=np.float64) + x = (pts[:, 0] - cx) / fx + y = (pts[:, 1] - cy) / fy + return np.column_stack([x, y, np.ones(len(pts), dtype=np.float64)]) + + return normalize_points + + model = _make_camera_model_from_spec(source_model_spec) + return model.normalize_points + + +def _angular_error_deg_from_xy( + reference_xy: np.ndarray, + approx_xy: np.ndarray, +) -> np.ndarray: + ref_x = reference_xy[:, 0] + ref_y = reference_xy[:, 1] + approx_x = approx_xy[:, 0] + approx_y = approx_xy[:, 1] + dot = ref_x * approx_x + ref_y * approx_y + 1.0 + ref_norm = np.sqrt(ref_x * ref_x + ref_y * ref_y + 1.0) + approx_norm = np.sqrt(approx_x * approx_x + approx_y * approx_y + 1.0) + return np.rad2deg(np.arccos(np.clip(dot / (ref_norm * approx_norm), -1.0, 1.0))) + + +def _dense_sample_grid( + *, + image_width: int, + image_height: int, + target_sample_count: int, +) -> tuple[int, int, np.ndarray]: + image_aspect = image_width / image_height + sample_grid_width = max( + 2, + int(round(np.sqrt(target_sample_count * image_aspect))), + ) + sample_grid_height = max( + 2, + int(round(target_sample_count / sample_grid_width)), + ) + xs = np.linspace(0.0, image_width - 1, sample_grid_width) + ys = np.linspace(0.0, image_height - 1, sample_grid_height) + grid_x, grid_y = np.meshgrid(xs, ys, indexing="xy") + sample_pixels = np.column_stack([grid_x.ravel(), grid_y.ravel()]) + return sample_grid_width, sample_grid_height, sample_pixels + + +def _sample_cell_points(x0: float, x1: float, y0: float, y1: float) -> np.ndarray: + x_mid = 0.5 * (x0 + x1) + y_mid = 0.5 * (y0 + y1) + return np.array( + [ + [x0, y0], + [x1, y0], + [x0, y1], + [x1, y1], + [x_mid, y0], + [x_mid, y1], + [x0, y_mid], + [x1, y_mid], + [x_mid, y_mid], + ], + dtype=np.float64, + ) + + +def _sample_cell_points_batch( + x0: np.ndarray, + x1: np.ndarray, + y0: np.ndarray, + y1: np.ndarray, +) -> np.ndarray: + x_mid = 0.5 * (x0 + x1) + y_mid = 0.5 * (y0 + y1) + points = np.empty((len(x0), 9, 2), dtype=np.float64) + points[:, 0, 0] = x0 + points[:, 0, 1] = y0 + points[:, 1, 0] = x1 + points[:, 1, 1] = y0 + points[:, 2, 0] = x0 + points[:, 2, 1] = y1 + points[:, 3, 0] = x1 + points[:, 3, 1] = y1 + points[:, 4, 0] = x_mid + points[:, 4, 1] = y0 + points[:, 5, 0] = x_mid + points[:, 5, 1] = y1 + points[:, 6, 0] = x0 + points[:, 6, 1] = y_mid + points[:, 7, 0] = x1 + points[:, 7, 1] = y_mid + points[:, 8, 0] = x_mid + points[:, 8, 1] = y_mid + return points + + +def _subdivide_cell( + x0: float, + x1: float, + y0: float, + y1: float, +) -> list[tuple[float, float, float, float]]: + x_mid = 0.5 * (x0 + x1) + y_mid = 0.5 * (y0 + y1) + return [ + (x0, x_mid, y0, y_mid), + (x_mid, x1, y0, y_mid), + (x0, x_mid, y_mid, y1), + (x_mid, x1, y_mid, y1), + ] + + +def _estimate_cells_error_detail_batch( + lut: UnprojectLUT, + normalize_points_fn: Callable[[np.ndarray], np.ndarray], + *, + mode: InterpolationMode, + x0: np.ndarray, + x1: np.ndarray, + y0: np.ndarray, + y1: np.ndarray, +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + sample_points = _sample_cell_points_batch(x0, x1, y0, y1) + flat_points = sample_points.reshape(-1, 2) + exact_rays = normalize_points_fn(flat_points) + exact_xy = np.asarray(exact_rays[:, :2], dtype=np.float64) + approx_xy = lut._interpolate_xy( + flat_points[:, 0], + flat_points[:, 1], + mode, + "strict", + ) + errors = _angular_error_deg_from_xy(exact_xy, approx_xy).reshape(len(x0), 9) + error_delta_xy = (approx_xy - exact_xy).reshape(len(x0), 9, 2) + + best_indices = np.argmax(errors, axis=1) + row_indices = np.arange(len(x0)) + max_errors = errors[row_indices, best_indices] + best_delta_xy = error_delta_xy[row_indices, best_indices] + delta_norm = np.linalg.norm(best_delta_xy, axis=1, keepdims=True) + best_direction_xy = np.divide( + best_delta_xy, + delta_norm, + out=np.zeros_like(best_delta_xy), + where=delta_norm > 0.0, + ) + best_peak_pixel = sample_points[row_indices, best_indices] + interior_peak = np.max(errors[:, 4:], axis=1) > ( + np.max(errors[:, :4], axis=1) + 1e-12 + ) + sampled_errors_deg = errors.reshape(-1).copy() + return ( + max_errors, + best_direction_xy, + best_delta_xy, + best_peak_pixel, + interior_peak, + sampled_errors_deg, + ) + + +def _estimate_cell_error_detail( + lut: UnprojectLUT, + normalize_points_fn: Callable[[np.ndarray], np.ndarray], + *, + mode: InterpolationMode, + x0: float, + x1: float, + y0: float, + y1: float, + depth: int, + max_depth: int, + min_cell_size: float, + sampled_errors_deg: list[float], +) -> tuple[float, np.ndarray, np.ndarray, np.ndarray]: + sample_points = _sample_cell_points(x0, x1, y0, y1) + exact_rays = normalize_points_fn(sample_points) + exact_xy = np.asarray(exact_rays[:, :2], dtype=np.float64) + approx_xy = lut._interpolate_xy( + sample_points[:, 0], + sample_points[:, 1], + mode, + "strict", + ) + errors = _angular_error_deg_from_xy(exact_xy, approx_xy) + sampled_errors_deg.extend(errors.tolist()) + error_delta_xy = approx_xy - exact_xy + best_index = int(np.argmax(errors)) + max_error = float(errors[best_index]) + best_delta_xy = np.asarray(error_delta_xy[best_index], dtype=np.float64) + delta_norm = float(np.linalg.norm(best_delta_xy)) + if delta_norm > 0.0: + best_direction_xy = best_delta_xy / delta_norm + else: + best_direction_xy = np.zeros(2, dtype=np.float64) + best_peak_pixel = np.asarray(sample_points[best_index], dtype=np.float64) + + interior_peak = float(np.max(errors[4:])) > float(np.max(errors[:4])) + 1e-12 + cell_width = abs(x1 - x0) + cell_height = abs(y1 - y0) + can_subdivide = depth < max_depth and ( + cell_width > min_cell_size or cell_height > min_cell_size + ) + if not (interior_peak and can_subdivide): + return max_error, best_direction_xy, best_delta_xy, best_peak_pixel + + for subcell in _subdivide_cell(x0, x1, y0, y1): + ( + subcell_max_error, + subcell_direction_xy, + subcell_delta_xy, + subcell_peak_pixel, + ) = _estimate_cell_error_detail( + lut, + normalize_points_fn, + mode=mode, + x0=subcell[0], + x1=subcell[1], + y0=subcell[2], + y1=subcell[3], + depth=depth + 1, + max_depth=max_depth, + min_cell_size=min_cell_size, + sampled_errors_deg=sampled_errors_deg, + ) + if subcell_max_error > max_error: + max_error = subcell_max_error + best_direction_xy = subcell_direction_xy + best_delta_xy = subcell_delta_xy + best_peak_pixel = subcell_peak_pixel + return max_error, best_direction_xy, best_delta_xy, best_peak_pixel + + +def _estimate_adaptive_errors_for_cell_chunk( + lut: UnprojectLUT, + normalize_points_fn: Callable[[np.ndarray], np.ndarray], + *, + mode: InterpolationMode, + x0: np.ndarray, + x1: np.ndarray, + y0: np.ndarray, + y1: np.ndarray, + max_depth: int, + min_cell_size: float, +) -> tuple[float, np.ndarray]: + ( + top_level_max_errors, + _top_level_direction_xy, + _top_level_delta_xy, + _top_level_peak_pixel, + interior_peak, + top_level_sampled_errors_deg, + ) = _estimate_cells_error_detail_batch( + lut, + normalize_points_fn, + mode=mode, + x0=x0, + x1=x1, + y0=y0, + y1=y1, + ) + + max_error = float(np.max(top_level_max_errors)) + sampled_error_arrays: list[np.ndarray] = [top_level_sampled_errors_deg] + cell_widths = np.abs(x1 - x0) + cell_heights = np.abs(y1 - y0) + can_subdivide = (cell_widths > min_cell_size) | (cell_heights > min_cell_size) + recurse_indices = np.flatnonzero(interior_peak & can_subdivide) + + if len(recurse_indices) == 0: + return max_error, top_level_sampled_errors_deg + + sampled_errors_deg: list[float] = [] + for cell_index in recurse_indices: + max_error = max( + max_error, + _estimate_cell_error_detail( + lut, + normalize_points_fn, + mode=mode, + x0=float(x0[cell_index]), + x1=float(x1[cell_index]), + y0=float(y0[cell_index]), + y1=float(y1[cell_index]), + depth=0, + max_depth=max_depth, + min_cell_size=min_cell_size, + sampled_errors_deg=sampled_errors_deg, + )[0], + ) + + if len(sampled_errors_deg) > 0: + sampled_error_arrays.append(np.asarray(sampled_errors_deg, dtype=np.float64)) + return max_error, np.concatenate(sampled_error_arrays) + + +@dataclass(frozen=True) +class UnprojectLUTAccuracyReport: + """Accuracy summary for one or more LUT interpolation modes. + + Args: + interpolations: Interpolation modes included in the report. + max_angular_error_mdeg: Observed maximum angular error per interpolation mode. + median_angular_error_mdeg: Observed median angular error per interpolation mode. + mode: Error-estimation mode used to build the report. + max_depth: Maximum adaptive subdivision depth. + min_cell_size: Minimum subcell size in pixels. + + Returns: + Immutable report describing the requested interpolation modes. + """ + + interpolations: tuple[InterpolationMode, ...] + max_angular_error_mdeg: dict[str, float] + median_angular_error_mdeg: dict[str, float] + mode: str + max_depth: int + min_cell_size: float + + +@dataclass +class UnprojectLUTSampleAccuracy: + """Dense sampled comparison between a LUT and its exact source model. + + Args: + interpolation: Interpolation mode used to query the LUT. + target_sample_count: Requested approximate number of sample pixels. + sample_grid_width: Sample-grid width. + sample_grid_height: Sample-grid height. + sample_pixels: Evenly spaced sample pixels with shape ``(N, 2)``. + exact_rays: Exact source-model rays with shape ``(N, 3)``. + approx_rays: LUT-queried rays with shape ``(N, 3)``. + angular_error_deg: Per-sample angular error in degrees, shape ``(N,)``. + + Returns: + In-memory sampled comparison result for one interpolation mode. + """ + + interpolation: InterpolationMode + target_sample_count: int + sample_grid_width: int + sample_grid_height: int + sample_pixels: np.ndarray + exact_rays: np.ndarray + approx_rays: np.ndarray + angular_error_deg: np.ndarray + + def __post_init__(self) -> None: + self.interpolation = _validate_interpolation_mode(self.interpolation) + self.target_sample_count = _validate_target_sample_count(self.target_sample_count) + self.sample_grid_width = int(self.sample_grid_width) + self.sample_grid_height = int(self.sample_grid_height) + self.sample_pixels = np.asarray(self.sample_pixels, dtype=np.float64).copy() + self.exact_rays = np.asarray(self.exact_rays, dtype=np.float64).copy() + self.approx_rays = np.asarray(self.approx_rays, dtype=np.float64).copy() + self.angular_error_deg = np.asarray( + self.angular_error_deg, dtype=np.float64 + ).copy() + + expected_samples = self.sample_grid_width * self.sample_grid_height + if self.sample_grid_width < 2 or self.sample_grid_height < 2: + raise ValueError("sample grid dimensions must both be at least 2.") + if self.sample_pixels.shape != (expected_samples, 2): + raise ValueError( + "sample_pixels must have shape " + f"({expected_samples}, 2), got {self.sample_pixels.shape}." + ) + if self.exact_rays.shape != (expected_samples, 3): + raise ValueError( + "exact_rays must have shape " + f"({expected_samples}, 3), got {self.exact_rays.shape}." + ) + if self.approx_rays.shape != (expected_samples, 3): + raise ValueError( + "approx_rays must have shape " + f"({expected_samples}, 3), got {self.approx_rays.shape}." + ) + if self.angular_error_deg.shape != (expected_samples,): + raise ValueError( + "angular_error_deg must have shape " + f"({expected_samples},), got {self.angular_error_deg.shape}." + ) + + @property + def sample_count(self) -> int: + """Return the number of sampled pixels. + + Returns: + Number of sample pixels in the dense comparison grid. + """ + return int(len(self.angular_error_deg)) + + @property + def max_angular_error_mdeg(self) -> float: + """Return the maximum sampled angular error in milli degrees. + + Returns: + Maximum per-sample angular error. + """ + return float(np.max(self.angular_error_deg) * _ANGULAR_ERROR_MDEG_SCALE) + + @property + def mean_angular_error_mdeg(self) -> float: + """Return the mean sampled angular error in milli degrees. + + Returns: + Mean per-sample angular error. + """ + return float(np.mean(self.angular_error_deg) * _ANGULAR_ERROR_MDEG_SCALE) + + @property + def median_angular_error_mdeg(self) -> float: + """Return the median sampled angular error in milli degrees. + + Returns: + Median per-sample angular error. + """ + return float(np.median(self.angular_error_deg) * _ANGULAR_ERROR_MDEG_SCALE) + + +@dataclass +class UnprojectLUTErrorHeatmap: + """Per-cell angular-error heatmap for a LUT interpolation mode. + + Args: + interpolation: Interpolation mode represented by the heatmap. + max_depth: Maximum adaptive subdivision depth. + min_cell_size: Minimum subcell size in pixels. + cell_x_edges: Cell x edges with shape ``(grid_width,)`` or ``(grid_width + 1,)``. + cell_y_edges: Cell y edges with shape ``(grid_height,)`` or ``(grid_height + 1,)``. + max_angular_error_deg: Per-cell maximum angular error, shape ``(H, W)``. + error_direction_xy: Unit x/y direction of the local peak error, shape ``(H, W, 2)``. + error_delta_xy: Peak x/y interpolation error vector, shape ``(H, W, 2)``. + peak_pixel_xy: Pixel location of the local peak error, shape ``(H, W, 2)``. + + Returns: + In-memory representation of a saved or computed heatmap. + """ + + interpolation: InterpolationMode + max_depth: int + min_cell_size: float + cell_x_edges: np.ndarray + cell_y_edges: np.ndarray + max_angular_error_deg: np.ndarray + error_direction_xy: np.ndarray + error_delta_xy: np.ndarray + peak_pixel_xy: np.ndarray + + def __post_init__(self) -> None: + self.interpolation = _validate_interpolation_mode(self.interpolation) + self.max_depth = int(self.max_depth) + self.min_cell_size = float(self.min_cell_size) + self.cell_x_edges = np.asarray(self.cell_x_edges, dtype=np.float64).copy() + self.cell_y_edges = np.asarray(self.cell_y_edges, dtype=np.float64).copy() + self.max_angular_error_deg = np.asarray( + self.max_angular_error_deg, dtype=np.float64 + ).copy() + self.error_direction_xy = np.asarray( + self.error_direction_xy, dtype=np.float64 + ).copy() + self.error_delta_xy = np.asarray(self.error_delta_xy, dtype=np.float64).copy() + self.peak_pixel_xy = np.asarray(self.peak_pixel_xy, dtype=np.float64).copy() + + if self.max_angular_error_deg.ndim != 2: + raise ValueError( + "max_angular_error_deg must have shape (H, W), " + f"got {self.max_angular_error_deg.shape}." + ) + expected_vector_shape = (*self.max_angular_error_deg.shape, 2) + if self.error_direction_xy.shape != expected_vector_shape: + raise ValueError( + "error_direction_xy must have shape (H, W, 2), " + f"got {self.error_direction_xy.shape}." + ) + if self.error_delta_xy.shape != expected_vector_shape: + raise ValueError( + "error_delta_xy must have shape (H, W, 2), " + f"got {self.error_delta_xy.shape}." + ) + if self.peak_pixel_xy.shape != expected_vector_shape: + raise ValueError( + "peak_pixel_xy must have shape (H, W, 2), " + f"got {self.peak_pixel_xy.shape}." + ) + + def save(self, path: Path | str) -> None: + """Save the heatmap to a compressed NumPy archive. + + Args: + path: Output `.npz` path. + + Returns: + None. + """ + np.savez_compressed( + Path(path), + interpolation=np.array(self.interpolation), + max_depth=np.array(self.max_depth, dtype=np.int64), + min_cell_size=np.array(self.min_cell_size, dtype=np.float64), + cell_x_edges=self.cell_x_edges, + cell_y_edges=self.cell_y_edges, + max_angular_error_deg=self.max_angular_error_deg, + error_direction_xy=self.error_direction_xy, + error_delta_xy=self.error_delta_xy, + peak_pixel_xy=self.peak_pixel_xy, + ) + + @staticmethod + def load(path: Path | str) -> UnprojectLUTErrorHeatmap: + """Load a saved heatmap archive. + + Args: + path: Path to a `.npz` archive written by ``save()``. + + Returns: + Loaded heatmap object. + """ + with np.load(Path(path)) as heatmap_data: + return UnprojectLUTErrorHeatmap( + interpolation=str(np.asarray(heatmap_data["interpolation"]).item()), + max_depth=int(np.asarray(heatmap_data["max_depth"]).item()), + min_cell_size=float(np.asarray(heatmap_data["min_cell_size"]).item()), + cell_x_edges=np.asarray(heatmap_data["cell_x_edges"], dtype=np.float64), + cell_y_edges=np.asarray(heatmap_data["cell_y_edges"], dtype=np.float64), + max_angular_error_deg=np.asarray( + heatmap_data["max_angular_error_deg"], dtype=np.float64 + ), + error_direction_xy=np.asarray( + heatmap_data["error_direction_xy"], dtype=np.float64 + ), + error_delta_xy=np.asarray( + heatmap_data["error_delta_xy"], dtype=np.float64 + ), + peak_pixel_xy=np.asarray(heatmap_data["peak_pixel_xy"], dtype=np.float64), + ) + + +@dataclass +class UnprojectLUTAnalyzer: + """Estimate accuracy statistics and heatmaps for a saved LUT. + + Args: + lut: Runtime LUT to analyze. + + Returns: + Analyzer that can compute error summaries and heatmaps using the LUT's + embedded exact source-model specification. + """ + + lut: UnprojectLUT + _normalize_points_exact_fn: Callable[[np.ndarray], np.ndarray] | None = field( + default=None, + init=False, + repr=False, + ) + + def estimate_accuracy( + self, + *, + interpolations: InterpolationMode + | tuple[InterpolationMode, ...] + | list[InterpolationMode] = "bilinear", + mode: str = "adaptive", + max_depth: int = _DEFAULT_ERROR_MAX_DEPTH, + min_cell_size: float = _DEFAULT_ERROR_MIN_CELL_SIZE, + ) -> UnprojectLUTAccuracyReport: + """Estimate angular interpolation accuracy for one or more modes. + + Args: + interpolations: Interpolation modes to include in the report. + mode: Error-estimation mode. Only ``"adaptive"`` is supported. + max_depth: Maximum adaptive subdivision depth. + min_cell_size: Minimum subcell size in pixels. + + Returns: + Accuracy report for the requested interpolation modes. + """ + normalized_interpolations = _normalize_interpolations(interpolations) + mode = _validate_error_mode(mode) + max_errors_mdeg, median_errors_mdeg = self._estimate_adaptive_errors( + interpolations=normalized_interpolations, + max_depth=max_depth, + min_cell_size=min_cell_size, + ) + return UnprojectLUTAccuracyReport( + interpolations=normalized_interpolations, + max_angular_error_mdeg=max_errors_mdeg, + median_angular_error_mdeg=median_errors_mdeg, + mode=mode, + max_depth=max_depth, + min_cell_size=min_cell_size, + ) + + def compute_error_heatmap( + self, + *, + interpolation: InterpolationMode = "bilinear", + mode: str = "adaptive", + max_depth: int = _DEFAULT_ERROR_MAX_DEPTH, + min_cell_size: float = _DEFAULT_ERROR_MIN_CELL_SIZE, + ) -> UnprojectLUTErrorHeatmap: + """Compute a per-cell error heatmap for one interpolation mode. + + Args: + interpolation: Interpolation mode to evaluate. + mode: Error-estimation mode. Only ``"adaptive"`` is supported. + max_depth: Maximum adaptive subdivision depth. + min_cell_size: Minimum subcell size in pixels. + + Returns: + In-memory heatmap for the requested interpolation mode. + """ + interpolation = _validate_interpolation_mode(interpolation) + _validate_error_mode(mode) + + x_edges = np.linspace( + self.lut.grid_x_min, self.lut.grid_x_max, self.lut.grid_width + ) + y_edges = np.linspace( + self.lut.grid_y_min, self.lut.grid_y_max, self.lut.grid_height + ) + heatmap_width = max(self.lut.grid_width - 1, 1) + heatmap_height = max(self.lut.grid_height - 1, 1) + max_angular_error_deg = np.zeros( + (heatmap_height, heatmap_width), dtype=np.float64 + ) + error_direction_xy = np.zeros( + (heatmap_height, heatmap_width, 2), dtype=np.float64 + ) + error_delta_xy = np.zeros((heatmap_height, heatmap_width, 2), dtype=np.float64) + peak_pixel_xy = np.zeros((heatmap_height, heatmap_width, 2), dtype=np.float64) + + normalize_points_fn = self._normalize_points_exact() + for iy in range(heatmap_height): + y0 = y_edges[min(iy, len(y_edges) - 1)] + y1 = y_edges[min(iy + 1, len(y_edges) - 1)] + for ix in range(heatmap_width): + x0 = x_edges[min(ix, len(x_edges) - 1)] + x1 = x_edges[min(ix + 1, len(x_edges) - 1)] + ( + max_error, + direction_xy, + delta_xy, + peak_pixel, + ) = _estimate_cell_error_detail( + self.lut, + normalize_points_fn, + mode=interpolation, + x0=x0, + x1=x1, + y0=y0, + y1=y1, + depth=0, + max_depth=max_depth, + min_cell_size=min_cell_size, + sampled_errors_deg=[], + ) + max_angular_error_deg[iy, ix] = max_error + error_direction_xy[iy, ix] = direction_xy + error_delta_xy[iy, ix] = delta_xy + peak_pixel_xy[iy, ix] = peak_pixel + + return UnprojectLUTErrorHeatmap( + interpolation=interpolation, + max_depth=max_depth, + min_cell_size=min_cell_size, + cell_x_edges=x_edges, + cell_y_edges=y_edges, + max_angular_error_deg=max_angular_error_deg, + error_direction_xy=error_direction_xy, + error_delta_xy=error_delta_xy, + peak_pixel_xy=peak_pixel_xy, + ) + + def sample_accuracy_grid( + self, + *, + interpolation: InterpolationMode = "bilinear", + target_sample_count: int = 2500, + ) -> UnprojectLUTSampleAccuracy: + """Sample LUT accuracy on an evenly spaced image grid. + + Args: + interpolation: Interpolation mode to evaluate. + target_sample_count: Approximate number of evenly spaced sample pixels. + + Returns: + Dense sampled comparison result between the LUT and the exact source + model. + """ + interpolation = _validate_interpolation_mode(interpolation) + target_sample_count = _validate_target_sample_count(target_sample_count) + ( + sample_grid_width, + sample_grid_height, + sample_pixels, + ) = _dense_sample_grid( + image_width=self.lut.image_width, + image_height=self.lut.image_height, + target_sample_count=target_sample_count, + ) + exact_rays = self._normalize_points_exact()(sample_pixels) + approx_rays = self.lut.normalize_points( + sample_pixels, + interpolation=interpolation, + bounds="strict", + ) + angular_error_deg = _angular_error_deg_from_xy( + np.asarray(exact_rays[:, :2], dtype=np.float64), + np.asarray(approx_rays[:, :2], dtype=np.float64), + ) + return UnprojectLUTSampleAccuracy( + interpolation=interpolation, + target_sample_count=target_sample_count, + sample_grid_width=sample_grid_width, + sample_grid_height=sample_grid_height, + sample_pixels=sample_pixels, + exact_rays=exact_rays, + approx_rays=approx_rays, + angular_error_deg=angular_error_deg, + ) + + def save_error_heatmap( + self, + path: Path | str, + *, + interpolation: InterpolationMode = "bilinear", + mode: str = "adaptive", + max_depth: int = _DEFAULT_ERROR_MAX_DEPTH, + min_cell_size: float = _DEFAULT_ERROR_MIN_CELL_SIZE, + ) -> None: + """Compute and save a heatmap archive. + + Args: + path: Output `.npz` path. + interpolation: Interpolation mode to evaluate. + mode: Error-estimation mode. Only ``"adaptive"`` is supported. + max_depth: Maximum adaptive subdivision depth. + min_cell_size: Minimum subcell size in pixels. + + Returns: + None. + """ + heatmap = self.compute_error_heatmap( + interpolation=interpolation, + mode=mode, + max_depth=max_depth, + min_cell_size=min_cell_size, + ) + heatmap.save(path) + + def plot_error_heatmap( + self, + *, + interpolation: InterpolationMode = "bilinear", + mode: str = "adaptive", + max_depth: int = _DEFAULT_ERROR_MAX_DEPTH, + min_cell_size: float = _DEFAULT_ERROR_MIN_CELL_SIZE, + title: str | None = None, + angular_unit: str = "mdeg", + show_directions: bool = True, + arrow_grid: int = 28, + arrow_scale: float = 0.5, + cmap_name: str = "inferno", + figsize: tuple[float, float] = (8.5, 6.0), + return_figure: bool = False, + ) -> Figure | None: + """Compute and plot a heatmap for one interpolation mode. + + Args: + interpolation: Interpolation mode to evaluate. + mode: Error-estimation mode. Only ``"adaptive"`` is supported. + max_depth: Maximum adaptive subdivision depth. + min_cell_size: Minimum subcell size in pixels. + title: Plot title override. + angular_unit: Angular units for the color scale. + show_directions: Whether to draw the error-direction arrows. + arrow_grid: Approximate maximum number of arrows along the longer axis. + arrow_scale: Arrow length as a fraction of the spacing between arrows. + cmap_name: Matplotlib colormap name. + figsize: Figure size in inches as ``(width, height)``. + return_figure: If True, return the figure instead of calling ``plt.show()``. + + Returns: + The figure if ``return_figure`` is True, otherwise None. + """ + heatmap = self.compute_error_heatmap( + interpolation=interpolation, + mode=mode, + max_depth=max_depth, + min_cell_size=min_cell_size, + ) + from lensboy.analysis.plots import plot_unproject_lut_error_heatmap + + return plot_unproject_lut_error_heatmap( + heatmap, + title=title, + angular_unit=angular_unit, + show_directions=show_directions, + arrow_grid=arrow_grid, + arrow_scale=arrow_scale, + cmap_name=cmap_name, + figsize=figsize, + return_figure=return_figure, + ) + + def _require_source_model_spec_json(self) -> str: + source_model_spec_json = _serialize_source_model_spec(self.lut.source_model_spec) + if source_model_spec_json is None: + raise ValueError( + "This LUT does not carry source_model_spec_json, so exact accuracy " + "analysis is unavailable." + ) + return source_model_spec_json + + def _normalize_points_exact(self) -> Callable[[np.ndarray], np.ndarray]: + if self._normalize_points_exact_fn is None: + self._normalize_points_exact_fn = _make_normalize_points_fn( + self._require_source_model_spec_json() + ) + return self._normalize_points_exact_fn + + def _estimate_adaptive_errors( + self, + *, + interpolations: tuple[InterpolationMode, ...], + max_depth: int, + min_cell_size: float, + ) -> tuple[dict[str, float], dict[str, float]]: + max_errors_mdeg: dict[str, float] = {} + median_errors_mdeg: dict[str, float] = {} + normalize_points_fn = self._normalize_points_exact() + + if self.lut.grid_width == 1 and self.lut.grid_height == 1: + sample_points = np.array( + [[self.lut.grid_x_min, self.lut.grid_y_min]], + dtype=np.float64, + ) + exact_rays = normalize_points_fn(sample_points) + exact_xy = np.asarray(exact_rays[:, :2], dtype=np.float64) + for mode in interpolations: + approx_xy = self.lut._interpolate_xy( + sample_points[:, 0], + sample_points[:, 1], + mode, + "strict", + ) + sample_errors_deg = _angular_error_deg_from_xy(exact_xy, approx_xy) + max_errors_mdeg[mode] = ( + float(np.max(sample_errors_deg)) * _ANGULAR_ERROR_MDEG_SCALE + ) + median_errors_mdeg[mode] = ( + float(np.median(sample_errors_deg)) * _ANGULAR_ERROR_MDEG_SCALE + ) + return max_errors_mdeg, median_errors_mdeg + + x_edges = np.linspace( + self.lut.grid_x_min, self.lut.grid_x_max, self.lut.grid_width + ) + y_edges = np.linspace( + self.lut.grid_y_min, self.lut.grid_y_max, self.lut.grid_height + ) + x0_cells = x_edges[: max(self.lut.grid_width - 1, 1)] + x1_cells = x_edges[1:] if self.lut.grid_width > 1 else x_edges[:1] + y0_cells = y_edges[: max(self.lut.grid_height - 1, 1)] + y1_cells = y_edges[1:] if self.lut.grid_height > 1 else y_edges[:1] + cell_x0, cell_y0 = np.meshgrid(x0_cells, y0_cells, indexing="xy") + cell_x1, cell_y1 = np.meshgrid(x1_cells, y1_cells, indexing="xy") + flat_x0 = cell_x0.ravel() + flat_x1 = cell_x1.ravel() + flat_y0 = cell_y0.ravel() + flat_y1 = cell_y1.ravel() + + for mode in interpolations: + max_error, sampled_errors_deg = _estimate_adaptive_errors_for_cell_chunk( + self.lut, + normalize_points_fn, + mode=mode, + x0=flat_x0, + x1=flat_x1, + y0=flat_y0, + y1=flat_y1, + max_depth=max_depth, + min_cell_size=min_cell_size, + ) + median_error = ( + float(np.median(sampled_errors_deg)) + if len(sampled_errors_deg) > 0 + else float("nan") + ) + max_errors_mdeg[mode] = max_error * _ANGULAR_ERROR_MDEG_SCALE + median_errors_mdeg[mode] = median_error * _ANGULAR_ERROR_MDEG_SCALE + + return max_errors_mdeg, median_errors_mdeg diff --git a/src/lensboy/camera_models/__init__.py b/src/lensboy/camera_models/__init__.py index af5cbe4..0c300c0 100644 --- a/src/lensboy/camera_models/__init__.py +++ b/src/lensboy/camera_models/__init__.py @@ -1,9 +1,13 @@ from lensboy.camera_models.opencv import OpenCV, OpenCVConfig +from lensboy.camera_models.pinhole_remapped import PinholeRemapped from lensboy.camera_models.pinhole_splined import PinholeSplined, PinholeSplinedConfig +from lensboy.camera_models.unproject_lut import UnprojectLUT __all__ = [ "OpenCV", "OpenCVConfig", + "PinholeRemapped", "PinholeSplined", "PinholeSplinedConfig", + "UnprojectLUT", ] diff --git a/src/lensboy/camera_models/base_model.py b/src/lensboy/camera_models/base_model.py index 43d0b2f..d01f470 100644 --- a/src/lensboy/camera_models/base_model.py +++ b/src/lensboy/camera_models/base_model.py @@ -2,9 +2,16 @@ from abc import ABC, abstractmethod from dataclasses import dataclass +from typing import TYPE_CHECKING import numpy as np +if TYPE_CHECKING: + from lensboy.camera_models.unproject_lut import ( + StorageEncoding, + UnprojectLUT, + ) + @dataclass class CameraModelConfig(ABC): @@ -39,3 +46,35 @@ def normalize_points(self, pixel_coords: np.ndarray) -> np.ndarray: Normalized points in camera frame, shape (N, 3) with z=1. """ ... + + def get_unproject_lut( + self, + *, + grid_size_wh: tuple[int, int] | None = None, + pixel_stride: float | tuple[float, float] | None = None, + storage_encoding: StorageEncoding = "float64_xy", + num_workers: int | None = None, + ) -> UnprojectLUT: + """Build a lookup table that caches `normalize_points()` over a regular grid. + + Args: + grid_size_wh: Number of cached samples as (width, height). If None, + a per-pixel grid is used unless `pixel_stride` is given. + pixel_stride: Approximate sample spacing in pixels. Mutually exclusive + with `grid_size_wh`. + storage_encoding: On-disk payload encoding to use when saving the LUT. + num_workers: Number of worker threads to use while sampling the LUT + grid. + + Returns: + A populated unprojection lookup table. + """ + from lensboy.camera_models.unproject_lut import UnprojectLUT + + return UnprojectLUT.from_camera_model( + self, + grid_size_wh=grid_size_wh, + pixel_stride=pixel_stride, + storage_encoding=storage_encoding, + num_workers=num_workers, + ) diff --git a/src/lensboy/camera_models/opencv.py b/src/lensboy/camera_models/opencv.py index 1af59e4..a3ab190 100644 --- a/src/lensboy/camera_models/opencv.py +++ b/src/lensboy/camera_models/opencv.py @@ -1,5 +1,6 @@ from __future__ import annotations +import functools import json from dataclasses import dataclass, field, replace from importlib.metadata import version as _package_version @@ -11,6 +12,11 @@ from lensboy.camera_models.base_model import CameraModel, CameraModelConfig K1, K2, P1, P2, K3, K4, K5, K6, S1, S2, S3, S4, TX, TY = range(14) +_UNDISTORT_POINTS_ITER_CRITERIA = ( + cv2.TERM_CRITERIA_COUNT | cv2.TERM_CRITERIA_EPS, + 100, + 1e-14, +) def _mask(*idx: int) -> np.ndarray: @@ -20,6 +26,11 @@ def _mask(*idx: int) -> np.ndarray: return m +@functools.lru_cache(maxsize=128) +def _camera_matrix_cached(fx: float, fy: float, cx: float, cy: float) -> np.ndarray: + return np.array([[fx, 0.0, cx], [0.0, fy, cy], [0.0, 0.0, 1.0]], dtype=np.float64) + + @dataclass class OpenCVConfig(CameraModelConfig): """Configuration for fitting an OpenCV pinhole + distortion model. @@ -167,14 +178,13 @@ def normalize_points(self, pixel_coords: np.ndarray) -> np.ndarray: assert pts.ndim == 2 and pts.shape[1] == 2, ( f"Expected (N, 2) array, got {pts.shape}" ) - criteria = (cv2.TERM_CRITERIA_COUNT | cv2.TERM_CRITERIA_EPS, 100, 1e-14) undistorted = cv2.undistortPointsIter( pts.reshape(-1, 1, 2), - self.K(), + self._camera_matrix_cached(), self.distortion_coeffs, R=None, # type: ignore P=None, # type: ignore - criteria=criteria, + criteria=_UNDISTORT_POINTS_ITER_CRITERIA, ).reshape(-1, 2) return np.column_stack([undistorted, np.ones(len(undistorted))]) @@ -200,13 +210,21 @@ def project_points( points_in_cam, rvec=np.zeros(3), tvec=np.zeros(3), - cameraMatrix=self.K(), + cameraMatrix=self._camera_matrix_cached(), distCoeffs=self.distortion_coeffs, )[0].reshape(-1, 2) def K(self): """Return the 3x3 camera intrinsics matrix.""" - return np.array([[self.fx, 0, self.cx], [0, self.fy, self.cy], [0, 0, 1]]) + return self._camera_matrix_cached().copy() + + def _camera_matrix_cached(self) -> np.ndarray: + return _camera_matrix_cached( + float(self.fx), + float(self.fy), + float(self.cx), + float(self.cy), + ) @property def fov_deg_x(self) -> float: diff --git a/src/lensboy/camera_models/unproject_lut.py b/src/lensboy/camera_models/unproject_lut.py new file mode 100644 index 0000000..ffaaad7 --- /dev/null +++ b/src/lensboy/camera_models/unproject_lut.py @@ -0,0 +1,1055 @@ +from __future__ import annotations + +import hashlib +import json +import math +import os +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass, field +from importlib.metadata import version as _package_version +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal + +import numpy as np + +if TYPE_CHECKING: + from lensboy.camera_models.base_model import CameraModel + +InterpolationMode = Literal["nearest", "bilinear", "bicubic"] +BoundsMode = Literal["strict", "clamp", "extrapolate"] +StorageEncoding = Literal["float64_xy", "float32_xy", "float16_xy"] + +_FORMAT_NAME = "lensboy_unproject_LUT" +_FORMAT_VERSION = 1 +_HEADER_END_MARKER = "END_HEADER" +_PAYLOAD_LAYOUT = "row_major_interleaved_xy" +_PAYLOAD_ENDIANNESS = "little" +_SUPPORTED_INTERPOLATIONS: tuple[InterpolationMode, ...] = ( + "nearest", + "bilinear", + "bicubic", +) +_SUPPORTED_BOUNDS: tuple[BoundsMode, ...] = ("strict", "clamp", "extrapolate") +_SUPPORTED_ENCODINGS: dict[StorageEncoding, np.dtype] = { + "float64_xy": np.dtype(" InterpolationMode: + if interpolation not in _SUPPORTED_INTERPOLATIONS: + raise ValueError( + f"Unsupported interpolation mode {interpolation!r}. " + f"Expected one of {_SUPPORTED_INTERPOLATIONS}." + ) + return interpolation # type: ignore[return-value] + + +def _validate_bounds_mode(bounds: str) -> BoundsMode: + if bounds not in _SUPPORTED_BOUNDS: + raise ValueError( + f"Unsupported bounds mode {bounds!r}. Expected one of {_SUPPORTED_BOUNDS}." + ) + return bounds # type: ignore[return-value] + + +def _validate_storage_encoding(storage_encoding: str) -> StorageEncoding: + if storage_encoding not in _SUPPORTED_ENCODINGS: + raise ValueError( + f"Unsupported storage encoding {storage_encoding!r}. " + f"Expected one of {tuple(_SUPPORTED_ENCODINGS)}." + ) + return storage_encoding # type: ignore[return-value] + + +def _snake_case_name(name: str) -> str: + out: list[str] = [] + for i, ch in enumerate(name): + if ch.isupper() and i > 0 and not name[i - 1].isupper(): + out.append("_") + out.append(ch.lower()) + return "".join(out) + + +def _camera_model_type_name(camera_model: CameraModel) -> str: + camera_model_name = getattr(camera_model, "_camera_model_name", None) + if callable(camera_model_name): + return str(camera_model_name()) + + name = _snake_case_name(type(camera_model).__name__) + if name == "open_cv": + return "opencv" + return name + + +def _camera_model_spec(camera_model: CameraModel) -> dict[str, Any] | None: + to_json = getattr(camera_model, "to_json", None) + if not callable(to_json): + return None + + serialized = to_json() + if isinstance(serialized, tuple): + serialized = serialized[0] + if not isinstance(serialized, dict): + return None + return serialized + + +def _serialize_source_model_spec(source_model_spec: dict[str, Any] | None) -> str | None: + if source_model_spec is None: + return None + return json.dumps( + source_model_spec, + separators=(",", ":"), + sort_keys=True, + ensure_ascii=True, + ) + + +def _parse_pair_of_ints(text: str, field_name: str) -> tuple[int, int]: + parts = [part.strip() for part in text.split(",")] + if len(parts) != 2: + raise ValueError(f"{field_name} must contain exactly 2 comma-separated values.") + return int(parts[0]), int(parts[1]) + + +def _parse_quad_of_floats( + text: str, field_name: str +) -> tuple[float, float, float, float]: + parts = [part.strip() for part in text.split(",")] + if len(parts) != 4: + raise ValueError(f"{field_name} must contain exactly 4 comma-separated values.") + return float(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]) + + +def _parse_pair_of_floats(text: str, field_name: str) -> tuple[float, float]: + parts = [part.strip() for part in text.split(",")] + if len(parts) != 2: + raise ValueError(f"{field_name} must contain exactly 2 comma-separated values.") + return float(parts[0]), float(parts[1]) + + +def _parse_optional_str(text: str) -> str | None: + if text == "not_computed": + return None + return text + + +def _format_float(value: float) -> str: + if math.isnan(value): + return "not_computed" + return f"{value:.17g}" + + +def _format_optional_str(value: str | None) -> str: + return "not_computed" if value is None else value + + +def _format_csv(values: list[str]) -> str: + return ", ".join(values) + + +def _normalize_num_workers(num_workers: int | None) -> int: + if num_workers is None: + resolved = os.cpu_count() or 1 + else: + resolved = int(num_workers) + if resolved <= 0: + raise ValueError("num_workers must be positive when provided.") + return resolved + + +def _catmull_rom_weights(t: np.ndarray) -> np.ndarray: + t2 = t * t + t3 = t2 * t + return np.stack( + [ + -0.5 * t + t2 - 0.5 * t3, + 1.0 - 2.5 * t2 + 1.5 * t3, + 0.5 * t + 2.0 * t2 - 1.5 * t3, + -0.5 * t2 + 0.5 * t3, + ], + axis=1, + ) + + +@dataclass +class UnprojectLUT: + """Regular-grid cache of `normalize_points()` values. + + Stores the x/y components of camera-frame rays on a regular image-space grid. + Queries interpolate those cached values and return rays of the form + ``[x, y, 1]``. + + Args: + image_width: Width of the source image in pixels. + image_height: Height of the source image in pixels. + grid_width: Number of cached samples along x. + grid_height: Number of cached samples along y. + grid_x_min: Minimum pixel x covered by the LUT. + grid_x_max: Maximum pixel x covered by the LUT. + grid_y_min: Minimum pixel y covered by the LUT. + grid_y_max: Maximum pixel y covered by the LUT. + storage_encoding: On-disk payload encoding. + xy_grid: Cached x/y ray components with shape ``(grid_height, grid_width, 2)``. + default_interpolation: Default interpolation mode for runtime queries. + default_bounds: Default bounds behavior for runtime queries. + source_model_type: Name of the camera model used to build the LUT. + source_model_spec: Exact serialized source-model specification, if available. + lensboy_version: Package version that produced the LUT. + + Returns: + A runtime lookup table that can save, load, and query cached unprojection rays. + """ + + image_width: int + image_height: int + grid_width: int + grid_height: int + grid_x_min: float + grid_x_max: float + grid_y_min: float + grid_y_max: float + storage_encoding: StorageEncoding + xy_grid: np.ndarray + default_interpolation: InterpolationMode = "bilinear" + default_bounds: BoundsMode = "strict" + source_model_type: str | None = None + source_model_spec: dict[str, Any] | None = None + lensboy_version: str = field(default_factory=lambda: _package_version("lensboy")) + _grid_scale_x: float = field(init=False, repr=False) + _grid_scale_y: float = field(init=False, repr=False) + + def __post_init__(self) -> None: + self.storage_encoding = _validate_storage_encoding(self.storage_encoding) + self.default_interpolation = _validate_interpolation_mode( + self.default_interpolation + ) + self.default_bounds = _validate_bounds_mode(self.default_bounds) + if self.source_model_spec is not None and not isinstance( + self.source_model_spec, dict + ): + raise ValueError("source_model_spec must be a dict when provided.") + if self.image_width <= 0 or self.image_height <= 0: + raise ValueError("image dimensions must be positive.") + if self.grid_width <= 0 or self.grid_height <= 0: + raise ValueError("grid dimensions must be positive.") + if self.grid_x_max < self.grid_x_min or self.grid_y_max < self.grid_y_min: + raise ValueError("grid extents must be ordered from min to max.") + + grid = np.asarray(self.xy_grid, dtype=np.float64) + if grid.shape != (self.grid_height, self.grid_width, 2): + raise ValueError( + "xy_grid must have shape " + f"({self.grid_height}, {self.grid_width}, 2), got {grid.shape}." + ) + if not np.all(np.isfinite(grid)): + raise ValueError("xy_grid must contain only finite values.") + self.xy_grid = np.ascontiguousarray(grid) + + self._grid_scale_x = self._compute_grid_scale( + self.grid_width, self.grid_x_min, self.grid_x_max + ) + self._grid_scale_y = self._compute_grid_scale( + self.grid_height, self.grid_y_min, self.grid_y_max + ) + + def __repr__(self) -> str: + return ( + f"UnprojectLUT(image={self.image_width}x{self.image_height}, " + f"grid={self.grid_width}x{self.grid_height}, " + f"encoding={self.storage_encoding})" + ) + + @property + def grid_size_wh(self) -> tuple[int, int]: + """Return the cached grid size as ``(width, height)``. + + Returns: + Grid size in samples. + """ + return self.grid_width, self.grid_height + + @property + def grid_extents_xy(self) -> tuple[float, float, float, float]: + """Return the covered pixel domain. + + Returns: + ``(x_min, x_max, y_min, y_max)`` in pixel coordinates. + """ + return self.grid_x_min, self.grid_x_max, self.grid_y_min, self.grid_y_max + + @property + def grid_stride_xy(self) -> tuple[float, float]: + """Return the actual spacing between neighboring cached samples. + + Returns: + ``(stride_x, stride_y)`` in pixel coordinates. These values are derived + from the grid extents and sample counts, so they may be fractional. + """ + stride_x = 0.0 + if self.grid_width > 1: + stride_x = (self.grid_x_max - self.grid_x_min) / (self.grid_width - 1) + stride_y = 0.0 + if self.grid_height > 1: + stride_y = (self.grid_y_max - self.grid_y_min) / (self.grid_height - 1) + return float(stride_x), float(stride_y) + + @property + def header_text(self) -> str: + """Return the ASCII file header for this LUT. + + Returns: + Header text exactly as it would be written before the binary payload. + """ + return self._encode_header() + + @property + def payload_offset_bytes(self) -> int: + """Return the byte offset where the binary payload begins. + + Returns: + Number of bytes occupied by the serialized ASCII header. + """ + return len(self.header_text.encode("ascii")) + + @property + def payload_bytes(self) -> int: + """Return the size of the serialized binary payload. + + Returns: + Number of bytes in the row-major interleaved x/y payload. + """ + dtype = _SUPPORTED_ENCODINGS[self.storage_encoding] + return self.grid_width * self.grid_height * 2 * dtype.itemsize + + @property + def total_bytes(self) -> int: + """Return the total serialized file size for this LUT. + + Returns: + Number of bytes in the full `.unproject_LUT` file. + """ + return self.payload_offset_bytes + self.payload_bytes + + def header_preview(self, max_lines: int = 0) -> str: + """Return a short human-readable preview of the file header. + + Args: + max_lines: Maximum number of header lines to include. Use ``0`` to + include the full header. + + Returns: + Preview text containing the first header lines. + """ + if max_lines < 0: + raise ValueError("max_lines must be non-negative.") + if max_lines == 0: + return self.header_text.rstrip("\n") + return "\n".join(self.header_text.splitlines()[:max_lines]) + + @property + def supported_interpolations(self) -> tuple[InterpolationMode, ...]: + """Return the interpolation modes supported by the LUT. + + Returns: + Tuple of interpolation mode names. + """ + return _SUPPORTED_INTERPOLATIONS + + @property + def supported_bounds(self) -> tuple[BoundsMode, ...]: + """Return the bounds behaviors supported by the LUT. + + Returns: + Tuple of bounds mode names. + """ + return _SUPPORTED_BOUNDS + + @property + def source_model_spec_json_sha256(self) -> str | None: + """Return a stable hash of the serialized source-model specification. + + Returns: + Lowercase SHA-256 hex digest of the canonical source-model JSON, or None + if the LUT does not carry a source-model specification. + """ + source_model_spec_json = self._source_model_spec_json() + if source_model_spec_json is None: + return None + return hashlib.sha256(source_model_spec_json.encode("ascii")).hexdigest() + + @staticmethod + def _compute_grid_scale(size: int, minimum: float, maximum: float) -> float: + if size == 1 or maximum == minimum: + return 0.0 + return (size - 1) / (maximum - minimum) + + def _source_model_spec_json(self) -> str | None: + return _serialize_source_model_spec(self.source_model_spec) + + @classmethod + def from_camera_model( + cls, + camera_model: CameraModel, + *, + grid_size_wh: tuple[int, int] | None = None, + pixel_stride: float | tuple[float, float] | None = None, + storage_encoding: StorageEncoding = "float64_xy", + num_workers: int | None = None, + ) -> UnprojectLUT: + """Build a LUT from a camera model. + + Args: + camera_model: Camera model to sample. + grid_size_wh: Number of samples as ``(width, height)``. If omitted, + the LUT uses a per-pixel grid. + pixel_stride: Approximate pixel spacing between cached samples. Mutually + exclusive with ``grid_size_wh``. + storage_encoding: On-disk payload encoding to use when saving. + num_workers: Number of worker threads to use while sampling the LUT grid. + + Returns: + A populated unprojection LUT. + """ + storage_encoding = _validate_storage_encoding(storage_encoding) + resolved_num_workers = _normalize_num_workers(num_workers) + if grid_size_wh is not None and pixel_stride is not None: + raise ValueError("grid_size_wh and pixel_stride are mutually exclusive.") + + if grid_size_wh is None: + if pixel_stride is None: + grid_width = camera_model.image_width + grid_height = camera_model.image_height + else: + stride_x, stride_y = cls._normalize_pixel_stride(pixel_stride) + grid_width = cls._grid_size_from_stride( + camera_model.image_width, stride_x + ) + grid_height = cls._grid_size_from_stride( + camera_model.image_height, stride_y + ) + else: + grid_width, grid_height = int(grid_size_wh[0]), int(grid_size_wh[1]) + + if grid_width <= 0 or grid_height <= 0: + raise ValueError("grid_size_wh must contain positive integers.") + + x_coords = np.linspace( + 0.0, float(camera_model.image_width - 1), grid_width, dtype=np.float64 + ) + y_coords = np.linspace( + 0.0, float(camera_model.image_height - 1), grid_height, dtype=np.float64 + ) + payload_dtype = _SUPPORTED_ENCODINGS[storage_encoding] + xy_grid = cls._sample_xy_grid( + camera_model, + x_coords=x_coords, + y_coords=y_coords, + payload_dtype=payload_dtype, + num_workers=resolved_num_workers, + ) + + lut = cls( + image_width=camera_model.image_width, + image_height=camera_model.image_height, + grid_width=grid_width, + grid_height=grid_height, + grid_x_min=0.0, + grid_x_max=float(camera_model.image_width - 1), + grid_y_min=0.0, + grid_y_max=float(camera_model.image_height - 1), + storage_encoding=storage_encoding, + xy_grid=xy_grid, + source_model_type=_camera_model_type_name(camera_model), + source_model_spec=_camera_model_spec(camera_model), + ) + return lut + + @staticmethod + def _normalize_pixel_stride( + pixel_stride: float | tuple[float, float], + ) -> tuple[float, float]: + if isinstance(pixel_stride, tuple): + stride_x = float(pixel_stride[0]) + stride_y = float(pixel_stride[1]) + else: + stride_x = float(pixel_stride) + stride_y = float(pixel_stride) + if stride_x <= 0.0 or stride_y <= 0.0: + raise ValueError("pixel_stride values must be positive.") + return stride_x, stride_y + + @staticmethod + def _grid_size_from_stride(image_size: int, stride: float) -> int: + if image_size <= 1: + return 1 + return int(math.ceil((image_size - 1) / stride)) + 1 + + @staticmethod + def _sample_xy_grid( + camera_model: CameraModel, + *, + x_coords: np.ndarray, + y_coords: np.ndarray, + payload_dtype: np.dtype, + num_workers: int, + ) -> np.ndarray: + grid_width = len(x_coords) + grid_height = len(y_coords) + xy_grid = np.empty((grid_height, grid_width, 2), dtype=np.float64) + target_chunk_rows = max( + 1, + min( + grid_height, + _LUT_BUILD_TARGET_CHUNK_SAMPLES // max(grid_width, 1), + ), + ) + if num_workers > 1: + target_parallel_chunks = min(num_workers, grid_height) + target_chunk_rows = min( + target_chunk_rows, + max(1, int(math.ceil(grid_height / target_parallel_chunks))), + ) + + row_ranges = [ + (row_start, min(row_start + target_chunk_rows, grid_height)) + for row_start in range(0, grid_height, target_chunk_rows) + ] + + if len(row_ranges) == 1 or num_workers == 1: + for row_start, row_stop in row_ranges: + _, _, xy_chunk = UnprojectLUT._sample_xy_grid_chunk( + camera_model, + x_coords=x_coords, + y_coords=y_coords, + payload_dtype=payload_dtype, + row_start=row_start, + row_stop=row_stop, + ) + xy_grid[row_start:row_stop] = xy_chunk + return xy_grid + + worker_count = min(num_workers, len(row_ranges)) + with ThreadPoolExecutor(max_workers=worker_count) as executor: + futures = [ + executor.submit( + UnprojectLUT._sample_xy_grid_chunk, + camera_model, + x_coords=x_coords, + y_coords=y_coords, + payload_dtype=payload_dtype, + row_start=row_start, + row_stop=row_stop, + ) + for row_start, row_stop in row_ranges + ] + for future in futures: + row_start, row_stop, xy_chunk = future.result() + xy_grid[row_start:row_stop] = xy_chunk + + return xy_grid + + @staticmethod + def _sample_xy_grid_chunk( + camera_model: CameraModel, + *, + x_coords: np.ndarray, + y_coords: np.ndarray, + payload_dtype: np.dtype, + row_start: int, + row_stop: int, + ) -> tuple[int, int, np.ndarray]: + y_chunk = y_coords[row_start:row_stop] + grid_width = len(x_coords) + pixel_count = grid_width * len(y_chunk) + pixels = np.empty((pixel_count, 2), dtype=np.float64) + pixels[:, 0] = np.tile(x_coords, len(y_chunk)) + pixels[:, 1] = np.repeat(y_chunk, grid_width) + + rays = camera_model.normalize_points(pixels) + xy_chunk = np.asarray(rays[:, :2], dtype=np.float64) + if payload_dtype != np.dtype(np.float64): + xy_chunk = np.asarray(xy_chunk, dtype=payload_dtype).astype(np.float64) + + return row_start, row_stop, xy_chunk.reshape(len(y_chunk), grid_width, 2) + + def save(self, path: Path | str) -> None: + """Write the LUT to a `.unproject_LUT` file. + + Args: + path: Destination path. Must end with ``.unproject_LUT``. + + Returns: + None. + """ + output_path = Path(path) + if output_path.suffix != ".unproject_LUT": + raise ValueError("UnprojectLUT files must use the .unproject_LUT suffix.") + + header = self._encode_header().encode("ascii") + payload_dtype = _SUPPORTED_ENCODINGS[self.storage_encoding] + payload = np.asarray(self.xy_grid, dtype=payload_dtype, order="C").tobytes() + + with output_path.open("wb") as f: + f.write(header) + f.write(payload) + + @staticmethod + def load(path: Path | str) -> UnprojectLUT: + """Load a LUT from disk. + + Args: + path: Path to a `.unproject_LUT` file. + + Returns: + The loaded LUT. + """ + input_path = Path(path) + with input_path.open("rb") as f: + header_lines = UnprojectLUT._read_header_lines(f) + header = UnprojectLUT._parse_header_lines(header_lines) + payload_offset_bytes = f.tell() + payload = f.read() + + format_name = header["format"] + if format_name != _FORMAT_NAME: + raise ValueError(f"Unsupported LUT format {format_name!r}.") + + format_version = int(header["format_version"]) + if format_version != _FORMAT_VERSION: + raise ValueError( + f"Unsupported LUT format_version {format_version}. " + f"Expected {_FORMAT_VERSION}." + ) + UnprojectLUT._validate_header_fields(header) + + storage_encoding = _validate_storage_encoding(header["storage_encoding"]) + if header["payload_layout"] != _PAYLOAD_LAYOUT: + raise ValueError(f"Unsupported payload_layout {header['payload_layout']!r}.") + if header["payload_endianness"] != _PAYLOAD_ENDIANNESS: + raise ValueError( + f"Unsupported payload_endianness {header['payload_endianness']!r}." + ) + declared_payload_offset_bytes = int(header["payload_offset_bytes"]) + if payload_offset_bytes != declared_payload_offset_bytes: + raise ValueError( + f"Header payload_offset_bytes={declared_payload_offset_bytes}, " + f"but payload begins at byte offset {payload_offset_bytes}." + ) + + image_width, image_height = _parse_pair_of_ints( + header["image_size_wh"], "image_size_wh" + ) + grid_width, grid_height = _parse_pair_of_ints( + header["grid_size_wh"], "grid_size_wh" + ) + grid_x_min, grid_x_max, grid_y_min, grid_y_max = _parse_quad_of_floats( + header["grid_extents_xy"], "grid_extents_xy" + ) + header_stride_x, header_stride_y = _parse_pair_of_floats( + header["grid_stride_xy"], "grid_stride_xy" + ) + dtype = _SUPPORTED_ENCODINGS[storage_encoding] + expected_payload_bytes = grid_width * grid_height * 2 * dtype.itemsize + if len(payload) != expected_payload_bytes: + raise ValueError( + f"Unexpected payload size {len(payload)} bytes; expected " + f"{expected_payload_bytes}." + ) + expected_stride_x = ( + 0.0 if grid_width <= 1 else (grid_x_max - grid_x_min) / (grid_width - 1) + ) + expected_stride_y = ( + 0.0 if grid_height <= 1 else (grid_y_max - grid_y_min) / (grid_height - 1) + ) + if not math.isclose( + header_stride_x, expected_stride_x, rel_tol=0.0, abs_tol=1e-12 + ): + raise ValueError( + "grid_stride_xy does not match grid_extents_xy and grid_size_wh." + ) + if not math.isclose( + header_stride_y, expected_stride_y, rel_tol=0.0, abs_tol=1e-12 + ): + raise ValueError( + "grid_stride_xy does not match grid_extents_xy and grid_size_wh." + ) + + source_model_spec_json = header["source_model_spec_json"] + source_model_spec_json_sha256 = header["source_model_spec_json_sha256"] + if source_model_spec_json == "not_computed": + if source_model_spec_json_sha256 != "not_computed": + raise ValueError( + "source_model_spec_json_sha256 must be not_computed when " + "source_model_spec_json is not_computed." + ) + source_model_spec = None + else: + expected_hash = hashlib.sha256( + source_model_spec_json.encode("ascii") + ).hexdigest() + if source_model_spec_json_sha256 != expected_hash: + raise ValueError( + "source_model_spec_json_sha256 does not match source_model_spec_json." + ) + source_model_spec = json.loads(source_model_spec_json) + + xy_grid = ( + np.frombuffer(payload, dtype=dtype) + .astype(np.float64) + .reshape(grid_height, grid_width, 2) + ) + + return UnprojectLUT( + image_width=image_width, + image_height=image_height, + grid_width=grid_width, + grid_height=grid_height, + grid_x_min=grid_x_min, + grid_x_max=grid_x_max, + grid_y_min=grid_y_min, + grid_y_max=grid_y_max, + storage_encoding=storage_encoding, + xy_grid=xy_grid, + default_interpolation=_validate_interpolation_mode( + header["default_interpolation"] + ), + default_bounds=_validate_bounds_mode(header["default_bounds"]), + source_model_type=_parse_optional_str(header["source_model_type"]), + source_model_spec=source_model_spec, + lensboy_version=header["lensboy_version"], + ) + + @staticmethod + def _read_header_lines(file_obj) -> list[str]: + header_lines: list[str] = [] + total_bytes = 0 + while True: + raw_line = file_obj.readline() + if raw_line == b"": + raise ValueError("Reached end of file before END_HEADER.") + total_bytes += len(raw_line) + if total_bytes > _MAX_HEADER_BYTES: + raise ValueError("Header exceeds the maximum supported size.") + try: + line = raw_line.decode("ascii").rstrip("\r\n") + except UnicodeDecodeError as exc: + raise ValueError("Header must contain only ASCII text.") from exc + if line == _HEADER_END_MARKER: + return header_lines + header_lines.append(line) + + @staticmethod + def _parse_header_lines(header_lines: list[str]) -> dict[str, str]: + header: dict[str, str] = {} + for line in header_lines: + if ":" not in line: + raise ValueError(f"Invalid header line {line!r}. Expected 'key: value'.") + key, value = line.split(":", 1) + key = key.strip() + value = value.strip() + if not key: + raise ValueError("Header keys must be non-empty.") + if key in header: + raise ValueError(f"Duplicate header key {key!r}.") + header[key] = value + return header + + @staticmethod + def _validate_header_fields(header: dict[str, str]) -> None: + required_fields = { + "format", + "format_version", + "lensboy_version", + "source_model_type", + "source_model_spec_json", + "source_model_spec_json_sha256", + "image_size_wh", + "grid_size_wh", + "grid_extents_xy", + "grid_stride_xy", + "storage_encoding", + "default_interpolation", + "default_bounds", + "payload_offset_bytes", + "payload_layout", + "payload_endianness", + } + + missing = required_fields - set(header) + if missing: + missing_text = ", ".join(sorted(missing)) + raise ValueError(f"Missing required header fields: {missing_text}.") + + removed_fields = { + "error_report_mode", + "error_report_max_depth", + "error_report_min_cell_size", + } + removed_prefixes = ( + "estimated_max_angular_error_", + "estimated_median_angular_error_", + ) + unexpected_removed_fields = sorted( + key + for key in header + if key in removed_fields + or any(key.startswith(prefix) for prefix in removed_prefixes) + ) + if unexpected_removed_fields: + removed_text = ", ".join(unexpected_removed_fields) + raise ValueError( + "This runtime-only .unproject_LUT format does not support " + f"legacy error-report header fields: {removed_text}." + ) + + def _encode_header(self) -> str: + source_model_spec_json = self._source_model_spec_json() + if source_model_spec_json is None: + source_model_spec_json = "not_computed" + source_model_spec_json_sha256 = "not_computed" + else: + source_model_spec_json_sha256 = hashlib.sha256( + source_model_spec_json.encode("ascii") + ).hexdigest() + + stride_x, stride_y = self.grid_stride_xy + + def build_lines(payload_offset_bytes_text: str) -> list[str]: + return [ + f"format: {_FORMAT_NAME}", + f"payload_offset_bytes: {payload_offset_bytes_text}", + f"format_version: {_FORMAT_VERSION}", + f"lensboy_version: {self.lensboy_version}", + f"source_model_type: {_format_optional_str(self.source_model_type)}", + f"source_model_spec_json_sha256: {source_model_spec_json_sha256}", + f"image_size_wh: {_format_csv([str(self.image_width), str(self.image_height)])}", + f"grid_size_wh: {_format_csv([str(self.grid_width), str(self.grid_height)])}", + "grid_extents_xy: " + + _format_csv( + [ + _format_float(self.grid_x_min), + _format_float(self.grid_x_max), + _format_float(self.grid_y_min), + _format_float(self.grid_y_max), + ] + ), + "grid_stride_xy: " + + _format_csv([_format_float(stride_x), _format_float(stride_y)]), + f"storage_encoding: {self.storage_encoding}", + f"default_interpolation: {self.default_interpolation}", + f"default_bounds: {self.default_bounds}", + f"payload_layout: {_PAYLOAD_LAYOUT}", + f"payload_endianness: {_PAYLOAD_ENDIANNESS}", + f"source_model_spec_json: {source_model_spec_json}", + _HEADER_END_MARKER, + ] + + payload_offset_bytes_text = "0" + while True: + header = "\n".join(build_lines(payload_offset_bytes_text)) + "\n" + next_payload_offset_bytes_text = str(len(header.encode("ascii"))) + if next_payload_offset_bytes_text == payload_offset_bytes_text: + return header + payload_offset_bytes_text = next_payload_offset_bytes_text + + def normalize_points( + self, + pixel_coords: np.ndarray, + *, + interpolation: InterpolationMode = "bilinear", + bounds: BoundsMode = "strict", + return_valid_mask: bool = False, + ) -> np.ndarray | tuple[np.ndarray, np.ndarray]: + """Query the LUT for camera-frame rays. + + Args: + pixel_coords: Pixel coordinates with shape ``(N, 2)``. + interpolation: Interpolation mode to use. + bounds: Bounds behavior for out-of-domain pixels. + return_valid_mask: Whether to also return a boolean validity mask. + + Returns: + Rays with shape ``(N, 3)``. + If ``return_valid_mask`` is True, also returns a boolean mask with + shape ``(N,)`` indicating which rows were valid in strict mode. + """ + interpolation = _validate_interpolation_mode(interpolation) + bounds = _validate_bounds_mode(bounds) + + pts = np.asarray(pixel_coords, dtype=np.float64) + if pts.ndim != 2 or pts.shape[1] != 2: + raise ValueError(f"pixel_coords must have shape (N, 2), got {pts.shape}.") + + query_x = pts[:, 0] + query_y = pts[:, 1] + valid_mask = self._valid_mask(query_x, query_y) + + if bounds == "strict" and not return_valid_mask and not np.all(valid_mask): + raise ValueError("Some pixel coordinates lie outside the LUT domain.") + + rays = np.full((len(pts), 3), np.nan, dtype=np.float64) + rays[:, 2] = 1.0 + + if bounds == "strict": + active_mask = valid_mask + sample_x = query_x[active_mask] + sample_y = query_y[active_mask] + elif bounds == "clamp": + active_mask = np.ones(len(pts), dtype=bool) + sample_x = np.clip(query_x, self.grid_x_min, self.grid_x_max) + sample_y = np.clip(query_y, self.grid_y_min, self.grid_y_max) + valid_mask = np.ones(len(pts), dtype=bool) + else: + active_mask = np.ones(len(pts), dtype=bool) + sample_x = query_x + sample_y = query_y + valid_mask = np.ones(len(pts), dtype=bool) + + if np.any(active_mask): + approx_xy = self._interpolate_xy(sample_x, sample_y, interpolation, bounds) + rays[active_mask, :2] = approx_xy + + if return_valid_mask: + return rays, valid_mask + return rays + + def _valid_mask(self, x: np.ndarray, y: np.ndarray) -> np.ndarray: + return ( + (x >= self.grid_x_min) + & (x <= self.grid_x_max) + & (y >= self.grid_y_min) + & (y <= self.grid_y_max) + ) + + def _interpolate_xy( + self, + x: np.ndarray, + y: np.ndarray, + interpolation: InterpolationMode, + bounds: BoundsMode, + ) -> np.ndarray: + if len(x) == 0: + return np.empty((0, 2), dtype=np.float64) + + gx = (x - self.grid_x_min) * self._grid_scale_x + gy = (y - self.grid_y_min) * self._grid_scale_y + + if interpolation == "nearest": + return self._query_nearest(gx, gy) + if interpolation == "bilinear": + return self._query_bilinear(gx, gy, bounds) + return self._query_bicubic(gx, gy, bounds) + + def _query_nearest(self, gx: np.ndarray, gy: np.ndarray) -> np.ndarray: + ix = np.clip(np.rint(gx).astype(np.int64), 0, self.grid_width - 1) + iy = np.clip(np.rint(gy).astype(np.int64), 0, self.grid_height - 1) + return self.xy_grid[iy, ix] + + def _query_bilinear( + self, + gx: np.ndarray, + gy: np.ndarray, + bounds: BoundsMode, + ) -> np.ndarray: + ix0, tx = self._linear_indices_and_weights(gx, self.grid_width, bounds) + iy0, ty = self._linear_indices_and_weights(gy, self.grid_height, bounds) + + ix1 = np.clip(ix0 + 1, 0, self.grid_width - 1) + iy1 = np.clip(iy0 + 1, 0, self.grid_height - 1) + + v00 = self.xy_grid[iy0, ix0] + v10 = self.xy_grid[iy0, ix1] + v01 = self.xy_grid[iy1, ix0] + v11 = self.xy_grid[iy1, ix1] + + tx_col = tx[:, None] + ty_col = ty[:, None] + top = v00 * (1.0 - tx_col) + v10 * tx_col + bottom = v01 * (1.0 - tx_col) + v11 * tx_col + return top * (1.0 - ty_col) + bottom * ty_col + + def _query_bicubic( + self, + gx: np.ndarray, + gy: np.ndarray, + bounds: BoundsMode, + ) -> np.ndarray: + bilinear_xy = self._query_bilinear(gx, gy, bounds) + if self.grid_width < 4 or self.grid_height < 4: + return bilinear_xy + + if bounds == "extrapolate": + gx_work = gx + gy_work = gy + else: + gx_work = np.clip(gx, 0.0, self.grid_width - 1.0) + gy_work = np.clip(gy, 0.0, self.grid_height - 1.0) + + ix1 = np.floor(gx_work).astype(np.int64) + iy1 = np.floor(gy_work).astype(np.int64) + cubic_mask = ( + (ix1 >= 1) + & (ix1 <= self.grid_width - 3) + & (iy1 >= 1) + & (iy1 <= self.grid_height - 3) + ) + if not np.any(cubic_mask): + return bilinear_xy + + ix1 = ix1[cubic_mask] + iy1 = iy1[cubic_mask] + tx = gx_work[cubic_mask] - ix1 + ty = gy_work[cubic_mask] - iy1 + + ix = np.stack( + [ + ix1 - 1, + ix1, + ix1 + 1, + ix1 + 2, + ], + axis=1, + ) + iy = np.stack( + [ + iy1 - 1, + iy1, + iy1 + 1, + iy1 + 2, + ], + axis=1, + ) + + wx = _catmull_rom_weights(tx) + wy = _catmull_rom_weights(ty) + + neighborhood = self.xy_grid[iy[:, :, None], ix[:, None, :], :] + weighted_x = np.sum(neighborhood * wx[:, None, :, None], axis=2) + bilinear_xy[cubic_mask] = np.sum(weighted_x * wy[:, :, None], axis=1) + return bilinear_xy + + @staticmethod + def _linear_indices_and_weights( + g: np.ndarray, + size: int, + bounds: BoundsMode, + ) -> tuple[np.ndarray, np.ndarray]: + if size == 1: + return np.zeros(len(g), dtype=np.int64), np.zeros(len(g), dtype=np.float64) + + if bounds == "extrapolate": + base = np.floor(g).astype(np.int64) + base = np.clip(base, 0, size - 2) + t = g - base + else: + g_clipped = np.clip(g, 0.0, size - 1.0) + base = np.floor(g_clipped).astype(np.int64) + base = np.minimum(base, size - 2) + t = g_clipped - base + + return base, t diff --git a/tests/test_unproject_lut.py b/tests/test_unproject_lut.py new file mode 100644 index 0000000..209dac8 --- /dev/null +++ b/tests/test_unproject_lut.py @@ -0,0 +1,912 @@ +"""Tests for UnprojectLUT creation, serialization, querying, and C++ loading.""" + +from __future__ import annotations + +import hashlib +import shutil +import subprocess +import textwrap +from pathlib import Path + +import numpy as np +import pytest + +import lensboy as lb + +DATA_DIR = Path(__file__).parent.parent / "data/test_datasets" +REPO_ROOT = Path(__file__).resolve().parent.parent +CPP_RUNTIME_DIR = REPO_ROOT / "cpp_runtime" + + +def _make_linear_pinhole_model() -> lb.PinholeRemapped: + return lb.PinholeRemapped( + image_width=17, + image_height=13, + fx=23.0, + fy=19.0, + cx=8.0, + cy=6.0, + map_x=np.zeros((13, 17), dtype=np.float32), + map_y=np.zeros((13, 17), dtype=np.float32), + input_image_width=17, + input_image_height=13, + ) + + +def _load_opencv_model() -> lb.OpenCV: + return lb.OpenCV.load(DATA_DIR / "opencv.json") + + +def _load_spline_model() -> lb.PinholeSplined: + return lb.PinholeSplined.load(DATA_DIR / "spline.json") + + +def _load_remapped_model() -> lb.PinholeRemapped: + return _load_spline_model().get_pinhole_model() + + +def _header_and_payload(file_path: Path) -> tuple[str, bytes]: + data = file_path.read_bytes() + marker = b"END_HEADER\n" + index = data.index(marker) + len(marker) + return data[:index].decode("ascii"), data[index:] + + +def _rewrite_header_field( + file_path: Path, + key: str, + new_value: str, + *, + recompute_payload_offset: bool = True, +) -> None: + header, payload = _header_and_payload(file_path) + lines = header.splitlines() + for i, line in enumerate(lines): + if line.startswith(f"{key}:"): + lines[i] = f"{key}: {new_value}" + break + else: + raise AssertionError(f"Missing header key {key!r}.") + + if recompute_payload_offset: + payload_offset_index = None + for i, line in enumerate(lines): + if line.startswith("payload_offset_bytes:"): + payload_offset_index = i + break + if payload_offset_index is not None: + payload_offset_text = "0" + while True: + lines[payload_offset_index] = ( + f"payload_offset_bytes: {payload_offset_text}" + ) + header_text = "\n".join(lines) + "\n" + next_payload_offset_text = str(len(header_text.encode("ascii"))) + if next_payload_offset_text == payload_offset_text: + break + payload_offset_text = next_payload_offset_text + + file_path.write_bytes(("\n".join(lines) + "\n").encode("ascii") + payload) + + +def _append_header_field(file_path: Path, key: str, value: str) -> None: + header, payload = _header_and_payload(file_path) + lines = header.splitlines() + end_header_index = lines.index("END_HEADER") + lines.insert(end_header_index, f"{key}: {value}") + + payload_offset_index = lines.index( + next(line for line in lines if line.startswith("payload_offset_bytes:")) + ) + payload_offset_text = "0" + while True: + lines[payload_offset_index] = f"payload_offset_bytes: {payload_offset_text}" + header_text = "\n".join(lines) + "\n" + next_payload_offset_text = str(len(header_text.encode("ascii"))) + if next_payload_offset_text == payload_offset_text: + break + payload_offset_text = next_payload_offset_text + + file_path.write_bytes(("\n".join(lines) + "\n").encode("ascii") + payload) + + +def _parse_header_text(header: str) -> dict[str, str]: + parsed: dict[str, str] = {} + for line in header.splitlines(): + if line == "END_HEADER": + continue + key, value = line.split(": ", 1) + parsed[key] = value + return parsed + + +def _random_pixels( + model: lb.PinholeRemapped, + n: int = 128, + seed: int = 0, +) -> np.ndarray: + rng = np.random.default_rng(seed) + xs = rng.uniform(0.0, model.image_width - 1, n) + ys = rng.uniform(0.0, model.image_height - 1, n) + return np.column_stack([xs, ys]) + + +def _query_error_deg( + reference: np.ndarray, + approx: np.ndarray, +) -> float: + reference_unit = reference / np.linalg.norm(reference, axis=1, keepdims=True) + approx_unit = approx / np.linalg.norm(approx, axis=1, keepdims=True) + dots = np.einsum("ij,ij->i", reference_unit, approx_unit) + return float(np.max(np.rad2deg(np.arccos(np.clip(dots, -1.0, 1.0))))) + + +def test_unproject_lut_header_is_human_readable(tmp_path: Path) -> None: + """The text header surfaces key metadata near the top of the file.""" + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(5, 4)) + + file_path = tmp_path / "linear.unproject_LUT" + lut.save(file_path) + + header, _ = _header_and_payload(file_path) + header_fields = _parse_header_text(header) + header_lines = header.splitlines() + first_lines = header_lines[:16] + + assert header_fields["format_version"] == "1" + assert first_lines[0] == "format: lensboy_unproject_LUT" + assert first_lines[1].startswith("payload_offset_bytes: ") + assert first_lines[2] == "format_version: 1" + assert first_lines[3].startswith("lensboy_version: ") + assert first_lines[4] == "source_model_type: pinhole_remapped" + assert first_lines[5].startswith("source_model_spec_json_sha256: ") + assert first_lines[6] == "image_size_wh: 17, 13" + assert first_lines[7] == "grid_size_wh: 5, 4" + assert first_lines[8] == "grid_extents_xy: 0, 16, 0, 12" + assert first_lines[9] == "grid_stride_xy: 4, 4" + assert first_lines[10] == "storage_encoding: float64_xy" + assert first_lines[11] == "default_interpolation: bilinear" + assert first_lines[12] == "default_bounds: strict" + assert first_lines[13] == "payload_layout: row_major_interleaved_xy" + assert first_lines[14] == "payload_endianness: little" + assert first_lines[15].startswith("source_model_spec_json: ") + assert not any( + line.startswith("supported_interpolations:") for line in header.splitlines() + ) + assert not any(line.startswith("supported_bounds:") for line in header.splitlines()) + assert not any( + line.startswith("estimated_max_angular_error_") for line in header.splitlines() + ) + assert not any( + line.startswith("estimated_median_angular_error_") for line in header.splitlines() + ) + assert not any(line.startswith("error_report_") for line in header.splitlines()) + assert any(line.startswith("source_model_spec_json:") for line in header.splitlines()) + assert int(header_fields["payload_offset_bytes"]) == len(header.encode("ascii")) + assert ( + header_fields["source_model_spec_json_sha256"] + == hashlib.sha256( + header_fields["source_model_spec_json"].encode("ascii") + ).hexdigest() + ) + + +def test_unproject_lut_exposes_serialized_file_metadata(tmp_path: Path) -> None: + """The runtime object exposes its serialized header and size information.""" + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(5, 4)) + file_path = tmp_path / "linear.unproject_LUT" + + lut.save(file_path) + loaded = lb.UnprojectLUT.load(file_path) + header, payload = _header_and_payload(file_path) + + assert loaded.header_text == header + assert loaded.payload_offset_bytes == len(header.encode("ascii")) + assert loaded.payload_bytes == len(payload) + assert loaded.total_bytes == file_path.stat().st_size + assert loaded.header_preview() == header.rstrip("\n") + assert loaded.header_preview(12) == "\n".join(header.splitlines()[:12]) + + +@pytest.mark.parametrize( + ("factory", "expected_model_type"), + [ + (_load_opencv_model, "opencv"), + (_load_spline_model, "pinhole_splined"), + (_load_remapped_model, "pinhole_remapped"), + ], +) +def test_unproject_lut_round_trip_for_camera_models( + tmp_path: Path, + factory, + expected_model_type: str, +) -> None: + """OpenCV, spline, and remapped models survive LUT save/load round trips.""" + model = factory() + lut = model.get_unproject_lut( + grid_size_wh=(11, 9), + storage_encoding="float64_xy", + ) + + file_path = tmp_path / f"{expected_model_type}.unproject_LUT" + lut.save(file_path) + loaded = lb.UnprojectLUT.load(file_path) + + assert loaded.source_model_type == expected_model_type + assert loaded.grid_size_wh == (11, 9) + assert loaded.storage_encoding == "float64_xy" + assert loaded.source_model_spec is not None + assert loaded.source_model_spec["type"] == expected_model_type + assert loaded.source_model_spec_json_sha256 is not None + + x_coords = np.linspace(0.0, model.image_width - 1, loaded.grid_width) + y_coords = np.linspace(0.0, model.image_height - 1, loaded.grid_height) + grid_x, grid_y = np.meshgrid(x_coords, y_coords, indexing="xy") + pixels = np.column_stack([grid_x.ravel(), grid_y.ravel()]) + + expected = model.normalize_points(pixels) + actual = loaded.normalize_points(pixels, interpolation="bilinear") + np.testing.assert_allclose(actual, expected, atol=1e-12) + + +def test_linear_pinhole_lut_encodings(tmp_path: Path) -> None: + """Linear pinhole data isolates storage-encoding error from interpolation error.""" + model = _make_linear_pinhole_model() + sample_pixels = _random_pixels(model, seed=4) + expected = model.normalize_points(sample_pixels) + max_abs_errors: dict[str, float] = {} + + for encoding in ("float64_xy", "float32_xy", "float16_xy"): + lut = model.get_unproject_lut( + grid_size_wh=(6, 5), + storage_encoding=encoding, + ) + file_path = tmp_path / f"{encoding}.unproject_LUT" + lut.save(file_path) + loaded = lb.UnprojectLUT.load(file_path) + + approx = loaded.normalize_points(sample_pixels, interpolation="bilinear") + max_abs_errors[encoding] = float(np.max(np.abs(approx[:, :2] - expected[:, :2]))) + + assert max_abs_errors["float64_xy"] < 1e-12 + assert max_abs_errors["float32_xy"] < 1e-6 + assert max_abs_errors["float16_xy"] < 1e-3 + assert max_abs_errors["float64_xy"] <= max_abs_errors["float32_xy"] + 1e-15 + assert max_abs_errors["float32_xy"] <= max_abs_errors["float16_xy"] + 1e-15 + + +def test_unproject_lut_analyzer_can_report_multiple_interpolations() -> None: + """The analyzer can report multiple interpolation modes at once.""" + from lensboy.analysis import UnprojectLUTAnalyzer + + model = _load_opencv_model() + lut = model.get_unproject_lut(grid_size_wh=(7, 6)) + report = UnprojectLUTAnalyzer(lut).estimate_accuracy( + interpolations=("nearest", "bilinear", "bicubic") + ) + + assert report.interpolations == ("nearest", "bilinear", "bicubic") + assert np.isfinite(report.max_angular_error_mdeg["nearest"]) + assert np.isfinite(report.max_angular_error_mdeg["bilinear"]) + assert np.isfinite(report.max_angular_error_mdeg["bicubic"]) + assert np.isfinite(report.median_angular_error_mdeg["nearest"]) + assert np.isfinite(report.median_angular_error_mdeg["bilinear"]) + assert np.isfinite(report.median_angular_error_mdeg["bicubic"]) + assert ( + report.max_angular_error_mdeg["bilinear"] + < report.max_angular_error_mdeg["nearest"] + ) + + +def test_unproject_lut_analyzer_accepts_single_interpolation() -> None: + """A single interpolation mode can be passed directly to the analyzer.""" + from lensboy.analysis import UnprojectLUTAnalyzer + + model = _load_opencv_model() + lut = model.get_unproject_lut(grid_size_wh=(7, 6)) + report = UnprojectLUTAnalyzer(lut).estimate_accuracy(interpolations="bicubic") + + assert report.interpolations == ("bicubic",) + assert set(report.max_angular_error_mdeg) == {"bicubic"} + assert set(report.median_angular_error_mdeg) == {"bicubic"} + assert np.isfinite(report.max_angular_error_mdeg["bicubic"]) + assert np.isfinite(report.median_angular_error_mdeg["bicubic"]) + + +def test_unproject_lut_analyzer_matches_loaded_and_in_memory_lut(tmp_path: Path) -> None: + """Loaded LUTs produce the same analyzer report as in-memory LUTs.""" + from lensboy.analysis import UnprojectLUTAnalyzer + + model = _load_opencv_model() + lut = model.get_unproject_lut(grid_size_wh=(7, 6)) + file_path = tmp_path / "opencv.unproject_LUT" + lut.save(file_path) + loaded = lb.UnprojectLUT.load(file_path) + + report_before = UnprojectLUTAnalyzer(lut).estimate_accuracy( + interpolations=("nearest", "bilinear", "bicubic") + ) + report_after = UnprojectLUTAnalyzer(loaded).estimate_accuracy( + interpolations=("nearest", "bilinear", "bicubic") + ) + + assert report_after.interpolations == report_before.interpolations + for mode in report_before.interpolations: + assert report_after.max_angular_error_mdeg[mode] == pytest.approx( + report_before.max_angular_error_mdeg[mode] + ) + assert report_after.median_angular_error_mdeg[mode] == pytest.approx( + report_before.median_angular_error_mdeg[mode] + ) + + +def test_unproject_lut_analyzer_can_sample_dense_accuracy_grid() -> None: + """The analyzer can compare LUT rays against exact rays on a dense sample grid.""" + from lensboy.analysis import UnprojectLUTAnalyzer + + model = _load_opencv_model() + lut = model.get_unproject_lut(grid_size_wh=(7, 6)) + sample = UnprojectLUTAnalyzer(lut).sample_accuracy_grid( + interpolation="bilinear", + target_sample_count=2500, + ) + + expected_sample_count = sample.sample_grid_width * sample.sample_grid_height + assert sample.interpolation == "bilinear" + assert sample.sample_count == expected_sample_count + assert sample.sample_pixels.shape == (expected_sample_count, 2) + assert sample.exact_rays.shape == (expected_sample_count, 3) + assert sample.approx_rays.shape == (expected_sample_count, 3) + assert sample.angular_error_deg.shape == (expected_sample_count,) + assert np.isfinite(sample.angular_error_deg).all() + assert sample.max_angular_error_mdeg == pytest.approx( + float(np.max(sample.angular_error_deg) * 1.0e3) + ) + assert sample.mean_angular_error_mdeg == pytest.approx( + float(np.mean(sample.angular_error_deg) * 1.0e3) + ) + assert sample.median_angular_error_mdeg == pytest.approx( + float(np.median(sample.angular_error_deg) * 1.0e3) + ) + + +def test_unproject_lut_analyzer_dense_accuracy_grid_is_exact_for_linear_model() -> None: + """A linear pinhole LUT matches the exact source model on dense sampled queries.""" + from lensboy.analysis import UnprojectLUTAnalyzer + + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(6, 5)) + sample = UnprojectLUTAnalyzer(lut).sample_accuracy_grid( + interpolation="bilinear", + target_sample_count=2500, + ) + + np.testing.assert_allclose(sample.approx_rays, sample.exact_rays, atol=1e-12) + assert sample.max_angular_error_mdeg < 1e-2 + + +def test_unproject_lut_parallel_grid_build_matches_serial() -> None: + """Parallel LUT grid sampling matches the serial build exactly.""" + model = _make_linear_pinhole_model() + serial_lut = model.get_unproject_lut( + grid_size_wh=(17, 13), + num_workers=1, + ) + parallel_lut = model.get_unproject_lut( + grid_size_wh=(17, 13), + num_workers=2, + ) + + np.testing.assert_allclose(parallel_lut.xy_grid, serial_lut.xy_grid, atol=0.0) + assert parallel_lut.grid_size_wh == serial_lut.grid_size_wh + assert parallel_lut.grid_extents_xy == serial_lut.grid_extents_xy + + +def test_unproject_lut_analyzer_is_stable_across_num_workers() -> None: + """Changing LUT build workers does not affect analyzer results.""" + from lensboy.analysis import UnprojectLUTAnalyzer + + model = _load_opencv_model() + serial_lut = model.get_unproject_lut( + grid_size_wh=(7, 6), + num_workers=1, + ) + parallel_lut = model.get_unproject_lut( + grid_size_wh=(7, 6), + num_workers=2, + ) + serial_report = UnprojectLUTAnalyzer(serial_lut).estimate_accuracy( + interpolations=("nearest", "bilinear", "bicubic") + ) + parallel_report = UnprojectLUTAnalyzer(parallel_lut).estimate_accuracy( + interpolations=("nearest", "bilinear", "bicubic") + ) + + for mode in ("nearest", "bilinear", "bicubic"): + assert parallel_report.max_angular_error_mdeg[mode] == pytest.approx( + serial_report.max_angular_error_mdeg[mode] + ) + assert parallel_report.median_angular_error_mdeg[mode] == pytest.approx( + serial_report.median_angular_error_mdeg[mode] + ) + + +def test_unproject_lut_grid_stride_can_be_fractional() -> None: + """The stored sample spacing reflects the actual grid spacing and may be fractional.""" + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(6, 5)) + + stride_x, stride_y = lut.grid_stride_xy + assert stride_x == pytest.approx(16.0 / 5.0) + assert stride_y == pytest.approx(12.0 / 4.0) + + +def test_unproject_lut_bounds_modes_match_expected_behavior() -> None: + """Strict, clamp, and extrapolate modes behave safely and predictably.""" + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(5, 4)) + pixels = np.array( + [ + [-1.5, -0.5], + [0.0, 0.0], + [model.image_width - 1, model.image_height - 1], + [model.image_width + 1.0, 3.5], + ] + ) + + with pytest.raises(ValueError, match="outside the LUT domain"): + lut.normalize_points(pixels, bounds="strict") + + strict_rays, valid_mask = lut.normalize_points( + pixels, + bounds="strict", + return_valid_mask=True, + ) + np.testing.assert_array_equal(valid_mask, np.array([False, True, True, False])) + assert np.isnan(strict_rays[0, 0]) and np.isnan(strict_rays[0, 1]) + assert np.isnan(strict_rays[3, 0]) and np.isnan(strict_rays[3, 1]) + + clamped_pixels = np.column_stack( + [ + np.clip(pixels[:, 0], 0.0, model.image_width - 1), + np.clip(pixels[:, 1], 0.0, model.image_height - 1), + ] + ) + clamp_expected = model.normalize_points(clamped_pixels) + clamp_actual = lut.normalize_points(pixels, interpolation="bilinear", bounds="clamp") + np.testing.assert_allclose(clamp_actual, clamp_expected, atol=1e-12) + + extrap_expected = model.normalize_points(pixels) + extrap_actual = lut.normalize_points( + pixels, + interpolation="bilinear", + bounds="extrapolate", + ) + np.testing.assert_allclose(extrap_actual, extrap_expected, atol=1e-12) + + +def test_unproject_lut_bicubic_falls_back_to_bilinear_without_full_stencil() -> None: + """Bicubic uses bilinear when a full 4x4 support region is unavailable.""" + model = _load_opencv_model() + + small_lut = model.get_unproject_lut(grid_size_wh=(3, 3)) + sample_pixels = np.array( + [ + [10.0, 20.0], + [512.25, 256.75], + [model.image_width - 10.0, model.image_height - 20.0], + ] + ) + np.testing.assert_allclose( + small_lut.normalize_points(sample_pixels, interpolation="bicubic"), + small_lut.normalize_points(sample_pixels, interpolation="bilinear"), + atol=1e-12, + ) + + lut = model.get_unproject_lut(grid_size_wh=(7, 6)) + boundary_pixels = np.array( + [ + [0.0, 0.0], + [0.0, model.image_height * 0.5], + [model.image_width - 1.0, model.image_height * 0.5], + [model.image_width * 0.5, 0.0], + [model.image_width * 0.5, model.image_height - 1.0], + [-10.0, model.image_height * 0.25], + ] + ) + np.testing.assert_allclose( + lut.normalize_points(boundary_pixels, interpolation="bicubic", bounds="clamp"), + lut.normalize_points(boundary_pixels, interpolation="bilinear", bounds="clamp"), + atol=1e-12, + ) + + +def test_unproject_lut_analyzer_report_is_finite_for_nonlinear_model() -> None: + """A nonlinear model produces finite observed angular error summaries.""" + from lensboy.analysis import UnprojectLUTAnalyzer + + model = _load_opencv_model() + lut = model.get_unproject_lut(grid_size_wh=(7, 6)) + report = UnprojectLUTAnalyzer(lut).estimate_accuracy() + + assert report.interpolations == ("bilinear",) + assert np.isfinite(report.max_angular_error_mdeg["bilinear"]) + assert np.isfinite(report.median_angular_error_mdeg["bilinear"]) + assert ( + report.median_angular_error_mdeg["bilinear"] + <= report.max_angular_error_mdeg["bilinear"] + ) + + xs = np.linspace(0.0, model.image_width - 1, 35) + ys = np.linspace(0.0, model.image_height - 1, 27) + grid_x, grid_y = np.meshgrid(xs, ys, indexing="xy") + pixels = np.column_stack([grid_x.ravel(), grid_y.ravel()]) + exact = model.normalize_points(pixels) + approx = lut.normalize_points(pixels, interpolation="bilinear") + dense_error = _query_error_deg(exact, approx) + assert dense_error <= (report.max_angular_error_mdeg["bilinear"] / 1.0e3) + 1.0 + + +def test_unproject_lut_analyzer_requires_source_model_spec() -> None: + """Analyzer methods fail clearly when the LUT lacks an exact source model spec.""" + from lensboy.analysis import UnprojectLUTAnalyzer + + lut = lb.UnprojectLUT( + image_width=5, + image_height=4, + grid_width=5, + grid_height=4, + grid_x_min=0.0, + grid_x_max=4.0, + grid_y_min=0.0, + grid_y_max=3.0, + storage_encoding="float64_xy", + xy_grid=np.zeros((4, 5, 2), dtype=np.float64), + source_model_type=None, + source_model_spec=None, + ) + + with pytest.raises(ValueError, match="source_model_spec_json"): + UnprojectLUTAnalyzer(lut).estimate_accuracy() + with pytest.raises(ValueError, match="source_model_spec_json"): + UnprojectLUTAnalyzer(lut).sample_accuracy_grid() + + +def test_unproject_lut_rejects_wrong_suffix(tmp_path: Path) -> None: + """Saving requires the `.unproject_LUT` suffix.""" + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(4, 4)) + + with pytest.raises(ValueError, match=".unproject_LUT"): + lut.save(tmp_path / "invalid.bin") + + +def test_unproject_lut_rejects_missing_header_marker(tmp_path: Path) -> None: + """Loading fails cleanly when the text header is incomplete.""" + file_path = tmp_path / "broken.unproject_LUT" + file_path.write_bytes(b"format: lensboy_unproject_LUT\n") + + with pytest.raises(ValueError, match="END_HEADER"): + lb.UnprojectLUT.load(file_path) + + +def test_unproject_lut_rejects_invalid_header_line(tmp_path: Path) -> None: + """Loading fails when a header line does not follow `key: value`.""" + file_path = tmp_path / "broken.unproject_LUT" + file_path.write_bytes(b"format lensboy_unproject_LUT\nEND_HEADER\n") + + with pytest.raises(ValueError, match="key: value"): + lb.UnprojectLUT.load(file_path) + + +def test_unproject_lut_rejects_bad_payload_offset(tmp_path: Path) -> None: + """Loading validates the declared payload byte offset.""" + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(4, 4)) + file_path = tmp_path / "bad_offset.unproject_LUT" + lut.save(file_path) + + _rewrite_header_field( + file_path, + "payload_offset_bytes", + "1", + recompute_payload_offset=False, + ) + + with pytest.raises(ValueError, match="payload_offset_bytes"): + lb.UnprojectLUT.load(file_path) + + +def test_unproject_lut_rejects_bad_source_model_spec_hash(tmp_path: Path) -> None: + """Loading validates the source-model spec hash.""" + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(4, 4)) + file_path = tmp_path / "bad_hash.unproject_LUT" + lut.save(file_path) + + _rewrite_header_field(file_path, "source_model_spec_json_sha256", "deadbeef") + + with pytest.raises(ValueError, match="source_model_spec_json_sha256"): + lb.UnprojectLUT.load(file_path) + + +def test_unproject_lut_rejects_legacy_error_report_header_fields(tmp_path: Path) -> None: + """Loading rejects the older mixed runtime-and-analysis header shape.""" + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(4, 4)) + file_path = tmp_path / "legacy_error_report.unproject_LUT" + lut.save(file_path) + + _append_header_field( + file_path, + "estimated_max_angular_error_mdeg_bilinear", + "12.5", + ) + + with pytest.raises(ValueError, match="legacy error-report header fields"): + lb.UnprojectLUT.load(file_path) + + +def test_unproject_lut_rejects_unsupported_storage_encoding(tmp_path: Path) -> None: + """Loading validates the declared storage encoding.""" + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(4, 4)) + file_path = tmp_path / "encoding.unproject_LUT" + lut.save(file_path) + + _rewrite_header_field(file_path, "storage_encoding", "float128_xy") + + with pytest.raises(ValueError, match="Unsupported storage encoding"): + lb.UnprojectLUT.load(file_path) + + +def test_unproject_lut_rejects_truncated_payload(tmp_path: Path) -> None: + """Loading validates the binary payload length.""" + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(4, 4)) + file_path = tmp_path / "truncated.unproject_LUT" + lut.save(file_path) + + header, payload = _header_and_payload(file_path) + file_path.write_bytes(header.encode("ascii") + payload[:-3]) + + with pytest.raises(ValueError, match="Unexpected payload size"): + lb.UnprojectLUT.load(file_path) + + +@pytest.mark.parametrize("replacement", [np.nan, np.inf]) +def test_unproject_lut_rejects_nonfinite_payload_values( + tmp_path: Path, + replacement: float, +) -> None: + """Loading rejects NaN and infinity in the cached payload.""" + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(4, 4)) + file_path = tmp_path / "nonfinite.unproject_LUT" + lut.save(file_path) + + header, payload = _header_and_payload(file_path) + payload_array = np.frombuffer(payload, dtype=" None: + """Analyzer-generated heatmaps can be saved and loaded without using the LUT header.""" + from lensboy.analysis import UnprojectLUTAnalyzer, UnprojectLUTErrorHeatmap + + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(6, 5)) + heatmap_path = tmp_path / "bilinear_error_heatmaps.npz" + analyzer = UnprojectLUTAnalyzer(lut) + + heatmap = analyzer.compute_error_heatmap(interpolation="bilinear") + heatmap.save(heatmap_path) + loaded = UnprojectLUTErrorHeatmap.load(heatmap_path) + + assert loaded.interpolation == "bilinear" + assert loaded.max_angular_error_deg.shape == (4, 5) + assert loaded.error_direction_xy.shape == (4, 5, 2) + assert loaded.error_delta_xy.shape == (4, 5, 2) + assert loaded.peak_pixel_xy.shape == (4, 5, 2) + assert np.max(loaded.max_angular_error_deg) < 2e-6 + + +def test_plot_unproject_lut_error_heatmap_supports_angular_units(tmp_path: Path) -> None: + """The heatmap plot helper rescales angular error into the requested units.""" + import matplotlib + + from lensboy.analysis import UnprojectLUTAnalyzer, plot_unproject_lut_error_heatmap + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(6, 5)) + heatmap = UnprojectLUTAnalyzer(lut).compute_error_heatmap(interpolation="bilinear") + expected_mdeg = heatmap.max_angular_error_deg * 1.0e3 + + fig = plot_unproject_lut_error_heatmap( + heatmap, + angular_unit="mdeg", + show_directions=False, + return_figure=True, + ) + assert fig is not None + np.testing.assert_allclose(fig.axes[0].images[0].get_array(), expected_mdeg) + assert ( + fig.axes[0].get_title() == "Per-cell max error heatmap (bilinear) [milli degrees]" + ) + assert fig.axes[1].get_ylabel() == "max angular error [milli degrees]" + assert len(fig.axes[0].collections) == 0 + plt.close(fig) + + +def test_plot_unproject_lut_error_heatmap_accepts_figsize(tmp_path: Path) -> None: + """The heatmap plot helper forwards the requested figure size.""" + import matplotlib + + from lensboy.analysis import UnprojectLUTAnalyzer, plot_unproject_lut_error_heatmap + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut(grid_size_wh=(6, 5)) + analyzer = UnprojectLUTAnalyzer(lut) + heatmap_path = tmp_path / "bilinear_error_heatmaps_figsize.npz" + analyzer.save_error_heatmap(heatmap_path, interpolation="bilinear") + + fig = plot_unproject_lut_error_heatmap( + heatmap_path, + figsize=(7.8, 5.3), + return_figure=True, + ) + assert fig is not None + np.testing.assert_allclose(fig.get_size_inches(), np.array([7.8, 5.3])) + assert fig.axes[0].get_aspect() == 1.0 + plt.close(fig) + + +def test_unproject_lut_cpp_smoke(tmp_path: Path) -> None: + """The standalone C++ loader can compile, load, and query a LUT.""" + compiler = shutil.which("c++") or shutil.which("clang++") or shutil.which("g++") + if compiler is None: + pytest.skip("No C++ compiler available.") + + model = _make_linear_pinhole_model() + lut = model.get_unproject_lut( + grid_size_wh=(6, 5), + storage_encoding="float16_xy", + ) + lut_path = tmp_path / "cpp_round_trip.unproject_LUT" + lut.save(lut_path) + loaded = lb.UnprojectLUT.load(lut_path) + + program_path = tmp_path / "smoke.cpp" + binary_path = tmp_path / "smoke" + program_path.write_text( + textwrap.dedent( + """ + #include "unproject_lut.hpp" + + #include + #include + + int main(int argc, char** argv) { + auto const lut = lensboy::UnprojectLUT::load(argv[1]); + + auto const q0 = lut.query( + 4.25, + 3.5, + lensboy::InterpolationMode::kBilinear, + lensboy::BoundsMode::kStrict + ); + auto const q1 = lut.query( + -1.0, + 2.0, + lensboy::InterpolationMode::kBilinear, + lensboy::BoundsMode::kStrict + ); + auto const q2 = lut.query( + -1.0, + 2.0, + lensboy::InterpolationMode::kBilinear, + lensboy::BoundsMode::kExtrapolate, + false + ); + + std::cout << std::setprecision(17); + std::cout << q0.valid << " " << q0.ray[0] << " " << q0.ray[1] << " " + << q0.ray[2] << "\\n"; + std::cout << q1.valid << "\\n"; + std::cout << q2.valid << " " << q2.ray[0] << " " << q2.ray[1] << " " + << q2.ray[2] << "\\n"; + auto const q3 = lut.query( + 0.0, + 2.0, + lensboy::InterpolationMode::kBicubic, + lensboy::BoundsMode::kStrict + ); + std::cout << q3.valid << " " << q3.ray[0] << " " << q3.ray[1] << " " + << q3.ray[2] << "\\n"; + return 0; + } + """ + ) + ) + + compile_result = subprocess.run( + [ + compiler, + "-std=c++17", + str(program_path), + str(CPP_RUNTIME_DIR / "unproject_lut.cpp"), + "-I", + str(CPP_RUNTIME_DIR), + "-O2", + "-o", + str(binary_path), + ], + check=True, + capture_output=True, + text=True, + ) + assert compile_result.returncode == 0 + + run_result = subprocess.run( + [str(binary_path), str(lut_path)], + check=True, + capture_output=True, + text=True, + ) + lines = run_result.stdout.strip().splitlines() + assert len(lines) == 4 + + q0_tokens = lines[0].split() + assert q0_tokens[0] == "1" + python_q0 = loaded.normalize_points( + np.array([[4.25, 3.5]]), + interpolation="bilinear", + bounds="strict", + )[0] + python_q0 = python_q0 / np.linalg.norm(python_q0) + np.testing.assert_allclose( + np.array([float(q0_tokens[1]), float(q0_tokens[2]), float(q0_tokens[3])]), + python_q0, + atol=1e-12, + ) + + assert lines[1] == "0" + + q2_tokens = lines[2].split() + assert q2_tokens[0] == "1" + python_q2 = loaded.normalize_points( + np.array([[-1.0, 2.0]]), + interpolation="bilinear", + bounds="extrapolate", + )[0] + np.testing.assert_allclose( + np.array([float(q2_tokens[1]), float(q2_tokens[2]), float(q2_tokens[3])]), + python_q2, + atol=1e-12, + ) + + q3_tokens = lines[3].split() + assert q3_tokens[0] == "1" + python_q3 = loaded.normalize_points( + np.array([[0.0, 2.0]]), + interpolation="bilinear", + bounds="strict", + )[0] + python_q3 = python_q3 / np.linalg.norm(python_q3) + np.testing.assert_allclose( + np.array([float(q3_tokens[1]), float(q3_tokens[2]), float(q3_tokens[3])]), + python_q3, + atol=5e-4, + ) diff --git a/uv.lock b/uv.lock index e3e85f4..4c57ab6 100644 --- a/uv.lock +++ b/uv.lock @@ -585,7 +585,7 @@ wheels = [ [[package]] name = "lensboy" -version = "1.0.5" +version = "3.0.1" source = { editable = "." } dependencies = [ { name = "numpy" }, @@ -595,6 +595,7 @@ dependencies = [ [package.optional-dependencies] analysis = [ { name = "matplotlib" }, + { name = "scipy" }, ] [package.dev-dependencies] @@ -603,6 +604,7 @@ dev = [ { name = "imageio" }, { name = "ipykernel" }, { name = "jupytext" }, + { name = "matplotlib" }, { name = "mediapy" }, { name = "pybind11" }, { name = "pybind11-stubgen" }, @@ -610,6 +612,7 @@ dev = [ { name = "pytest" }, { name = "ruff" }, { name = "scikit-build-core" }, + { name = "scipy" }, { name = "slamd" }, { name = "tqdm" }, ] @@ -619,6 +622,7 @@ requires-dist = [ { name = "matplotlib", marker = "extra == 'analysis'", specifier = ">=3.8" }, { name = "numpy", specifier = ">=1.26" }, { name = "opencv-contrib-python", specifier = ">=4.8" }, + { name = "scipy", marker = "extra == 'analysis'", specifier = ">=1.11" }, ] provides-extras = ["analysis"] @@ -628,6 +632,7 @@ dev = [ { name = "imageio", specifier = ">=2.37.2" }, { name = "ipykernel", specifier = ">=7.0.1" }, { name = "jupytext", specifier = ">=1.18.1" }, + { name = "matplotlib", specifier = ">=3.8" }, { name = "mediapy", specifier = ">=1.2.4" }, { name = "pybind11", specifier = ">=3.0.1" }, { name = "pybind11-stubgen", specifier = ">=2.5.5" }, @@ -635,6 +640,7 @@ dev = [ { name = "pytest", specifier = ">=9.0.2" }, { name = "ruff", specifier = ">=0.14.1" }, { name = "scikit-build-core", specifier = ">=0.11.6" }, + { name = "scipy", specifier = ">=1.11" }, { name = "slamd", specifier = ">=2.1.12" }, { name = "tqdm", specifier = ">=4.67.1" }, ] @@ -1444,6 +1450,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/49/ec16b3db6893db788ae35f98506ff5a9c25dca7eb18cc38ada8a4c1dc944/scikit_build_core-0.11.6-py3-none-any.whl", hash = "sha256:ce6d8fe64e6b4c759ea0fb95d2f8a68f60d2df31c2989838633b8ec930736360", size = 185764, upload-time = "2025-08-22T22:11:52.438Z" }, ] +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + [[package]] name = "six" version = "1.17.0" From 7b767ff2be99b2214463668724422231cd705542 Mon Sep 17 00:00:00 2001 From: Robertleoj Date: Sun, 12 Apr 2026 16:23:06 +0200 Subject: [PATCH 02/27] wip --- CMakeLists.txt | 3 + cpp_src/main.cpp | 24 + cpp_src/seeded_normalize.cpp | 634 +++++++++++++++++++++ cpp_src/seeded_normalize.hpp | 34 ++ src/lensboy/analysis/__init__.py | 2 +- src/lensboy/analysis/plots.py | 5 +- src/lensboy/analysis/unproject_lut.py | 32 +- src/lensboy/camera_models/unproject_lut.py | 233 ++++++-- src/lensboy/lensboy_bindings.pyi | 6 +- tests/test_unproject_lut.py | 10 +- 10 files changed, 932 insertions(+), 51 deletions(-) create mode 100644 cpp_src/seeded_normalize.cpp create mode 100644 cpp_src/seeded_normalize.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 870b133..5475082 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ find_package(Eigen3 REQUIRED) find_package(fmt CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) +find_package(OpenMP REQUIRED) pybind11_add_module( @@ -44,6 +45,7 @@ pybind11_add_module( cpp_src/matching_spline_model.cpp cpp_src/pinhole_splined_fine_tune.cpp cpp_src/normalize_pinhole_splined.cpp + cpp_src/seeded_normalize.cpp ) target_compile_definitions(lensboy_bindings @@ -59,6 +61,7 @@ target_link_libraries( Eigen3::Eigen fmt::fmt spdlog::spdlog + OpenMP::OpenMP_CXX ) if(APPLE) diff --git a/cpp_src/main.cpp b/cpp_src/main.cpp index 53f3c32..3faf046 100644 --- a/cpp_src/main.cpp +++ b/cpp_src/main.cpp @@ -3,6 +3,7 @@ #include #include #include "./python_camera_functions.hpp" +#include "./seeded_normalize.hpp" #include "calibrate.hpp" #include "cameramodels.hpp" @@ -239,6 +240,29 @@ PYBIND11_MODULE( py::arg("image_size_wh") ); + m.def( + "seeded_normalize_opencv", + &lensboy::seeded_normalize_opencv, + py::arg("seed_pixels"), + py::arg("seed_normals"), + py::arg("seed_w"), + py::arg("seed_h"), + py::arg("query_pixels"), + py::arg("intrinsics") + ); + + m.def( + "seeded_normalize_splined", + &lensboy::seeded_normalize_splined, + py::arg("seed_pixels"), + py::arg("seed_normals"), + py::arg("seed_w"), + py::arg("seed_h"), + py::arg("query_pixels"), + py::arg("config"), + py::arg("intrinsics") + ); + m.def( "set_log_level", [](const std::string& level) { diff --git a/cpp_src/seeded_normalize.cpp b/cpp_src/seeded_normalize.cpp new file mode 100644 index 0000000..f63cb44 --- /dev/null +++ b/cpp_src/seeded_normalize.cpp @@ -0,0 +1,634 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "./cameramodels.hpp" +#include "./pybind_utils.hpp" + +namespace lensboy { +namespace py = pybind11; + +// --------------------------------------------------------------------------- +// Hash grid for O(1) nearest-neighbor lookup over seed pixels. +// +// Cell size = minimum adjacent seed spacing. Each cell holds a short list +// of seed indices. Query checks the cell + 8 neighbors. +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// Flat-array spatial index for O(1) nearest-neighbor lookup. +// +// Cell size = min adjacent seed spacing. Flat 2D array of short vectors. +// Query checks the cell + 8 neighbors. +// --------------------------------------------------------------------------- + +class SeedGrid { + public: + SeedGrid( + const double* pixel_xy, + int n, + int seed_w, + int seed_h + ) { + // Find min adjacent edge length in pixel space + double min_edge_sq = std::numeric_limits::max(); + for (int j = 0; j < seed_h; j++) { + for (int i = 0; i < seed_w; i++) { + int idx = j * seed_w + i; + double x0 = pixel_xy[idx * 2], y0 = pixel_xy[idx * 2 + 1]; + if (!std::isfinite(x0)) continue; + if (i + 1 < seed_w) { + int idx1 = j * seed_w + (i + 1); + double x1 = pixel_xy[idx1 * 2], y1 = pixel_xy[idx1 * 2 + 1]; + if (std::isfinite(x1)) { + double d = (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0); + if (d > 0) min_edge_sq = std::min(min_edge_sq, d); + } + } + if (j + 1 < seed_h) { + int idx1 = (j + 1) * seed_w + i; + double x1 = pixel_xy[idx1 * 2], y1 = pixel_xy[idx1 * 2 + 1]; + if (std::isfinite(x1)) { + double d = (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0); + if (d > 0) min_edge_sq = std::min(min_edge_sq, d); + } + } + } + } + cell_size_ = std::sqrt(min_edge_sq); + if (cell_size_ < 1e-6) cell_size_ = 1.0; + inv_cell_ = 1.0 / cell_size_; + + // Bounding box + double xmin = 1e30, xmax = -1e30, ymin = 1e30, ymax = -1e30; + for (int i = 0; i < n; i++) { + double x = pixel_xy[i * 2], y = pixel_xy[i * 2 + 1]; + if (!std::isfinite(x)) continue; + xmin = std::min(xmin, x); xmax = std::max(xmax, x); + ymin = std::min(ymin, y); ymax = std::max(ymax, y); + } + x_min_ = xmin; + y_min_ = ymin; + gw_ = (int)std::ceil((xmax - xmin) * inv_cell_) + 1; + gh_ = (int)std::ceil((ymax - ymin) * inv_cell_) + 1; + + // Flat array, each cell stores up to 4 indices (enough for ~1 point + // per cell). Use -1 as sentinel. + constexpr int CELL_CAP = 4; + cells_.assign((size_t)gw_ * gh_ * CELL_CAP, -1); + + for (int i = 0; i < n; i++) { + double x = pixel_xy[i * 2], y = pixel_xy[i * 2 + 1]; + if (!std::isfinite(x) || !std::isfinite(y)) continue; + int cx = to_cx(x), cy = to_cy(y); + int base = (cy * gw_ + cx) * CELL_CAP; + for (int s = 0; s < CELL_CAP; s++) { + if (cells_[base + s] < 0) { + cells_[base + s] = i; + break; + } + } + } + } + + int nearest(double qx, double qy, const double* pixel_xy) const { + constexpr int CELL_CAP = 4; + int cx = to_cx(qx), cy = to_cy(qy); + int best = -1; + double best_sq = std::numeric_limits::max(); + for (int dy = -1; dy <= 1; dy++) { + int ry = cy + dy; + if (ry < 0 || ry >= gh_) continue; + for (int dx = -1; dx <= 1; dx++) { + int rx = cx + dx; + if (rx < 0 || rx >= gw_) continue; + int base = (ry * gw_ + rx) * CELL_CAP; + for (int s = 0; s < CELL_CAP; s++) { + int idx = cells_[base + s]; + if (idx < 0) break; + double ex = qx - pixel_xy[idx * 2]; + double ey = qy - pixel_xy[idx * 2 + 1]; + double d = ex * ex + ey * ey; + if (d < best_sq) { best_sq = d; best = idx; } + } + } + } + return best; + } + + private: + double cell_size_, inv_cell_, x_min_, y_min_; + int gw_, gh_; + std::vector cells_; + + int to_cx(double x) const { + return std::max(0, std::min(gw_ - 1, (int)std::floor((x - x_min_) * inv_cell_))); + } + int to_cy(double y) const { + return std::max(0, std::min(gh_ - 1, (int)std::floor((y - y_min_) * inv_cell_))); + } +}; + +// --------------------------------------------------------------------------- +// Inverse-distance weighted interpolation from the 4 quad corners. +// +// The NN tells us grid node (ni, nj). The query pixel's position relative +// to that node picks one of the 4 adjacent quads. Then IDW over the 4 +// corners of that quad gives the initial guess -- no iteration needed. +// --------------------------------------------------------------------------- + +static bool idw_interp_from_nn( + double px, + double py, + int nearest_idx, + int seed_w, + int seed_h, + const double* seed_pixels, + const double* seed_normals, + double& nx_out, + double& ny_out +) { + int ni = nearest_idx % seed_w; + int nj = nearest_idx / seed_w; + double nn_px = seed_pixels[nearest_idx * 2]; + double nn_py = seed_pixels[nearest_idx * 2 + 1]; + + // Pick the quad based on which side of the NN the query falls + int i0 = (px >= nn_px) ? ni : ni - 1; + int j0 = (py >= nn_py) ? nj : nj - 1; + // Clamp to valid quad range + i0 = std::max(0, std::min(seed_w - 2, i0)); + j0 = std::max(0, std::min(seed_h - 2, j0)); + + int idx[4] = { + j0 * seed_w + i0, + j0 * seed_w + (i0 + 1), + (j0 + 1) * seed_w + i0, + (j0 + 1) * seed_w + (i0 + 1), + }; + + // Check all 4 corners are valid + for (int c = 0; c < 4; c++) { + if (!std::isfinite(seed_pixels[idx[c] * 2])) return false; + } + + // Approximate bilinear (u,v) by projecting onto the quad edges. + const double* p00 = &seed_pixels[idx[0] * 2]; + const double* p10 = &seed_pixels[idx[1] * 2]; + const double* p01 = &seed_pixels[idx[2] * 2]; + double ex = p10[0] - p00[0], ey = p10[1] - p00[1]; + double fx = p01[0] - p00[0], fy = p01[1] - p00[1]; + double qx = px - p00[0], qy = py - p00[1]; + + double det = ex * fy - fx * ey; + if (std::abs(det) < 1e-30) return false; + double inv = 1.0 / det; + double u = std::max(0.0, std::min(1.0, (qx * fy - fx * qy) * inv)); + double v = std::max(0.0, std::min(1.0, (ex * qy - qx * ey) * inv)); + + double mu = 1.0 - u, mv = 1.0 - v; + const double* n00 = &seed_normals[idx[0] * 2]; + const double* n10 = &seed_normals[idx[1] * 2]; + const double* n01 = &seed_normals[idx[2] * 2]; + const double* n11 = &seed_normals[idx[3] * 2]; + nx_out = mu * mv * n00[0] + u * mv * n10[0] + mu * v * n01[0] + u * v * n11[0]; + ny_out = mu * mv * n00[1] + u * mv * n10[1] + mu * v * n01[1] + u * v * n11[1]; + return true; +} + +// --------------------------------------------------------------------------- +// Newton refinement: generic 2D solver using Ceres Jet autodiff. +// --------------------------------------------------------------------------- + +// Forward-project (nx, ny) -> (px, py) for the OpenCV model. +template +static inline void forward_opencv( + const T& nx, + const T& ny, + const T* intrinsics, // fx, fy, cx, cy, dist[14] + T& px, + T& py +) { + Vec3 point(nx, ny, T(1)); + Vec2 result; + project_opencv(intrinsics, point, result); + px = result[0]; + py = result[1]; +} + +// Forward-project (nx, ny) -> (px, py) for the pinhole-splined model. +template +static inline void forward_splined( + const T& nx, + const T& ny, + PinholeSplinedConfig* config, + const T* pinhole_params, + const T* dx_grid, + const T* dy_grid, + T& px, + T& py +) { + Vec3 point(nx, ny, T(1)); + Vec2 result; + project_pinhole_splined(config, pinhole_params, dx_grid, dy_grid, point, result); + px = result[0]; + py = result[1]; +} + +// Newton refinement for OpenCV model, starting from initial guess (nx, ny). +static void refine_opencv( + double target_u, + double target_v, + double& nx, + double& ny, + const double* intrinsics // fx, fy, cx, cy, dist[14] +) { + using Jet = ceres::Jet; + constexpr int max_iter = 20; + constexpr double tol_sq = 1e-14; + + // Convert intrinsics to Jet constants (only nx, ny are variables) + std::array jintrinsics; + for (int i = 0; i < 18; i++) { + jintrinsics[i] = Jet(intrinsics[i]); + } + + for (int iter = 0; iter < max_iter; iter++) { + Jet jnx(nx, 0); + Jet jny(ny, 1); + Jet jpx, jpy; + forward_opencv(jnx, jny, jintrinsics.data(), jpx, jpy); + + double r0 = jpx.a - target_u; + double r1 = jpy.a - target_v; + if (r0 * r0 + r1 * r1 < tol_sq) break; + + double J00 = jpx.v[0], J01 = jpx.v[1]; + double J10 = jpy.v[0], J11 = jpy.v[1]; + double det = J00 * J11 - J01 * J10; + if (std::abs(det) < 1e-30) break; + double inv = 1.0 / det; + nx -= inv * (J11 * r0 - J01 * r1); + ny -= inv * (-J10 * r0 + J00 * r1); + } +} + +struct SplineConstants { + int Nx, Ny; + double half_x, half_y, x_scale, y_scale; + double fx, fy, cx, cy; + + explicit SplineConstants( + PinholeSplinedConfig* config, + const double* pinhole_params + ) + : Nx((int)config->num_knots_x), + Ny((int)config->num_knots_y), + fx(pinhole_params[0]), + fy(pinhole_params[1]), + cx(pinhole_params[2]), + cy(pinhole_params[3]) { + const double fov_rad_x = config->fov_deg_x * M_PI / 180.0; + const double fov_rad_y = config->fov_deg_y * M_PI / 180.0; + half_x = stereo_half_range(fov_rad_x); + half_y = stereo_half_range(fov_rad_y); + x_scale = (Nx - 3) / (2.0 * half_x); + y_scale = (Ny - 3) / (2.0 * half_y); + } +}; + +// Newton refinement for pinhole-splined model. +static void refine_splined( + double target_u, + double target_v, + double& nx, + double& ny, + const SplineConstants& sc, + const double* dx_grid, + const double* dy_grid, + int* out_rebuilds = nullptr, + int* out_iters = nullptr +) { + using Jet = ceres::Jet; + constexpr int max_newton = 15; + constexpr int max_rebuilds = 5; + constexpr double tol_sq = 1e-20; + constexpr double eps = 1e-12; + + const int Nx = sc.Nx; + const int Ny = sc.Ny; + const double half_x = sc.half_x; + const double half_y = sc.half_y; + const double x_scale = sc.x_scale; + const double y_scale = sc.y_scale; + + int rebuild_count = 0, iter_count = 0; + for (int rebuild = 0; rebuild < max_rebuilds; rebuild++) { + rebuild_count++; + double sx, sy; + normalized_to_stereographic(nx, ny, sx, sy); + double gx = std::max( + 0.0, std::min(1.0 + (sx + half_x) * x_scale, Nx - 1.0 - eps) + ); + double gy = std::max( + 0.0, std::min(1.0 + (sy + half_y) * y_scale, Ny - 1.0 - eps) + ); + const int ix0 = (int)std::floor(gx); + const int iy0 = (int)std::floor(gy); + + double local_dx[16], local_dy[16]; + int kidx = 0; + for (int b = 0; b < 4; b++) { + const int yy = clamp_int(iy0 + b - 1, 0, Ny - 1); + for (int a = 0; a < 4; a++) { + const int xx = clamp_int(ix0 + a - 1, 0, Nx - 1); + local_dx[kidx] = dx_grid[yy * Nx + xx]; + local_dy[kidx] = dy_grid[yy * Nx + xx]; + kidx++; + } + } + + for (int iter = 0; iter < max_newton; iter++) { + iter_count++; + Jet jnx(nx, 0); + Jet jny(ny, 1); + Jet jsx, jsy; + normalized_to_stereographic(jnx, jny, jsx, jsy); + Jet jgx = clamp_T( + Jet(1.0) + (jsx + Jet(half_x)) * Jet(x_scale), + Jet(0.0), + Jet(Nx - 1.0 - eps) + ); + Jet jgy = clamp_T( + Jet(1.0) + (jsy + Jet(half_y)) * Jet(y_scale), + Jet(0.0), + Jet(Ny - 1.0 - eps) + ); + Jet ju = jgx - Jet((double)ix0); + Jet jv = jgy - Jet((double)iy0); + Jet wx[4], wy[4]; + cubic_bspline_basis_uniform(ju, wx); + cubic_bspline_basis_uniform(jv, wy); + Jet dx_val(0.0), dy_val(0.0); + int ki = 0; + for (int b = 0; b < 4; b++) { + for (int a = 0; a < 4; a++) { + Jet w = wy[b] * wx[a]; + dx_val += Jet(local_dx[ki]) * w; + dy_val += Jet(local_dy[ki]) * w; + ki++; + } + } + Jet r0 = + Jet(sc.fx) * (jnx + dx_val) + + Jet(sc.cx) - Jet(target_u); + Jet r1 = + Jet(sc.fy) * (jny + dy_val) + + Jet(sc.cy) - Jet(target_v); + double res0 = r0.a, res1 = r1.a; + if (res0 * res0 + res1 * res1 < tol_sq) break; + double J00 = r0.v[0], J01 = r0.v[1]; + double J10 = r1.v[0], J11 = r1.v[1]; + double det = J00 * J11 - J01 * J10; + if (std::abs(det) < 1e-30) break; + double inv = 1.0 / det; + nx -= inv * (J11 * res0 - J01 * res1); + ny -= inv * (-J10 * res0 + J00 * res1); + } + + normalized_to_stereographic(nx, ny, sx, sy); + gx = std::max( + 0.0, std::min(1.0 + (sx + half_x) * x_scale, Nx - 1.0 - eps) + ); + gy = std::max( + 0.0, std::min(1.0 + (sy + half_y) * y_scale, Ny - 1.0 - eps) + ); + if ((int)std::floor(gx) == ix0 && (int)std::floor(gy) == iy0) break; + } + if (out_rebuilds) *out_rebuilds = rebuild_count; + if (out_iters) *out_iters = iter_count; +} + +// --------------------------------------------------------------------------- +// Python-facing entry points. +// --------------------------------------------------------------------------- + +py::array_t seeded_normalize_opencv( + py::array_t seed_pixels, + py::array_t + seed_normals, + int seed_w, + int seed_h, + py::array_t + query_pixels, + py::array_t intrinsics +) { + auto sp = seed_pixels.request(); + auto sn = seed_normals.request(); + auto qp = query_pixels.request(); + auto ip = intrinsics.request(); + + require(sp.ndim == 2 && sp.shape[1] == 2, "seed_pixels must be (M, 2)"); + require( + sn.ndim == 2 && sn.shape[1] == 2, "seed_normals must be (M, 2)" + ); + require( + sp.shape[0] == sn.shape[0], + "seed_pixels and seed_normals must have same length" + ); + require( + sp.shape[0] == (ssize_t)(seed_w * seed_h), + "seed length must equal seed_w * seed_h" + ); + require( + qp.ndim == 2 && qp.shape[1] == 2, "query_pixels must be (N, 2)" + ); + require( + ip.ndim == 1 && ip.shape[0] == 18, + "intrinsics must be (18,): fx, fy, cx, cy, dist[14]" + ); + + const int M = (int)sp.shape[0]; + const ssize_t N = qp.shape[0]; + const double* SP = static_cast(sp.ptr); + const double* SN = static_cast(sn.ptr); + const double* QP = static_cast(qp.ptr); + const double* IP = static_cast(ip.ptr); + + const double fx = IP[0], fy = IP[1], cx = IP[2], cy = IP[3]; + + py::array_t out({N, (ssize_t)3}); + double* O = static_cast(out.request().ptr); + + auto t0 = std::chrono::high_resolution_clock::now(); + SeedGrid grid(SP, M, seed_w, seed_h); + auto t1 = std::chrono::high_resolution_clock::now(); + + py::gil_scoped_release release; + + auto t_loop = std::chrono::high_resolution_clock::now(); + + #pragma omp parallel for schedule(static) + for (ssize_t i = 0; i < N; i++) { + double px = QP[i * 2]; + double py = QP[i * 2 + 1]; + + double nx = (px - cx) / fx; + double ny = (py - cy) / fy; + + int nearest = grid.nearest(px, py, SP); + if (nearest >= 0) { + double interp_nx, interp_ny; + if (idw_interp_from_nn( + px, py, nearest, seed_w, seed_h, SP, SN, + interp_nx, interp_ny)) { + nx = interp_nx; + ny = interp_ny; + } else { + nx = SN[nearest * 2]; + ny = SN[nearest * 2 + 1]; + } + } + + refine_opencv(px, py, nx, ny, IP); + + O[i * 3 + 0] = nx; + O[i * 3 + 1] = ny; + O[i * 3 + 2] = 1.0; + } + + auto t2 = std::chrono::high_resolution_clock::now(); + double grid_ms = std::chrono::duration_cast(t1 - t0).count() / 1000.0; + double loop_ms = std::chrono::duration_cast(t2 - t_loop).count() / 1000.0; + std::fprintf(stderr, + "[opencv] grid=%.1fms loop=%.1fms N=%lld\n", + grid_ms, loop_ms, (long long)N); + + return out; +} + +py::array_t seeded_normalize_splined( + py::array_t seed_pixels, + py::array_t + seed_normals, + int seed_w, + int seed_h, + py::array_t + query_pixels, + PinholeSplinedConfig& config, + PinholeSplinedIntrinsicsParameters& params +) { + auto sp = seed_pixels.request(); + auto sn = seed_normals.request(); + auto qp = query_pixels.request(); + + require(sp.ndim == 2 && sp.shape[1] == 2, "seed_pixels must be (M, 2)"); + require( + sn.ndim == 2 && sn.shape[1] == 2, "seed_normals must be (M, 2)" + ); + require( + sp.shape[0] == sn.shape[0], + "seed_pixels and seed_normals must have same length" + ); + require( + sp.shape[0] == (ssize_t)(seed_w * seed_h), + "seed length must equal seed_w * seed_h" + ); + require( + qp.ndim == 2 && qp.shape[1] == 2, "query_pixels must be (N, 2)" + ); + + auto dxb = params.dx_grid.request(); + auto dyb = params.dy_grid.request(); + require( + (uint32_t)dxb.shape[0] == config.num_knots_y && + (uint32_t)dxb.shape[1] == config.num_knots_x, + "dx_grid shape mismatch" + ); + require( + (uint32_t)dyb.shape[0] == config.num_knots_y && + (uint32_t)dyb.shape[1] == config.num_knots_x, + "dy_grid shape mismatch" + ); + + auto ppb = params.pinhole_parameters.request(); + require( + ppb.ndim == 1 && ppb.shape[0] == 4, + "pinhole_parameters must be (4,)" + ); + const double* pinhole_params = static_cast(ppb.ptr); + const double fx = pinhole_params[0], fy = pinhole_params[1], + cx = pinhole_params[2], cy = pinhole_params[3]; + require(fx != 0.0 && fy != 0.0, "fx/fy must be non-zero"); + + const double* dxp = static_cast(dxb.ptr); + const double* dyp = static_cast(dyb.ptr); + + const int M = (int)sp.shape[0]; + const ssize_t N = qp.shape[0]; + const double* SP = static_cast(sp.ptr); + const double* SN = static_cast(sn.ptr); + const double* QP = static_cast(qp.ptr); + + py::array_t out({N, (ssize_t)3}); + double* O = static_cast(out.request().ptr); + + SplineConstants sc(&config, pinhole_params); + + auto t0 = std::chrono::high_resolution_clock::now(); + SeedGrid grid(SP, M, seed_w, seed_h); + auto t1 = std::chrono::high_resolution_clock::now(); + + py::gil_scoped_release release; + + auto t_loop = std::chrono::high_resolution_clock::now(); + + #pragma omp parallel for schedule(static) + for (ssize_t i = 0; i < N; i++) { + double px = QP[i * 2]; + double py_val = QP[i * 2 + 1]; + + double nx = (px - cx) / fx; + double ny = (py_val - cy) / fy; + + int nearest = grid.nearest(px, py_val, SP); + if (nearest >= 0) { + double interp_nx, interp_ny; + if (idw_interp_from_nn( + px, py_val, nearest, seed_w, seed_h, SP, SN, + interp_nx, interp_ny)) { + nx = interp_nx; + ny = interp_ny; + } else { + nx = SN[nearest * 2]; + ny = SN[nearest * 2 + 1]; + } + } + + refine_splined(px, py_val, nx, ny, sc, dxp, dyp); + + O[i * 3 + 0] = nx; + O[i * 3 + 1] = ny; + O[i * 3 + 2] = 1.0; + } + + auto t2 = std::chrono::high_resolution_clock::now(); + double grid_ms = std::chrono::duration_cast(t1 - t0).count() / 1000.0; + double loop_ms = std::chrono::duration_cast(t2 - t_loop).count() / 1000.0; + std::fprintf(stderr, + "[spline] grid=%.1fms loop=%.1fms N=%lld\n", + grid_ms, loop_ms, (long long)N); + + return out; +} + +} // namespace lensboy diff --git a/cpp_src/seeded_normalize.hpp b/cpp_src/seeded_normalize.hpp new file mode 100644 index 0000000..9b813b0 --- /dev/null +++ b/cpp_src/seeded_normalize.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include "./cameramodels.hpp" + +namespace lensboy { +namespace py = pybind11; + +py::array_t seeded_normalize_opencv( + py::array_t seed_pixels, + py::array_t + seed_normals, + int seed_w, + int seed_h, + py::array_t + query_pixels, + py::array_t intrinsics +); + +py::array_t seeded_normalize_splined( + py::array_t seed_pixels, + py::array_t + seed_normals, + int seed_w, + int seed_h, + py::array_t + query_pixels, + PinholeSplinedConfig& config, + PinholeSplinedIntrinsicsParameters& params +); + +} // namespace lensboy diff --git a/src/lensboy/analysis/__init__.py b/src/lensboy/analysis/__init__.py index b11a146..522f879 100644 --- a/src/lensboy/analysis/__init__.py +++ b/src/lensboy/analysis/__init__.py @@ -13,7 +13,7 @@ ] try: - from lensboy.analysis.plots import ( + from lensboy.analysis.plots import ( # noqa: F401 draw_points, plot_detection_coverage, plot_distortion_grid, diff --git a/src/lensboy/analysis/plots.py b/src/lensboy/analysis/plots.py index 981de6a..ad83442 100644 --- a/src/lensboy/analysis/plots.py +++ b/src/lensboy/analysis/plots.py @@ -1,7 +1,10 @@ from __future__ import annotations from pathlib import Path -from typing import Literal +from typing import TYPE_CHECKING, Literal + +if TYPE_CHECKING: + from lensboy.analysis.unproject_lut import UnprojectLUTErrorHeatmap import cv2 import matplotlib.colors as mcolors diff --git a/src/lensboy/analysis/unproject_lut.py b/src/lensboy/analysis/unproject_lut.py index 6155b7a..ee72d88 100644 --- a/src/lensboy/analysis/unproject_lut.py +++ b/src/lensboy/analysis/unproject_lut.py @@ -4,7 +4,7 @@ from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal, cast import numpy as np @@ -534,10 +534,14 @@ class UnprojectLUTErrorHeatmap: interpolation: Interpolation mode represented by the heatmap. max_depth: Maximum adaptive subdivision depth. min_cell_size: Minimum subcell size in pixels. - cell_x_edges: Cell x edges with shape ``(grid_width,)`` or ``(grid_width + 1,)``. - cell_y_edges: Cell y edges with shape ``(grid_height,)`` or ``(grid_height + 1,)``. - max_angular_error_deg: Per-cell maximum angular error, shape ``(H, W)``. - error_direction_xy: Unit x/y direction of the local peak error, shape ``(H, W, 2)``. + cell_x_edges: Cell x edges, shape ``(grid_width,)`` + or ``(grid_width + 1,)``. + cell_y_edges: Cell y edges, shape ``(grid_height,)`` + or ``(grid_height + 1,)``. + max_angular_error_deg: Per-cell maximum angular error, + shape ``(H, W)``. + error_direction_xy: Unit x/y direction of the local peak + error, shape ``(H, W, 2)``. error_delta_xy: Peak x/y interpolation error vector, shape ``(H, W, 2)``. peak_pixel_xy: Pixel location of the local peak error, shape ``(H, W, 2)``. @@ -626,7 +630,10 @@ def load(path: Path | str) -> UnprojectLUTErrorHeatmap: """ with np.load(Path(path)) as heatmap_data: return UnprojectLUTErrorHeatmap( - interpolation=str(np.asarray(heatmap_data["interpolation"]).item()), + interpolation=cast( + InterpolationMode, + str(np.asarray(heatmap_data["interpolation"]).item()), + ), max_depth=int(np.asarray(heatmap_data["max_depth"]).item()), min_cell_size=float(np.asarray(heatmap_data["min_cell_size"]).item()), cell_x_edges=np.asarray(heatmap_data["cell_x_edges"], dtype=np.float64), @@ -809,10 +816,13 @@ def sample_accuracy_grid( target_sample_count=target_sample_count, ) exact_rays = self._normalize_points_exact()(sample_pixels) - approx_rays = self.lut.normalize_points( - sample_pixels, - interpolation=interpolation, - bounds="strict", + approx_rays = cast( + np.ndarray, + self.lut.normalize_points( + sample_pixels, + interpolation=interpolation, + bounds="strict", + ), ) angular_error_deg = _angular_error_deg_from_xy( np.asarray(exact_rays[:, :2], dtype=np.float64), @@ -866,7 +876,7 @@ def plot_error_heatmap( max_depth: int = _DEFAULT_ERROR_MAX_DEPTH, min_cell_size: float = _DEFAULT_ERROR_MIN_CELL_SIZE, title: str | None = None, - angular_unit: str = "mdeg", + angular_unit: Literal["deg", "mdeg", "udeg", "rad", "mrad", "urad"] = "mdeg", show_directions: bool = True, arrow_grid: int = 28, arrow_scale: float = 0.5, diff --git a/src/lensboy/camera_models/unproject_lut.py b/src/lensboy/camera_models/unproject_lut.py index ffaaad7..eff6b75 100644 --- a/src/lensboy/camera_models/unproject_lut.py +++ b/src/lensboy/camera_models/unproject_lut.py @@ -14,6 +14,8 @@ if TYPE_CHECKING: from lensboy.camera_models.base_model import CameraModel + from lensboy.camera_models.opencv import OpenCV + from lensboy.camera_models.pinhole_splined import PinholeSplined InterpolationMode = Literal["nearest", "bilinear", "bicubic"] BoundsMode = Literal["strict", "clamp", "extrapolate"] @@ -176,6 +178,192 @@ def _catmull_rom_weights(t: np.ndarray) -> np.ndarray: ) +def _stereographic_to_normalized(sx: np.ndarray, sy: np.ndarray) -> np.ndarray: + """Convert stereographic coordinates to normalized pinhole coordinates. + + Inverts the normalized_to_stereographic mapping. + + Args: + sx: Stereographic x coordinates, shape ``(N,)``. + sy: Stereographic y coordinates, shape ``(N,)``. + + Returns: + Normalized coordinates, shape ``(N, 2)``. + """ + r_s = np.sqrt(sx * sx + sy * sy + 1e-30) + theta = 2.0 * np.arctan(r_s / 2.0) + r_n = np.tan(theta) + scale = r_n / r_s + return np.column_stack([sx * scale, sy * scale]) + + +def _compute_seed_grid( + camera_model: OpenCV | PinholeSplined, +) -> tuple[np.ndarray, np.ndarray, int, int]: + """Build a seed correspondence grid for seeded normalization. + + Creates a regular grid in stereographic space, converts to normalized + coordinates, and forward-projects through the camera model to get pixel + locations. Returns (pixel, normalized) correspondences suitable for + building an initial-guess spatial index. + + Args: + camera_model: Camera model with ``project_points`` and FOV properties. + + Returns: + Tuple of (seed_pixels, seed_normals, seed_w, seed_h) where pixels and + normals have shape ``(seed_w * seed_h, 2)``. + """ + fov_x_rad = camera_model.fov_deg_x * math.pi / 180.0 + fov_y_rad = camera_model.fov_deg_y * math.pi / 180.0 + half_x = 2.0 * math.tan(fov_x_rad / 4.0) + half_y = 2.0 * math.tan(fov_y_rad / 4.0) + + # No overscan -- the FOV-based range covers the image exactly. + # Overscanning risks entering fold-over regions where the distortion + # model maps multiple rays to the same pixel. + + # Seed grid: ~1 seed per 4 image pixels along the long axis + w = camera_model.image_width + h = camera_model.image_height + aspect = half_x / half_y if half_y > 0 else 1.0 + seed_long = max(w, h) // 4 + if aspect >= 1.0: + seed_w = seed_long + seed_h = max(4, int(round(seed_long / aspect))) + else: + seed_h = seed_long + seed_w = max(4, int(round(seed_long * aspect))) + + sx = np.linspace(-half_x, half_x, seed_w, dtype=np.float64) + sy = np.linspace(-half_y, half_y, seed_h, dtype=np.float64) + gsx, gsy = np.meshgrid(sx, sy, indexing="xy") + flat_sx = gsx.ravel() + flat_sy = gsy.ravel() + + normals_xy = _stereographic_to_normalized(flat_sx, flat_sy) + rays_3d = np.column_stack([normals_xy, np.ones(len(normals_xy), dtype=np.float64)]) + seed_pixels = camera_model.project_points(rays_3d) + + # Discard seed points that project outside the image. Points beyond + # the valid FOV can fold back into the image under heavy distortion, + # but the stereographic margin is tight enough that these are rare + # and the Newton solver handles them via the pinhole fallback guess. + w = camera_model.image_width + h = camera_model.image_height + outside = ( + (seed_pixels[:, 0] < -0.5) + | (seed_pixels[:, 0] > w - 0.5) + | (seed_pixels[:, 1] < -0.5) + | (seed_pixels[:, 1] > h - 0.5) + | ~np.isfinite(seed_pixels[:, 0]) + ) + seed_pixels[outside] = np.nan + normals_xy[outside] = np.nan + + seed_normals = np.ascontiguousarray(normals_xy, dtype=np.float64) + seed_pixels = np.ascontiguousarray(seed_pixels, dtype=np.float64) + + return seed_pixels, seed_normals, seed_w, seed_h + + +def _sample_xy_grid_seeded( + camera_model: CameraModel, + *, + x_coords: np.ndarray, + y_coords: np.ndarray, + payload_dtype: np.dtype, +) -> np.ndarray: + """Sample the unproject grid using seeded C++ normalization. + + Builds a seed correspondence grid from a stereographic-space sampling, + then dispatches to C++ for spatial-index lookup, bilinear initial guess, + and Newton refinement. + + Args: + camera_model: Camera model to sample. + x_coords: Grid x pixel coordinates, shape ``(grid_width,)``. + y_coords: Grid y pixel coordinates, shape ``(grid_height,)``. + payload_dtype: Storage dtype for quantization simulation. + + Returns: + Sampled xy grid, shape ``(grid_height, grid_width, 2)``. + """ + from lensboy import lensboy_bindings as lbb + + grid_width = len(x_coords) + grid_height = len(y_coords) + + # Build query pixel grid + gx, gy = np.meshgrid(x_coords, y_coords, indexing="xy") + query_pixels = np.ascontiguousarray( + np.column_stack([gx.ravel(), gy.ravel()]), dtype=np.float64 + ) + + seed_pixels, seed_normals, seed_w, seed_h = _compute_seed_grid(camera_model) # type: ignore[arg-type] + + from lensboy.camera_models.opencv import OpenCV + from lensboy.camera_models.pinhole_splined import PinholeSplined + + if isinstance(camera_model, OpenCV): + dist = np.asarray(camera_model.distortion_coeffs, dtype=np.float64) + if len(dist) < 14: + dist = np.pad(dist, (0, 14 - len(dist))) + intrinsics = np.concatenate( + [ + np.array( + [camera_model.fx, camera_model.fy, camera_model.cx, camera_model.cy], + dtype=np.float64, + ), + dist[:14], + ] + ) + rays = lbb.seeded_normalize_opencv( + seed_pixels, seed_normals, seed_w, seed_h, query_pixels, intrinsics + ) + elif isinstance(camera_model, PinholeSplined): + rays = lbb.seeded_normalize_splined( + seed_pixels, + seed_normals, + seed_w, + seed_h, + query_pixels, + camera_model._cpp_config(), + camera_model._cpp_params(), + ) + else: + raise TypeError( + f"Seeded normalization not supported for {type(camera_model).__name__}. " + "Use the standard sampling path instead." + ) + + xy = np.asarray(rays[:, :2], dtype=np.float64) + if payload_dtype != np.dtype(np.float64): + xy = np.asarray(xy, dtype=payload_dtype).astype(np.float64) + + return xy.reshape(grid_height, grid_width, 2) + + +def _normalize_pixel_stride( + pixel_stride: float | tuple[float, float], +) -> tuple[float, float]: + if isinstance(pixel_stride, tuple): + stride_x = float(pixel_stride[0]) + stride_y = float(pixel_stride[1]) + else: + stride_x = float(pixel_stride) + stride_y = float(pixel_stride) + if stride_x <= 0.0 or stride_y <= 0.0: + raise ValueError("pixel_stride values must be positive.") + return stride_x, stride_y + + +def _grid_size_from_stride(image_size: int, stride: float) -> int: + if image_size <= 1: + return 1 + return int(math.ceil((image_size - 1) / stride)) + 1 + + @dataclass class UnprojectLUT: """Regular-grid cache of `normalize_points()` values. @@ -391,9 +579,8 @@ def _compute_grid_scale(size: int, minimum: float, maximum: float) -> float: def _source_model_spec_json(self) -> str | None: return _serialize_source_model_spec(self.source_model_spec) - @classmethod + @staticmethod def from_camera_model( - cls, camera_model: CameraModel, *, grid_size_wh: tuple[int, int] | None = None, @@ -425,13 +612,9 @@ def from_camera_model( grid_width = camera_model.image_width grid_height = camera_model.image_height else: - stride_x, stride_y = cls._normalize_pixel_stride(pixel_stride) - grid_width = cls._grid_size_from_stride( - camera_model.image_width, stride_x - ) - grid_height = cls._grid_size_from_stride( - camera_model.image_height, stride_y - ) + stride_x, stride_y = _normalize_pixel_stride(pixel_stride) + grid_width = _grid_size_from_stride(camera_model.image_width, stride_x) + grid_height = _grid_size_from_stride(camera_model.image_height, stride_y) else: grid_width, grid_height = int(grid_size_wh[0]), int(grid_size_wh[1]) @@ -445,15 +628,15 @@ def from_camera_model( 0.0, float(camera_model.image_height - 1), grid_height, dtype=np.float64 ) payload_dtype = _SUPPORTED_ENCODINGS[storage_encoding] - xy_grid = cls._sample_xy_grid( + + xy_grid = _sample_xy_grid_seeded( camera_model, x_coords=x_coords, y_coords=y_coords, payload_dtype=payload_dtype, - num_workers=resolved_num_workers, ) - lut = cls( + lut = UnprojectLUT( image_width=camera_model.image_width, image_height=camera_model.image_height, grid_width=grid_width, @@ -469,26 +652,6 @@ def from_camera_model( ) return lut - @staticmethod - def _normalize_pixel_stride( - pixel_stride: float | tuple[float, float], - ) -> tuple[float, float]: - if isinstance(pixel_stride, tuple): - stride_x = float(pixel_stride[0]) - stride_y = float(pixel_stride[1]) - else: - stride_x = float(pixel_stride) - stride_y = float(pixel_stride) - if stride_x <= 0.0 or stride_y <= 0.0: - raise ValueError("pixel_stride values must be positive.") - return stride_x, stride_y - - @staticmethod - def _grid_size_from_stride(image_size: int, stride: float) -> int: - if image_size <= 1: - return 1 - return int(math.ceil((image_size - 1) / stride)) + 1 - @staticmethod def _sample_xy_grid( camera_model: CameraModel, @@ -827,8 +990,10 @@ def build_lines(payload_offset_bytes_text: str) -> list[str]: f"lensboy_version: {self.lensboy_version}", f"source_model_type: {_format_optional_str(self.source_model_type)}", f"source_model_spec_json_sha256: {source_model_spec_json_sha256}", - f"image_size_wh: {_format_csv([str(self.image_width), str(self.image_height)])}", - f"grid_size_wh: {_format_csv([str(self.grid_width), str(self.grid_height)])}", + "image_size_wh: " + + _format_csv([str(self.image_width), str(self.image_height)]), + "grid_size_wh: " + + _format_csv([str(self.grid_width), str(self.grid_height)]), "grid_extents_xy: " + _format_csv( [ diff --git a/src/lensboy/lensboy_bindings.pyi b/src/lensboy/lensboy_bindings.pyi index 0507939..6eca639 100644 --- a/src/lensboy/lensboy_bindings.pyi +++ b/src/lensboy/lensboy_bindings.pyi @@ -6,7 +6,7 @@ import collections.abc import numpy import numpy.typing import typing -__all__: list[str] = ['PinholeSplinedConfig', 'PinholeSplinedIntrinsicsParameters', 'WarpCoordinates', 'add', 'calibrate_opencv', 'fine_tune_pinhole_splined', 'get_matching_spline_distortion_model', 'make_undistortion_maps_pinhole_splined', 'normalize_pinhole_splined_points', 'project_pinhole_splined_points', 'set_log_level', 'warp_target_points'] +__all__: list[str] = ['PinholeSplinedConfig', 'PinholeSplinedIntrinsicsParameters', 'WarpCoordinates', 'add', 'calibrate_opencv', 'fine_tune_pinhole_splined', 'get_matching_spline_distortion_model', 'make_undistortion_maps_pinhole_splined', 'normalize_pinhole_splined_points', 'project_pinhole_splined_points', 'seeded_normalize_opencv', 'seeded_normalize_splined', 'set_log_level', 'warp_target_points'] class PinholeSplinedConfig: def __init__(self, image_width: typing.SupportsInt, image_height: typing.SupportsInt, fov_deg_x: typing.SupportsFloat, fov_deg_y: typing.SupportsFloat, num_knots_x: typing.SupportsInt, num_knots_y: typing.SupportsInt, smoothness_lambda: typing.SupportsFloat) -> None: ... @@ -116,6 +116,10 @@ def normalize_pinhole_splined_points(model_config: PinholeSplinedConfig, intrins ... def project_pinhole_splined_points(model_config: PinholeSplinedConfig, intrinsics: PinholeSplinedIntrinsicsParameters, points_in_camera: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]) -> numpy.typing.NDArray[numpy.float64]: ... +def seeded_normalize_opencv(seed_pixels: typing.Annotated[numpy.typing.ArrayLike, numpy.float64], seed_normals: typing.Annotated[numpy.typing.ArrayLike, numpy.float64], seed_w: typing.SupportsInt, seed_h: typing.SupportsInt, query_pixels: typing.Annotated[numpy.typing.ArrayLike, numpy.float64], intrinsics: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]) -> numpy.typing.NDArray[numpy.float64]: + ... +def seeded_normalize_splined(seed_pixels: typing.Annotated[numpy.typing.ArrayLike, numpy.float64], seed_normals: typing.Annotated[numpy.typing.ArrayLike, numpy.float64], seed_w: typing.SupportsInt, seed_h: typing.SupportsInt, query_pixels: typing.Annotated[numpy.typing.ArrayLike, numpy.float64], config: PinholeSplinedConfig, intrinsics: PinholeSplinedIntrinsicsParameters) -> numpy.typing.NDArray[numpy.float64]: + ... def set_log_level(level: str) -> None: ... def warp_target_points(warp_coordinates: WarpCoordinates, warp_coeffs: typing.Annotated[numpy.typing.ArrayLike, numpy.float64], target_points: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]) -> numpy.typing.NDArray[numpy.float64]: diff --git a/tests/test_unproject_lut.py b/tests/test_unproject_lut.py index 209dac8..bfa5b70 100644 --- a/tests/test_unproject_lut.py +++ b/tests/test_unproject_lut.py @@ -268,6 +268,7 @@ def test_linear_pinhole_lut_encodings(tmp_path: Path) -> None: loaded = lb.UnprojectLUT.load(file_path) approx = loaded.normalize_points(sample_pixels, interpolation="bilinear") + assert isinstance(approx, np.ndarray) max_abs_errors[encoding] = float(np.max(np.abs(approx[:, :2] - expected[:, :2]))) assert max_abs_errors["float64_xy"] < 1e-12 @@ -434,7 +435,7 @@ def test_unproject_lut_analyzer_is_stable_across_num_workers() -> None: def test_unproject_lut_grid_stride_can_be_fractional() -> None: - """The stored sample spacing reflects the actual grid spacing and may be fractional.""" + """The stored sample spacing reflects the actual grid spacing.""" model = _make_linear_pinhole_model() lut = model.get_unproject_lut(grid_size_wh=(6, 5)) @@ -545,6 +546,7 @@ def test_unproject_lut_analyzer_report_is_finite_for_nonlinear_model() -> None: pixels = np.column_stack([grid_x.ravel(), grid_y.ravel()]) exact = model.normalize_points(pixels) approx = lut.normalize_points(pixels, interpolation="bilinear") + assert isinstance(approx, np.ndarray) dense_error = _query_error_deg(exact, approx) assert dense_error <= (report.max_angular_error_mdeg["bilinear"] / 1.0e3) + 1.0 @@ -697,7 +699,7 @@ def test_unproject_lut_rejects_nonfinite_payload_values( def test_unproject_lut_analyzer_can_save_and_load_error_heatmaps(tmp_path: Path) -> None: - """Analyzer-generated heatmaps can be saved and loaded without using the LUT header.""" + """Analyzer heatmaps can be saved and loaded without the LUT header.""" from lensboy.analysis import UnprojectLUTAnalyzer, UnprojectLUTErrorHeatmap model = _make_linear_pinhole_model() @@ -738,7 +740,9 @@ def test_plot_unproject_lut_error_heatmap_supports_angular_units(tmp_path: Path) return_figure=True, ) assert fig is not None - np.testing.assert_allclose(fig.axes[0].images[0].get_array(), expected_mdeg) + actual = fig.axes[0].images[0].get_array() + assert actual is not None + np.testing.assert_allclose(actual, expected_mdeg) assert ( fig.axes[0].get_title() == "Per-cell max error heatmap (bilinear) [milli degrees]" ) From 4c6f87359883e2e99ed1d49ce60221aa5083b3d3 Mon Sep 17 00:00:00 2001 From: Robertleoj Date: Sun, 12 Apr 2026 16:57:49 +0200 Subject: [PATCH 03/27] wip --- README.md | 7 +- cpp_runtime/unproject_lut.cpp | 21 - cpp_runtime/unproject_lut.hpp | 3 - cpp_src/seeded_normalize.cpp | 228 ++++--- cpp_src/seeded_normalize.hpp | 12 +- docs/unproject_lut.md | 21 +- examples/unproject_lut_mo.py | 82 +-- src/lensboy/analysis/__init__.py | 8 +- src/lensboy/analysis/unproject_lut.py | 604 +++++++------------ src/lensboy/camera_models/base_model.py | 39 -- src/lensboy/camera_models/opencv.py | 31 + src/lensboy/camera_models/pinhole_splined.py | 31 + src/lensboy/camera_models/unproject_lut.py | 227 +------ tests/test_unproject_lut.py | 218 ++----- 14 files changed, 537 insertions(+), 995 deletions(-) diff --git a/README.md b/README.md index 3d8f158..87be049 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ payload: ```python import lensboy as lb -from lensboy.analysis import UnprojectLUTAnalyzer +from lensboy.analysis import estimate_lut_accuracy, compute_lut_error_heatmap model = lb.OpenCV.load("camera.json") lut = model.get_unproject_lut( @@ -113,9 +113,8 @@ rays = runtime_lut.normalize_points( bounds="strict", ) -analyzer = UnprojectLUTAnalyzer(runtime_lut) -report = analyzer.estimate_accuracy(interpolations="bilinear") -heatmap = analyzer.compute_error_heatmap(interpolation="bilinear") +report = estimate_lut_accuracy(runtime_lut, model, interpolations="bilinear") +heatmap = compute_lut_error_heatmap(runtime_lut, model, interpolation="bilinear") ``` See the [unproject LUT guide](https://robertleoj.github.io/lensboy/unproject_lut.html) diff --git a/cpp_runtime/unproject_lut.cpp b/cpp_runtime/unproject_lut.cpp index 65a333d..c1d659a 100644 --- a/cpp_runtime/unproject_lut.cpp +++ b/cpp_runtime/unproject_lut.cpp @@ -462,12 +462,6 @@ UnprojectLUT UnprojectLUT::load( std::string const default_interpolation = require_field("default_interpolation"); std::string const default_bounds = require_field("default_bounds"); - std::string const source_model_type = - parse_optional_string(require_field("source_model_type")); - std::string const source_model_spec_json = - parse_optional_string(require_field("source_model_spec_json")); - std::string const source_model_spec_json_sha256 = - parse_optional_string(require_field("source_model_spec_json_sha256")); std::string const lensboy_version = require_field("lensboy_version"); std::vector string_storage; @@ -475,9 +469,6 @@ UnprojectLUT UnprojectLUT::load( storage_encoding.size() + default_interpolation.size() + default_bounds.size() + - source_model_type.size() + - source_model_spec_json.size() + - source_model_spec_json_sha256.size() + lensboy_version.size() + 8 ); @@ -502,18 +493,6 @@ UnprojectLUT UnprojectLUT::load( string_storage, default_bounds ); - metadata.source_model_type = append_string_view( - string_storage, - source_model_type - ); - metadata.source_model_spec_json = append_string_view( - string_storage, - source_model_spec_json - ); - metadata.source_model_spec_json_sha256 = append_string_view( - string_storage, - source_model_spec_json_sha256 - ); metadata.lensboy_version = append_string_view( string_storage, lensboy_version diff --git a/cpp_runtime/unproject_lut.hpp b/cpp_runtime/unproject_lut.hpp index 8c88d5b..1c33167 100644 --- a/cpp_runtime/unproject_lut.hpp +++ b/cpp_runtime/unproject_lut.hpp @@ -37,9 +37,6 @@ struct UnprojectLUTMetadata { std::string_view storage_encoding; std::string_view default_interpolation; std::string_view default_bounds; - std::string_view source_model_type; - std::string_view source_model_spec_json; - std::string_view source_model_spec_json_sha256; std::string_view lensboy_version; std::size_t payload_offset_bytes = 0; }; diff --git a/cpp_src/seeded_normalize.cpp b/cpp_src/seeded_normalize.cpp index f63cb44..06d5d16 100644 --- a/cpp_src/seeded_normalize.cpp +++ b/cpp_src/seeded_normalize.cpp @@ -4,10 +4,8 @@ #include #include -#include #include #include -#include #include #include @@ -45,36 +43,50 @@ class SeedGrid { for (int i = 0; i < seed_w; i++) { int idx = j * seed_w + i; double x0 = pixel_xy[idx * 2], y0 = pixel_xy[idx * 2 + 1]; - if (!std::isfinite(x0)) continue; + if (!std::isfinite(x0)) { + continue; + } if (i + 1 < seed_w) { int idx1 = j * seed_w + (i + 1); double x1 = pixel_xy[idx1 * 2], y1 = pixel_xy[idx1 * 2 + 1]; if (std::isfinite(x1)) { - double d = (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0); - if (d > 0) min_edge_sq = std::min(min_edge_sq, d); + double d = + (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0); + if (d > 0) { + min_edge_sq = std::min(min_edge_sq, d); + } } } if (j + 1 < seed_h) { int idx1 = (j + 1) * seed_w + i; double x1 = pixel_xy[idx1 * 2], y1 = pixel_xy[idx1 * 2 + 1]; if (std::isfinite(x1)) { - double d = (x1-x0)*(x1-x0) + (y1-y0)*(y1-y0); - if (d > 0) min_edge_sq = std::min(min_edge_sq, d); + double d = + (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0); + if (d > 0) { + min_edge_sq = std::min(min_edge_sq, d); + } } } } } cell_size_ = std::sqrt(min_edge_sq); - if (cell_size_ < 1e-6) cell_size_ = 1.0; + if (cell_size_ < 1e-6) { + cell_size_ = 1.0; + } inv_cell_ = 1.0 / cell_size_; // Bounding box double xmin = 1e30, xmax = -1e30, ymin = 1e30, ymax = -1e30; for (int i = 0; i < n; i++) { double x = pixel_xy[i * 2], y = pixel_xy[i * 2 + 1]; - if (!std::isfinite(x)) continue; - xmin = std::min(xmin, x); xmax = std::max(xmax, x); - ymin = std::min(ymin, y); ymax = std::max(ymax, y); + if (!std::isfinite(x)) { + continue; + } + xmin = std::min(xmin, x); + xmax = std::max(xmax, x); + ymin = std::min(ymin, y); + ymax = std::max(ymax, y); } x_min_ = xmin; y_min_ = ymin; @@ -88,7 +100,9 @@ class SeedGrid { for (int i = 0; i < n; i++) { double x = pixel_xy[i * 2], y = pixel_xy[i * 2 + 1]; - if (!std::isfinite(x) || !std::isfinite(y)) continue; + if (!std::isfinite(x) || !std::isfinite(y)) { + continue; + } int cx = to_cx(x), cy = to_cy(y); int base = (cy * gw_ + cx) * CELL_CAP; for (int s = 0; s < CELL_CAP; s++) { @@ -100,25 +114,38 @@ class SeedGrid { } } - int nearest(double qx, double qy, const double* pixel_xy) const { + int nearest( + double qx, + double qy, + const double* pixel_xy + ) const { constexpr int CELL_CAP = 4; int cx = to_cx(qx), cy = to_cy(qy); int best = -1; double best_sq = std::numeric_limits::max(); for (int dy = -1; dy <= 1; dy++) { int ry = cy + dy; - if (ry < 0 || ry >= gh_) continue; + if (ry < 0 || ry >= gh_) { + continue; + } for (int dx = -1; dx <= 1; dx++) { int rx = cx + dx; - if (rx < 0 || rx >= gw_) continue; + if (rx < 0 || rx >= gw_) { + continue; + } int base = (ry * gw_ + rx) * CELL_CAP; for (int s = 0; s < CELL_CAP; s++) { int idx = cells_[base + s]; - if (idx < 0) break; + if (idx < 0) { + break; + } double ex = qx - pixel_xy[idx * 2]; double ey = qy - pixel_xy[idx * 2 + 1]; double d = ex * ex + ey * ey; - if (d < best_sq) { best_sq = d; best = idx; } + if (d < best_sq) { + best_sq = d; + best = idx; + } } } } @@ -130,11 +157,21 @@ class SeedGrid { int gw_, gh_; std::vector cells_; - int to_cx(double x) const { - return std::max(0, std::min(gw_ - 1, (int)std::floor((x - x_min_) * inv_cell_))); + int to_cx( + double x + ) const { + return std::max( + 0, + std::min(gw_ - 1, (int)std::floor((x - x_min_) * inv_cell_)) + ); } - int to_cy(double y) const { - return std::max(0, std::min(gh_ - 1, (int)std::floor((y - y_min_) * inv_cell_))); + int to_cy( + double y + ) const { + return std::max( + 0, + std::min(gh_ - 1, (int)std::floor((y - y_min_) * inv_cell_)) + ); } }; @@ -178,7 +215,9 @@ static bool idw_interp_from_nn( // Check all 4 corners are valid for (int c = 0; c < 4; c++) { - if (!std::isfinite(seed_pixels[idx[c] * 2])) return false; + if (!std::isfinite(seed_pixels[idx[c] * 2])) { + return false; + } } // Approximate bilinear (u,v) by projecting onto the quad edges. @@ -190,7 +229,9 @@ static bool idw_interp_from_nn( double qx = px - p00[0], qy = py - p00[1]; double det = ex * fy - fx * ey; - if (std::abs(det) < 1e-30) return false; + if (std::abs(det) < 1e-30) { + return false; + } double inv = 1.0 / det; double u = std::max(0.0, std::min(1.0, (qx * fy - fx * qy) * inv)); double v = std::max(0.0, std::min(1.0, (ex * qy - qx * ey) * inv)); @@ -200,8 +241,10 @@ static bool idw_interp_from_nn( const double* n10 = &seed_normals[idx[1] * 2]; const double* n01 = &seed_normals[idx[2] * 2]; const double* n11 = &seed_normals[idx[3] * 2]; - nx_out = mu * mv * n00[0] + u * mv * n10[0] + mu * v * n01[0] + u * v * n11[0]; - ny_out = mu * mv * n00[1] + u * mv * n10[1] + mu * v * n01[1] + u * v * n11[1]; + nx_out = + mu * mv * n00[0] + u * mv * n10[0] + mu * v * n01[0] + u * v * n11[0]; + ny_out = + mu * mv * n00[1] + u * mv * n10[1] + mu * v * n01[1] + u * v * n11[1]; return true; } @@ -239,7 +282,14 @@ static inline void forward_splined( ) { Vec3 point(nx, ny, T(1)); Vec2 result; - project_pinhole_splined(config, pinhole_params, dx_grid, dy_grid, point, result); + project_pinhole_splined( + config, + pinhole_params, + dx_grid, + dy_grid, + point, + result + ); px = result[0]; py = result[1]; } @@ -270,12 +320,16 @@ static void refine_opencv( double r0 = jpx.a - target_u; double r1 = jpy.a - target_v; - if (r0 * r0 + r1 * r1 < tol_sq) break; + if (r0 * r0 + r1 * r1 < tol_sq) { + break; + } double J00 = jpx.v[0], J01 = jpx.v[1]; double J10 = jpy.v[0], J11 = jpy.v[1]; double det = J00 * J11 - J01 * J10; - if (std::abs(det) < 1e-30) break; + if (std::abs(det) < 1e-30) { + break; + } double inv = 1.0 / det; nx -= inv * (J11 * r0 - J01 * r1); ny -= inv * (-J10 * r0 + J00 * r1); @@ -337,10 +391,12 @@ static void refine_splined( double sx, sy; normalized_to_stereographic(nx, ny, sx, sy); double gx = std::max( - 0.0, std::min(1.0 + (sx + half_x) * x_scale, Nx - 1.0 - eps) + 0.0, + std::min(1.0 + (sx + half_x) * x_scale, Nx - 1.0 - eps) ); double gy = std::max( - 0.0, std::min(1.0 + (sy + half_y) * y_scale, Ny - 1.0 - eps) + 0.0, + std::min(1.0 + (sy + half_y) * y_scale, Ny - 1.0 - eps) ); const int ix0 = (int)std::floor(gx); const int iy0 = (int)std::floor(gy); @@ -388,18 +444,18 @@ static void refine_splined( ki++; } } - Jet r0 = - Jet(sc.fx) * (jnx + dx_val) + - Jet(sc.cx) - Jet(target_u); - Jet r1 = - Jet(sc.fy) * (jny + dy_val) + - Jet(sc.cy) - Jet(target_v); + Jet r0 = Jet(sc.fx) * (jnx + dx_val) + Jet(sc.cx) - Jet(target_u); + Jet r1 = Jet(sc.fy) * (jny + dy_val) + Jet(sc.cy) - Jet(target_v); double res0 = r0.a, res1 = r1.a; - if (res0 * res0 + res1 * res1 < tol_sq) break; + if (res0 * res0 + res1 * res1 < tol_sq) { + break; + } double J00 = r0.v[0], J01 = r0.v[1]; double J10 = r1.v[0], J11 = r1.v[1]; double det = J00 * J11 - J01 * J10; - if (std::abs(det) < 1e-30) break; + if (std::abs(det) < 1e-30) { + break; + } double inv = 1.0 / det; nx -= inv * (J11 * res0 - J01 * res1); ny -= inv * (-J10 * res0 + J00 * res1); @@ -407,15 +463,23 @@ static void refine_splined( normalized_to_stereographic(nx, ny, sx, sy); gx = std::max( - 0.0, std::min(1.0 + (sx + half_x) * x_scale, Nx - 1.0 - eps) + 0.0, + std::min(1.0 + (sx + half_x) * x_scale, Nx - 1.0 - eps) ); gy = std::max( - 0.0, std::min(1.0 + (sy + half_y) * y_scale, Ny - 1.0 - eps) + 0.0, + std::min(1.0 + (sy + half_y) * y_scale, Ny - 1.0 - eps) ); - if ((int)std::floor(gx) == ix0 && (int)std::floor(gy) == iy0) break; + if ((int)std::floor(gx) == ix0 && (int)std::floor(gy) == iy0) { + break; + } + } + if (out_rebuilds) { + *out_rebuilds = rebuild_count; + } + if (out_iters) { + *out_iters = iter_count; } - if (out_rebuilds) *out_rebuilds = rebuild_count; - if (out_iters) *out_iters = iter_count; } // --------------------------------------------------------------------------- @@ -424,12 +488,10 @@ static void refine_splined( py::array_t seeded_normalize_opencv( py::array_t seed_pixels, - py::array_t - seed_normals, + py::array_t seed_normals, int seed_w, int seed_h, - py::array_t - query_pixels, + py::array_t query_pixels, py::array_t intrinsics ) { auto sp = seed_pixels.request(); @@ -438,9 +500,7 @@ py::array_t seeded_normalize_opencv( auto ip = intrinsics.request(); require(sp.ndim == 2 && sp.shape[1] == 2, "seed_pixels must be (M, 2)"); - require( - sn.ndim == 2 && sn.shape[1] == 2, "seed_normals must be (M, 2)" - ); + require(sn.ndim == 2 && sn.shape[1] == 2, "seed_normals must be (M, 2)"); require( sp.shape[0] == sn.shape[0], "seed_pixels and seed_normals must have same length" @@ -449,9 +509,7 @@ py::array_t seeded_normalize_opencv( sp.shape[0] == (ssize_t)(seed_w * seed_h), "seed length must equal seed_w * seed_h" ); - require( - qp.ndim == 2 && qp.shape[1] == 2, "query_pixels must be (N, 2)" - ); + require(qp.ndim == 2 && qp.shape[1] == 2, "query_pixels must be (N, 2)"); require( ip.ndim == 1 && ip.shape[0] == 18, "intrinsics must be (18,): fx, fy, cx, cy, dist[14]" @@ -469,15 +527,11 @@ py::array_t seeded_normalize_opencv( py::array_t out({N, (ssize_t)3}); double* O = static_cast(out.request().ptr); - auto t0 = std::chrono::high_resolution_clock::now(); SeedGrid grid(SP, M, seed_w, seed_h); - auto t1 = std::chrono::high_resolution_clock::now(); py::gil_scoped_release release; - auto t_loop = std::chrono::high_resolution_clock::now(); - - #pragma omp parallel for schedule(static) +#pragma omp parallel for schedule(static) for (ssize_t i = 0; i < N; i++) { double px = QP[i * 2]; double py = QP[i * 2 + 1]; @@ -489,8 +543,16 @@ py::array_t seeded_normalize_opencv( if (nearest >= 0) { double interp_nx, interp_ny; if (idw_interp_from_nn( - px, py, nearest, seed_w, seed_h, SP, SN, - interp_nx, interp_ny)) { + px, + py, + nearest, + seed_w, + seed_h, + SP, + SN, + interp_nx, + interp_ny + )) { nx = interp_nx; ny = interp_ny; } else { @@ -506,24 +568,15 @@ py::array_t seeded_normalize_opencv( O[i * 3 + 2] = 1.0; } - auto t2 = std::chrono::high_resolution_clock::now(); - double grid_ms = std::chrono::duration_cast(t1 - t0).count() / 1000.0; - double loop_ms = std::chrono::duration_cast(t2 - t_loop).count() / 1000.0; - std::fprintf(stderr, - "[opencv] grid=%.1fms loop=%.1fms N=%lld\n", - grid_ms, loop_ms, (long long)N); - return out; } py::array_t seeded_normalize_splined( py::array_t seed_pixels, - py::array_t - seed_normals, + py::array_t seed_normals, int seed_w, int seed_h, - py::array_t - query_pixels, + py::array_t query_pixels, PinholeSplinedConfig& config, PinholeSplinedIntrinsicsParameters& params ) { @@ -532,9 +585,7 @@ py::array_t seeded_normalize_splined( auto qp = query_pixels.request(); require(sp.ndim == 2 && sp.shape[1] == 2, "seed_pixels must be (M, 2)"); - require( - sn.ndim == 2 && sn.shape[1] == 2, "seed_normals must be (M, 2)" - ); + require(sn.ndim == 2 && sn.shape[1] == 2, "seed_normals must be (M, 2)"); require( sp.shape[0] == sn.shape[0], "seed_pixels and seed_normals must have same length" @@ -543,9 +594,7 @@ py::array_t seeded_normalize_splined( sp.shape[0] == (ssize_t)(seed_w * seed_h), "seed length must equal seed_w * seed_h" ); - require( - qp.ndim == 2 && qp.shape[1] == 2, "query_pixels must be (N, 2)" - ); + require(qp.ndim == 2 && qp.shape[1] == 2, "query_pixels must be (N, 2)"); auto dxb = params.dx_grid.request(); auto dyb = params.dy_grid.request(); @@ -584,15 +633,11 @@ py::array_t seeded_normalize_splined( SplineConstants sc(&config, pinhole_params); - auto t0 = std::chrono::high_resolution_clock::now(); SeedGrid grid(SP, M, seed_w, seed_h); - auto t1 = std::chrono::high_resolution_clock::now(); py::gil_scoped_release release; - auto t_loop = std::chrono::high_resolution_clock::now(); - - #pragma omp parallel for schedule(static) +#pragma omp parallel for schedule(static) for (ssize_t i = 0; i < N; i++) { double px = QP[i * 2]; double py_val = QP[i * 2 + 1]; @@ -604,8 +649,16 @@ py::array_t seeded_normalize_splined( if (nearest >= 0) { double interp_nx, interp_ny; if (idw_interp_from_nn( - px, py_val, nearest, seed_w, seed_h, SP, SN, - interp_nx, interp_ny)) { + px, + py_val, + nearest, + seed_w, + seed_h, + SP, + SN, + interp_nx, + interp_ny + )) { nx = interp_nx; ny = interp_ny; } else { @@ -621,13 +674,6 @@ py::array_t seeded_normalize_splined( O[i * 3 + 2] = 1.0; } - auto t2 = std::chrono::high_resolution_clock::now(); - double grid_ms = std::chrono::duration_cast(t1 - t0).count() / 1000.0; - double loop_ms = std::chrono::duration_cast(t2 - t_loop).count() / 1000.0; - std::fprintf(stderr, - "[spline] grid=%.1fms loop=%.1fms N=%lld\n", - grid_ms, loop_ms, (long long)N); - return out; } diff --git a/cpp_src/seeded_normalize.hpp b/cpp_src/seeded_normalize.hpp index 9b813b0..13f2d12 100644 --- a/cpp_src/seeded_normalize.hpp +++ b/cpp_src/seeded_normalize.hpp @@ -10,23 +10,19 @@ namespace py = pybind11; py::array_t seeded_normalize_opencv( py::array_t seed_pixels, - py::array_t - seed_normals, + py::array_t seed_normals, int seed_w, int seed_h, - py::array_t - query_pixels, + py::array_t query_pixels, py::array_t intrinsics ); py::array_t seeded_normalize_splined( py::array_t seed_pixels, - py::array_t - seed_normals, + py::array_t seed_normals, int seed_w, int seed_h, - py::array_t - query_pixels, + py::array_t query_pixels, PinholeSplinedConfig& config, PinholeSplinedIntrinsicsParameters& params ); diff --git a/docs/unproject_lut.md b/docs/unproject_lut.md index bd694d0..e549e8d 100644 --- a/docs/unproject_lut.md +++ b/docs/unproject_lut.md @@ -5,7 +5,7 @@ The `.unproject_LUT` file format is intentionally split into two concerns: - `UnprojectLUT` is the runtime object. It handles grid construction, file I/O, and fast lookup. -- `UnprojectLUTAnalyzer` is the analysis object. It reconstructs the exact source model from the embedded model spec and computes accuracy summaries or error heatmaps on demand. +- The analysis functions (`estimate_lut_accuracy`, `compute_lut_error_heatmap`) take the LUT and the original camera model to compute accuracy summaries or error heatmaps. This keeps the runtime file format compact while still making later analysis possible. @@ -61,14 +61,13 @@ Supported bounds modes: ## Analyze accuracy later ```python -from lensboy.analysis import UnprojectLUTAnalyzer +from lensboy.analysis import estimate_lut_accuracy, compute_lut_error_heatmap -analyzer = UnprojectLUTAnalyzer(runtime_lut) - -report = analyzer.estimate_accuracy( +report = estimate_lut_accuracy( + runtime_lut, model, interpolations=("nearest", "bilinear"), ) -heatmap = analyzer.compute_error_heatmap(interpolation="bilinear") +heatmap = compute_lut_error_heatmap(runtime_lut, model, interpolation="bilinear") heatmap.save("camera_bilinear_error_heatmap.npz") ``` @@ -79,8 +78,6 @@ The accuracy report contains: - the interpolation modes that were analyzed - the adaptive estimator settings that produced the result -The analyzer requires `source_model_spec_json` to be present in the LUT. Standard `lensboy` camera models write that field automatically. - ## Plot a heatmap ```python @@ -106,11 +103,9 @@ The header looks like this: ```text format: lensboy_unproject_LUT -payload_offset_bytes: 1306 +payload_offset_bytes: 382 format_version: 1 lensboy_version: 3.0.1 -source_model_type: opencv -source_model_spec_json_sha256: e31fc9944f7ad2a787a38a65331bf873baeeed68fc881f6e237e3b6a5a3e9811 image_size_wh: 3088, 2064 grid_size_wh: 98, 66 grid_extents_xy: 0, 3087, 0, 2063 @@ -120,16 +115,14 @@ default_interpolation: bilinear default_bounds: strict payload_layout: row_major_interleaved_xy payload_endianness: little -source_model_spec_json: {"cx":1514.1042261000703,"cy":1076.8896307961334,"distortion_coeffs":[...],"fx":1354.51232559645,"fy":1354.318019481934,"image_height":2064,"image_width":3088,"lensboy-version":"3.0.1","type":"opencv"} END_HEADER ``` Notes: - `grid_stride_xy` is derived from the extents and grid size, so it may be fractional. -- `source_model_spec_json` is stored in canonical JSON form. - The binary payload is little-endian, row-major, interleaved `x/y`. -- The runtime format does not store accuracy reports. Those are computed separately by `UnprojectLUTAnalyzer`. +- The LUT is a pure runtime artifact -- it does not store the source camera model. ## Standalone C++ runtime diff --git a/examples/unproject_lut_mo.py b/examples/unproject_lut_mo.py index 54c9037..077142c 100755 --- a/examples/unproject_lut_mo.py +++ b/examples/unproject_lut_mo.py @@ -23,9 +23,11 @@ import lensboy as lb from lensboy.analysis import ( - UnprojectLUTAnalyzer, UnprojectLUTErrorHeatmap, + compute_lut_error_heatmap, + estimate_lut_accuracy, plot_unproject_lut_error_heatmap, + sample_lut_accuracy, ) @@ -34,9 +36,14 @@ def _(): mo.md(""" # Unproject LUT (LookUp Table) example - The function which goes from pixel location to 3D ray is called `unproject()`. It is usually not possible to compute in closed form, so it must be iteratively approximated. Unfortunately, this process is slow, which can be a problem for some applications. + The function which goes from pixel location to 3D ray is called + `unproject()`. It is usually not possible to compute in closed form, + so it must be iteratively approximated. Unfortunately, this process + is slow, which can be a problem for some applications. - Lensboy allows you to precompute these values once ahead of time, for fast lookup during operation. The runtime LUT stays compact and fast, while the accuracy analysis is available separately on demand. + Lensboy allows you to precompute these values once ahead of time, + for fast lookup during operation. The runtime LUT stays compact and + fast, while the accuracy analysis is available separately on demand. This notebook builds a `.unproject_LUT` file from the bundled OpenCV test calibration, saves it to `examples/generated/`, reloads it, and compares the @@ -80,7 +87,8 @@ def _(): mo.md(r""" ## Compute the lookup table - Use the drop-down widget below to provide paramters for how the LUT is created and used. + Use the drop-down widget below to provide paramters for how the + LUT is created and used. """) return @@ -112,32 +120,21 @@ def _(): value="float32_xy", label="Storage encoding", ) - num_workers_widget = mo.ui.slider( - start=1, - step=1, - stop=64, - value=8, - label="Number of workers", - show_value=True, - debounce=True, - ) controls_ui = mo.vstack( [ pixel_stride_widget, storage_encoding_widget, - num_workers_widget, ] ) controls_ui - return num_workers_widget, pixel_stride_widget, storage_encoding_widget + return pixel_stride_widget, storage_encoding_widget @app.cell(hide_code=True) -def _(num_workers_widget, pixel_stride_widget, storage_encoding_widget): +def _(pixel_stride_widget, storage_encoding_widget): controls = SimpleNamespace( pixel_stride=float(pixel_stride_widget.value), storage_encoding=storage_encoding_widget.value, - num_workers=num_workers_widget.value, ) return (controls,) @@ -148,7 +145,6 @@ def _(controls, model): lut = model.get_unproject_lut( pixel_stride=controls.pixel_stride, storage_encoding=controls.storage_encoding, - num_workers=controls.num_workers, ) return (lut,) @@ -163,7 +159,10 @@ def _(): @app.cell def _(controls, lut, output_dir): - lut_filename = f"opencv_stride_{int(controls.pixel_stride)}_{controls.storage_encoding}.unproject_LUT" + lut_filename = ( + f"opencv_stride_{int(controls.pixel_stride)}" + f"_{controls.storage_encoding}.unproject_LUT" + ) lut_path = output_dir / lut_filename lut.save(lut_path) @@ -203,9 +202,8 @@ def _(loaded, lut_path, repo_root): @app.cell -def _(loaded): - analyzer = UnprojectLUTAnalyzer(loaded) - return (analyzer,) +def _(): + return @app.cell(hide_code=True) @@ -238,21 +236,27 @@ def _(accuracy_report, interpolation_mode, sample_accuracy): Queried `{sample_accuracy.sample_count}` evenly spaced pixels with `{interpolation_mode}` interpolation. - - sample grid: `{sample_accuracy.sample_grid_width} x {sample_accuracy.sample_grid_height}` - - max observed angular error on this sample: `{sample_accuracy.max_angular_error_mdeg:.3f} milli degrees` - - mean angular error on this sample: `{sample_accuracy.mean_angular_error_mdeg:.3f} milli degrees` - - analyzer-estimated max for `{interpolation_mode}`: `{accuracy_report.max_angular_error_mdeg[interpolation_mode]:.3f} milli degrees` - - analyzer-estimated median for `{interpolation_mode}`: `{accuracy_report.median_angular_error_mdeg[interpolation_mode]:.3f} milli degrees` + - sample grid: `{sample_accuracy.sample_grid_width} x \ +{sample_accuracy.sample_grid_height}` + - max observed angular error on this sample: \ +`{sample_accuracy.max_angular_error_mdeg:.3f} mdeg` + - mean angular error on this sample: \ +`{sample_accuracy.mean_angular_error_mdeg:.3f} mdeg` + - analyzer-estimated max for `{interpolation_mode}`: \ +`{accuracy_report.max_angular_error_mdeg[interpolation_mode]:.3f} mdeg` + - analyzer-estimated median for `{interpolation_mode}`: \ +`{accuracy_report.median_angular_error_mdeg[interpolation_mode]:.3f} mdeg` """) return @app.cell -def _(analyzer, interpolation_mode): - accuracy_report = analyzer.estimate_accuracy(interpolations=interpolation_mode) - sample_accuracy = analyzer.sample_accuracy_grid( - interpolation=interpolation_mode, - target_sample_count=2500, +def _(loaded, model, interpolation_mode): + accuracy_report = estimate_lut_accuracy( + loaded, model, interpolations=interpolation_mode + ) + sample_accuracy = sample_lut_accuracy( + loaded, model, interpolation=interpolation_mode, target_sample_count=2500 ) return accuracy_report, sample_accuracy @@ -273,11 +277,15 @@ def _(heatmap_path, interpolation_mode): @app.cell -def _(analyzer, controls, interpolation_mode, output_dir): - heatmap_filename = f"opencv_stride_{int(controls.pixel_stride)}_{controls.storage_encoding}_{interpolation_mode}_error_heatmap.npz" +def _(loaded, model, controls, interpolation_mode, output_dir): + heatmap_filename = ( + f"opencv_stride_{int(controls.pixel_stride)}" + f"_{controls.storage_encoding}" + f"_{interpolation_mode}_error_heatmap.npz" + ) heatmap_path = output_dir / heatmap_filename - heatmap = analyzer.compute_error_heatmap(interpolation=interpolation_mode) + heatmap = compute_lut_error_heatmap(loaded, model, interpolation=interpolation_mode) heatmap.save(heatmap_path) loaded_heatmap = UnprojectLUTErrorHeatmap.load(heatmap_path) fig = plot_unproject_lut_error_heatmap( @@ -333,7 +341,9 @@ def _(): mo.md(r""" ## C++ runtime - The same file can be loaded from the standalone runtime in `cpp_runtime/`. Simply copy-paste those files into your project and use them like so: + The same file can be loaded from the standalone runtime in + `cpp_runtime/`. Simply copy-paste those files into your project + and use them like so: ```cpp #include "unproject_lut.hpp" diff --git a/src/lensboy/analysis/__init__.py b/src/lensboy/analysis/__init__.py index 522f879..fd01560 100644 --- a/src/lensboy/analysis/__init__.py +++ b/src/lensboy/analysis/__init__.py @@ -1,15 +1,19 @@ from lensboy.analysis.unproject_lut import ( UnprojectLUTAccuracyReport, - UnprojectLUTAnalyzer, UnprojectLUTErrorHeatmap, UnprojectLUTSampleAccuracy, + compute_lut_error_heatmap, + estimate_lut_accuracy, + sample_lut_accuracy, ) __all__ = [ "UnprojectLUTAccuracyReport", - "UnprojectLUTAnalyzer", "UnprojectLUTErrorHeatmap", "UnprojectLUTSampleAccuracy", + "compute_lut_error_heatmap", + "estimate_lut_accuracy", + "sample_lut_accuracy", ] try: diff --git a/src/lensboy/analysis/unproject_lut.py b/src/lensboy/analysis/unproject_lut.py index ee72d88..1bf4c0d 100644 --- a/src/lensboy/analysis/unproject_lut.py +++ b/src/lensboy/analysis/unproject_lut.py @@ -1,20 +1,19 @@ from __future__ import annotations -import json from collections.abc import Callable -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import TYPE_CHECKING, cast import numpy as np from lensboy.camera_models.unproject_lut import InterpolationMode, UnprojectLUT if TYPE_CHECKING: - from matplotlib.figure import Figure - from lensboy.camera_models.base_model import CameraModel +_NormalizePointsFn = Callable[[np.ndarray], np.ndarray] + _SUPPORTED_INTERPOLATIONS: tuple[InterpolationMode, ...] = ( "nearest", "bilinear", @@ -70,53 +69,6 @@ def _normalize_interpolations( return tuple(normalized) -def _serialize_source_model_spec(source_model_spec: dict[str, Any] | None) -> str | None: - if source_model_spec is None: - return None - return json.dumps( - source_model_spec, - separators=(",", ":"), - sort_keys=True, - ensure_ascii=True, - ) - - -def _make_camera_model_from_spec(source_model_spec: dict[str, Any]) -> CameraModel: - model_type = source_model_spec.get("type") - if model_type == "opencv": - from lensboy.camera_models.opencv import OpenCV - - return OpenCV.from_json(source_model_spec) - if model_type == "pinhole_splined": - from lensboy.camera_models.pinhole_splined import PinholeSplined - - return PinholeSplined.from_json(source_model_spec) - raise ValueError(f"Unsupported source_model_spec type {model_type!r}.") - - -def _make_normalize_points_fn( - source_model_spec_json: str, -) -> Callable[[np.ndarray], np.ndarray]: - source_model_spec = json.loads(source_model_spec_json) - model_type = source_model_spec.get("type") - if model_type == "pinhole_remapped": - fx = float(source_model_spec["fx"]) - fy = float(source_model_spec["fy"]) - cx = float(source_model_spec["cx"]) - cy = float(source_model_spec["cy"]) - - def normalize_points(pixel_coords: np.ndarray) -> np.ndarray: - pts = np.asarray(pixel_coords, dtype=np.float64) - x = (pts[:, 0] - cx) / fx - y = (pts[:, 1] - cy) / fy - return np.column_stack([x, y, np.ones(len(pts), dtype=np.float64)]) - - return normalize_points - - model = _make_camera_model_from_spec(source_model_spec) - return model.normalize_points - - def _angular_error_deg_from_xy( reference_xy: np.ndarray, approx_xy: np.ndarray, @@ -651,363 +603,245 @@ def load(path: Path | str) -> UnprojectLUTErrorHeatmap: ) -@dataclass -class UnprojectLUTAnalyzer: - """Estimate accuracy statistics and heatmaps for a saved LUT. +def estimate_lut_accuracy( + lut: UnprojectLUT, + model: CameraModel, + *, + interpolations: InterpolationMode + | tuple[InterpolationMode, ...] + | list[InterpolationMode] = "bilinear", + mode: str = "adaptive", + max_depth: int = _DEFAULT_ERROR_MAX_DEPTH, + min_cell_size: float = _DEFAULT_ERROR_MIN_CELL_SIZE, +) -> UnprojectLUTAccuracyReport: + """Estimate angular interpolation accuracy for one or more modes. Args: lut: Runtime LUT to analyze. + model: The exact camera model the LUT was built from. + interpolations: Interpolation modes to include in the report. + mode: Error-estimation mode. Only ``"adaptive"`` is supported. + max_depth: Maximum adaptive subdivision depth. + min_cell_size: Minimum subcell size in pixels. Returns: - Analyzer that can compute error summaries and heatmaps using the LUT's - embedded exact source-model specification. + Accuracy report for the requested interpolation modes. """ - - lut: UnprojectLUT - _normalize_points_exact_fn: Callable[[np.ndarray], np.ndarray] | None = field( - default=None, - init=False, - repr=False, + normalized_interpolations = _normalize_interpolations(interpolations) + mode = _validate_error_mode(mode) + max_errors_mdeg, median_errors_mdeg = _estimate_adaptive_errors( + lut, + model.normalize_points, + interpolations=normalized_interpolations, + max_depth=max_depth, + min_cell_size=min_cell_size, + ) + return UnprojectLUTAccuracyReport( + interpolations=normalized_interpolations, + max_angular_error_mdeg=max_errors_mdeg, + median_angular_error_mdeg=median_errors_mdeg, + mode=mode, + max_depth=max_depth, + min_cell_size=min_cell_size, ) - def estimate_accuracy( - self, - *, - interpolations: InterpolationMode - | tuple[InterpolationMode, ...] - | list[InterpolationMode] = "bilinear", - mode: str = "adaptive", - max_depth: int = _DEFAULT_ERROR_MAX_DEPTH, - min_cell_size: float = _DEFAULT_ERROR_MIN_CELL_SIZE, - ) -> UnprojectLUTAccuracyReport: - """Estimate angular interpolation accuracy for one or more modes. - - Args: - interpolations: Interpolation modes to include in the report. - mode: Error-estimation mode. Only ``"adaptive"`` is supported. - max_depth: Maximum adaptive subdivision depth. - min_cell_size: Minimum subcell size in pixels. - Returns: - Accuracy report for the requested interpolation modes. - """ - normalized_interpolations = _normalize_interpolations(interpolations) - mode = _validate_error_mode(mode) - max_errors_mdeg, median_errors_mdeg = self._estimate_adaptive_errors( - interpolations=normalized_interpolations, - max_depth=max_depth, - min_cell_size=min_cell_size, - ) - return UnprojectLUTAccuracyReport( - interpolations=normalized_interpolations, - max_angular_error_mdeg=max_errors_mdeg, - median_angular_error_mdeg=median_errors_mdeg, - mode=mode, - max_depth=max_depth, - min_cell_size=min_cell_size, - ) - - def compute_error_heatmap( - self, - *, - interpolation: InterpolationMode = "bilinear", - mode: str = "adaptive", - max_depth: int = _DEFAULT_ERROR_MAX_DEPTH, - min_cell_size: float = _DEFAULT_ERROR_MIN_CELL_SIZE, - ) -> UnprojectLUTErrorHeatmap: - """Compute a per-cell error heatmap for one interpolation mode. +def compute_lut_error_heatmap( + lut: UnprojectLUT, + model: CameraModel, + *, + interpolation: InterpolationMode = "bilinear", + mode: str = "adaptive", + max_depth: int = _DEFAULT_ERROR_MAX_DEPTH, + min_cell_size: float = _DEFAULT_ERROR_MIN_CELL_SIZE, +) -> UnprojectLUTErrorHeatmap: + """Compute a per-cell error heatmap for one interpolation mode. - Args: - interpolation: Interpolation mode to evaluate. - mode: Error-estimation mode. Only ``"adaptive"`` is supported. - max_depth: Maximum adaptive subdivision depth. - min_cell_size: Minimum subcell size in pixels. + Args: + lut: Runtime LUT to analyze. + model: The exact camera model the LUT was built from. + interpolation: Interpolation mode to evaluate. + mode: Error-estimation mode. Only ``"adaptive"`` is supported. + max_depth: Maximum adaptive subdivision depth. + min_cell_size: Minimum subcell size in pixels. - Returns: - In-memory heatmap for the requested interpolation mode. - """ - interpolation = _validate_interpolation_mode(interpolation) - _validate_error_mode(mode) + Returns: + In-memory heatmap for the requested interpolation mode. + """ + interpolation = _validate_interpolation_mode(interpolation) + _validate_error_mode(mode) + normalize_points_fn = model.normalize_points + + x_edges = np.linspace(lut.grid_x_min, lut.grid_x_max, lut.grid_width) + y_edges = np.linspace(lut.grid_y_min, lut.grid_y_max, lut.grid_height) + heatmap_width = max(lut.grid_width - 1, 1) + heatmap_height = max(lut.grid_height - 1, 1) + max_angular_error_deg = np.zeros((heatmap_height, heatmap_width), dtype=np.float64) + error_direction_xy = np.zeros((heatmap_height, heatmap_width, 2), dtype=np.float64) + error_delta_xy = np.zeros((heatmap_height, heatmap_width, 2), dtype=np.float64) + peak_pixel_xy = np.zeros((heatmap_height, heatmap_width, 2), dtype=np.float64) + + for iy in range(heatmap_height): + y0 = y_edges[min(iy, len(y_edges) - 1)] + y1 = y_edges[min(iy + 1, len(y_edges) - 1)] + for ix in range(heatmap_width): + x0 = x_edges[min(ix, len(x_edges) - 1)] + x1 = x_edges[min(ix + 1, len(x_edges) - 1)] + ( + max_error, + direction_xy, + delta_xy, + peak_pixel, + ) = _estimate_cell_error_detail( + lut, + normalize_points_fn, + mode=interpolation, + x0=x0, + x1=x1, + y0=y0, + y1=y1, + depth=0, + max_depth=max_depth, + min_cell_size=min_cell_size, + sampled_errors_deg=[], + ) + max_angular_error_deg[iy, ix] = max_error + error_direction_xy[iy, ix] = direction_xy + error_delta_xy[iy, ix] = delta_xy + peak_pixel_xy[iy, ix] = peak_pixel + + return UnprojectLUTErrorHeatmap( + interpolation=interpolation, + max_depth=max_depth, + min_cell_size=min_cell_size, + cell_x_edges=x_edges, + cell_y_edges=y_edges, + max_angular_error_deg=max_angular_error_deg, + error_direction_xy=error_direction_xy, + error_delta_xy=error_delta_xy, + peak_pixel_xy=peak_pixel_xy, + ) - x_edges = np.linspace( - self.lut.grid_x_min, self.lut.grid_x_max, self.lut.grid_width - ) - y_edges = np.linspace( - self.lut.grid_y_min, self.lut.grid_y_max, self.lut.grid_height - ) - heatmap_width = max(self.lut.grid_width - 1, 1) - heatmap_height = max(self.lut.grid_height - 1, 1) - max_angular_error_deg = np.zeros( - (heatmap_height, heatmap_width), dtype=np.float64 - ) - error_direction_xy = np.zeros( - (heatmap_height, heatmap_width, 2), dtype=np.float64 - ) - error_delta_xy = np.zeros((heatmap_height, heatmap_width, 2), dtype=np.float64) - peak_pixel_xy = np.zeros((heatmap_height, heatmap_width, 2), dtype=np.float64) - - normalize_points_fn = self._normalize_points_exact() - for iy in range(heatmap_height): - y0 = y_edges[min(iy, len(y_edges) - 1)] - y1 = y_edges[min(iy + 1, len(y_edges) - 1)] - for ix in range(heatmap_width): - x0 = x_edges[min(ix, len(x_edges) - 1)] - x1 = x_edges[min(ix + 1, len(x_edges) - 1)] - ( - max_error, - direction_xy, - delta_xy, - peak_pixel, - ) = _estimate_cell_error_detail( - self.lut, - normalize_points_fn, - mode=interpolation, - x0=x0, - x1=x1, - y0=y0, - y1=y1, - depth=0, - max_depth=max_depth, - min_cell_size=min_cell_size, - sampled_errors_deg=[], - ) - max_angular_error_deg[iy, ix] = max_error - error_direction_xy[iy, ix] = direction_xy - error_delta_xy[iy, ix] = delta_xy - peak_pixel_xy[iy, ix] = peak_pixel - - return UnprojectLUTErrorHeatmap( - interpolation=interpolation, - max_depth=max_depth, - min_cell_size=min_cell_size, - cell_x_edges=x_edges, - cell_y_edges=y_edges, - max_angular_error_deg=max_angular_error_deg, - error_direction_xy=error_direction_xy, - error_delta_xy=error_delta_xy, - peak_pixel_xy=peak_pixel_xy, - ) - def sample_accuracy_grid( - self, - *, - interpolation: InterpolationMode = "bilinear", - target_sample_count: int = 2500, - ) -> UnprojectLUTSampleAccuracy: - """Sample LUT accuracy on an evenly spaced image grid. +def sample_lut_accuracy( + lut: UnprojectLUT, + model: CameraModel, + *, + interpolation: InterpolationMode = "bilinear", + target_sample_count: int = 2500, +) -> UnprojectLUTSampleAccuracy: + """Sample LUT accuracy on an evenly spaced image grid. - Args: - interpolation: Interpolation mode to evaluate. - target_sample_count: Approximate number of evenly spaced sample pixels. + Args: + lut: Runtime LUT to analyze. + model: The exact camera model the LUT was built from. + interpolation: Interpolation mode to evaluate. + target_sample_count: Approximate number of evenly spaced sample pixels. - Returns: - Dense sampled comparison result between the LUT and the exact source - model. - """ - interpolation = _validate_interpolation_mode(interpolation) - target_sample_count = _validate_target_sample_count(target_sample_count) - ( - sample_grid_width, - sample_grid_height, + Returns: + Dense sampled comparison between the LUT and the exact model. + """ + interpolation = _validate_interpolation_mode(interpolation) + target_sample_count = _validate_target_sample_count(target_sample_count) + ( + sample_grid_width, + sample_grid_height, + sample_pixels, + ) = _dense_sample_grid( + image_width=lut.image_width, + image_height=lut.image_height, + target_sample_count=target_sample_count, + ) + exact_rays = model.normalize_points(sample_pixels) + approx_rays = cast( + np.ndarray, + lut.normalize_points( sample_pixels, - ) = _dense_sample_grid( - image_width=self.lut.image_width, - image_height=self.lut.image_height, - target_sample_count=target_sample_count, - ) - exact_rays = self._normalize_points_exact()(sample_pixels) - approx_rays = cast( - np.ndarray, - self.lut.normalize_points( - sample_pixels, - interpolation=interpolation, - bounds="strict", - ), - ) - angular_error_deg = _angular_error_deg_from_xy( - np.asarray(exact_rays[:, :2], dtype=np.float64), - np.asarray(approx_rays[:, :2], dtype=np.float64), - ) - return UnprojectLUTSampleAccuracy( interpolation=interpolation, - target_sample_count=target_sample_count, - sample_grid_width=sample_grid_width, - sample_grid_height=sample_grid_height, - sample_pixels=sample_pixels, - exact_rays=exact_rays, - approx_rays=approx_rays, - angular_error_deg=angular_error_deg, - ) - - def save_error_heatmap( - self, - path: Path | str, - *, - interpolation: InterpolationMode = "bilinear", - mode: str = "adaptive", - max_depth: int = _DEFAULT_ERROR_MAX_DEPTH, - min_cell_size: float = _DEFAULT_ERROR_MIN_CELL_SIZE, - ) -> None: - """Compute and save a heatmap archive. + bounds="strict", + ), + ) + angular_error_deg = _angular_error_deg_from_xy( + np.asarray(exact_rays[:, :2], dtype=np.float64), + np.asarray(approx_rays[:, :2], dtype=np.float64), + ) + return UnprojectLUTSampleAccuracy( + interpolation=interpolation, + target_sample_count=target_sample_count, + sample_grid_width=sample_grid_width, + sample_grid_height=sample_grid_height, + sample_pixels=sample_pixels, + exact_rays=exact_rays, + approx_rays=approx_rays, + angular_error_deg=angular_error_deg, + ) - Args: - path: Output `.npz` path. - interpolation: Interpolation mode to evaluate. - mode: Error-estimation mode. Only ``"adaptive"`` is supported. - max_depth: Maximum adaptive subdivision depth. - min_cell_size: Minimum subcell size in pixels. - Returns: - None. - """ - heatmap = self.compute_error_heatmap( - interpolation=interpolation, - mode=mode, - max_depth=max_depth, - min_cell_size=min_cell_size, +def _estimate_adaptive_errors( + lut: UnprojectLUT, + normalize_points_fn: _NormalizePointsFn, + *, + interpolations: tuple[InterpolationMode, ...], + max_depth: int, + min_cell_size: float, +) -> tuple[dict[str, float], dict[str, float]]: + max_errors_mdeg: dict[str, float] = {} + median_errors_mdeg: dict[str, float] = {} + + if lut.grid_width == 1 and lut.grid_height == 1: + sample_points = np.array( + [[lut.grid_x_min, lut.grid_y_min]], + dtype=np.float64, ) - heatmap.save(path) - - def plot_error_heatmap( - self, - *, - interpolation: InterpolationMode = "bilinear", - mode: str = "adaptive", - max_depth: int = _DEFAULT_ERROR_MAX_DEPTH, - min_cell_size: float = _DEFAULT_ERROR_MIN_CELL_SIZE, - title: str | None = None, - angular_unit: Literal["deg", "mdeg", "udeg", "rad", "mrad", "urad"] = "mdeg", - show_directions: bool = True, - arrow_grid: int = 28, - arrow_scale: float = 0.5, - cmap_name: str = "inferno", - figsize: tuple[float, float] = (8.5, 6.0), - return_figure: bool = False, - ) -> Figure | None: - """Compute and plot a heatmap for one interpolation mode. - - Args: - interpolation: Interpolation mode to evaluate. - mode: Error-estimation mode. Only ``"adaptive"`` is supported. - max_depth: Maximum adaptive subdivision depth. - min_cell_size: Minimum subcell size in pixels. - title: Plot title override. - angular_unit: Angular units for the color scale. - show_directions: Whether to draw the error-direction arrows. - arrow_grid: Approximate maximum number of arrows along the longer axis. - arrow_scale: Arrow length as a fraction of the spacing between arrows. - cmap_name: Matplotlib colormap name. - figsize: Figure size in inches as ``(width, height)``. - return_figure: If True, return the figure instead of calling ``plt.show()``. + exact_rays = normalize_points_fn(sample_points) + exact_xy = np.asarray(exact_rays[:, :2], dtype=np.float64) + for mode in interpolations: + approx_xy = lut._interpolate_xy( + sample_points[:, 0], + sample_points[:, 1], + mode, + "strict", + ) + sample_errors_deg = _angular_error_deg_from_xy(exact_xy, approx_xy) + max_errors_mdeg[mode] = ( + float(np.max(sample_errors_deg)) * _ANGULAR_ERROR_MDEG_SCALE + ) + median_errors_mdeg[mode] = ( + float(np.median(sample_errors_deg)) * _ANGULAR_ERROR_MDEG_SCALE + ) + return max_errors_mdeg, median_errors_mdeg - Returns: - The figure if ``return_figure`` is True, otherwise None. - """ - heatmap = self.compute_error_heatmap( - interpolation=interpolation, + x_edges = np.linspace(lut.grid_x_min, lut.grid_x_max, lut.grid_width) + y_edges = np.linspace(lut.grid_y_min, lut.grid_y_max, lut.grid_height) + x0_cells = x_edges[: max(lut.grid_width - 1, 1)] + x1_cells = x_edges[1:] if lut.grid_width > 1 else x_edges[:1] + y0_cells = y_edges[: max(lut.grid_height - 1, 1)] + y1_cells = y_edges[1:] if lut.grid_height > 1 else y_edges[:1] + cell_x0, cell_y0 = np.meshgrid(x0_cells, y0_cells, indexing="xy") + cell_x1, cell_y1 = np.meshgrid(x1_cells, y1_cells, indexing="xy") + flat_x0 = cell_x0.ravel() + flat_x1 = cell_x1.ravel() + flat_y0 = cell_y0.ravel() + flat_y1 = cell_y1.ravel() + + for mode in interpolations: + max_error, sampled_errors_deg = _estimate_adaptive_errors_for_cell_chunk( + lut, + normalize_points_fn, mode=mode, + x0=flat_x0, + x1=flat_x1, + y0=flat_y0, + y1=flat_y1, max_depth=max_depth, min_cell_size=min_cell_size, ) - from lensboy.analysis.plots import plot_unproject_lut_error_heatmap - - return plot_unproject_lut_error_heatmap( - heatmap, - title=title, - angular_unit=angular_unit, - show_directions=show_directions, - arrow_grid=arrow_grid, - arrow_scale=arrow_scale, - cmap_name=cmap_name, - figsize=figsize, - return_figure=return_figure, + median_error = ( + float(np.median(sampled_errors_deg)) + if len(sampled_errors_deg) > 0 + else float("nan") ) + max_errors_mdeg[mode] = max_error * _ANGULAR_ERROR_MDEG_SCALE + median_errors_mdeg[mode] = median_error * _ANGULAR_ERROR_MDEG_SCALE - def _require_source_model_spec_json(self) -> str: - source_model_spec_json = _serialize_source_model_spec(self.lut.source_model_spec) - if source_model_spec_json is None: - raise ValueError( - "This LUT does not carry source_model_spec_json, so exact accuracy " - "analysis is unavailable." - ) - return source_model_spec_json - - def _normalize_points_exact(self) -> Callable[[np.ndarray], np.ndarray]: - if self._normalize_points_exact_fn is None: - self._normalize_points_exact_fn = _make_normalize_points_fn( - self._require_source_model_spec_json() - ) - return self._normalize_points_exact_fn - - def _estimate_adaptive_errors( - self, - *, - interpolations: tuple[InterpolationMode, ...], - max_depth: int, - min_cell_size: float, - ) -> tuple[dict[str, float], dict[str, float]]: - max_errors_mdeg: dict[str, float] = {} - median_errors_mdeg: dict[str, float] = {} - normalize_points_fn = self._normalize_points_exact() - - if self.lut.grid_width == 1 and self.lut.grid_height == 1: - sample_points = np.array( - [[self.lut.grid_x_min, self.lut.grid_y_min]], - dtype=np.float64, - ) - exact_rays = normalize_points_fn(sample_points) - exact_xy = np.asarray(exact_rays[:, :2], dtype=np.float64) - for mode in interpolations: - approx_xy = self.lut._interpolate_xy( - sample_points[:, 0], - sample_points[:, 1], - mode, - "strict", - ) - sample_errors_deg = _angular_error_deg_from_xy(exact_xy, approx_xy) - max_errors_mdeg[mode] = ( - float(np.max(sample_errors_deg)) * _ANGULAR_ERROR_MDEG_SCALE - ) - median_errors_mdeg[mode] = ( - float(np.median(sample_errors_deg)) * _ANGULAR_ERROR_MDEG_SCALE - ) - return max_errors_mdeg, median_errors_mdeg - - x_edges = np.linspace( - self.lut.grid_x_min, self.lut.grid_x_max, self.lut.grid_width - ) - y_edges = np.linspace( - self.lut.grid_y_min, self.lut.grid_y_max, self.lut.grid_height - ) - x0_cells = x_edges[: max(self.lut.grid_width - 1, 1)] - x1_cells = x_edges[1:] if self.lut.grid_width > 1 else x_edges[:1] - y0_cells = y_edges[: max(self.lut.grid_height - 1, 1)] - y1_cells = y_edges[1:] if self.lut.grid_height > 1 else y_edges[:1] - cell_x0, cell_y0 = np.meshgrid(x0_cells, y0_cells, indexing="xy") - cell_x1, cell_y1 = np.meshgrid(x1_cells, y1_cells, indexing="xy") - flat_x0 = cell_x0.ravel() - flat_x1 = cell_x1.ravel() - flat_y0 = cell_y0.ravel() - flat_y1 = cell_y1.ravel() - - for mode in interpolations: - max_error, sampled_errors_deg = _estimate_adaptive_errors_for_cell_chunk( - self.lut, - normalize_points_fn, - mode=mode, - x0=flat_x0, - x1=flat_x1, - y0=flat_y0, - y1=flat_y1, - max_depth=max_depth, - min_cell_size=min_cell_size, - ) - median_error = ( - float(np.median(sampled_errors_deg)) - if len(sampled_errors_deg) > 0 - else float("nan") - ) - max_errors_mdeg[mode] = max_error * _ANGULAR_ERROR_MDEG_SCALE - median_errors_mdeg[mode] = median_error * _ANGULAR_ERROR_MDEG_SCALE - - return max_errors_mdeg, median_errors_mdeg + return max_errors_mdeg, median_errors_mdeg diff --git a/src/lensboy/camera_models/base_model.py b/src/lensboy/camera_models/base_model.py index d01f470..43d0b2f 100644 --- a/src/lensboy/camera_models/base_model.py +++ b/src/lensboy/camera_models/base_model.py @@ -2,16 +2,9 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import TYPE_CHECKING import numpy as np -if TYPE_CHECKING: - from lensboy.camera_models.unproject_lut import ( - StorageEncoding, - UnprojectLUT, - ) - @dataclass class CameraModelConfig(ABC): @@ -46,35 +39,3 @@ def normalize_points(self, pixel_coords: np.ndarray) -> np.ndarray: Normalized points in camera frame, shape (N, 3) with z=1. """ ... - - def get_unproject_lut( - self, - *, - grid_size_wh: tuple[int, int] | None = None, - pixel_stride: float | tuple[float, float] | None = None, - storage_encoding: StorageEncoding = "float64_xy", - num_workers: int | None = None, - ) -> UnprojectLUT: - """Build a lookup table that caches `normalize_points()` over a regular grid. - - Args: - grid_size_wh: Number of cached samples as (width, height). If None, - a per-pixel grid is used unless `pixel_stride` is given. - pixel_stride: Approximate sample spacing in pixels. Mutually exclusive - with `grid_size_wh`. - storage_encoding: On-disk payload encoding to use when saving the LUT. - num_workers: Number of worker threads to use while sampling the LUT - grid. - - Returns: - A populated unprojection lookup table. - """ - from lensboy.camera_models.unproject_lut import UnprojectLUT - - return UnprojectLUT.from_camera_model( - self, - grid_size_wh=grid_size_wh, - pixel_stride=pixel_stride, - storage_encoding=storage_encoding, - num_workers=num_workers, - ) diff --git a/src/lensboy/camera_models/opencv.py b/src/lensboy/camera_models/opencv.py index a3ab190..a531060 100644 --- a/src/lensboy/camera_models/opencv.py +++ b/src/lensboy/camera_models/opencv.py @@ -5,12 +5,16 @@ from dataclasses import dataclass, field, replace from importlib.metadata import version as _package_version from pathlib import Path +from typing import TYPE_CHECKING import cv2 import numpy as np from lensboy.camera_models.base_model import CameraModel, CameraModelConfig +if TYPE_CHECKING: + from lensboy.camera_models.unproject_lut import StorageEncoding, UnprojectLUT + K1, K2, P1, P2, K3, K4, K5, K6, S1, S2, S3, S4, TX, TY = range(14) _UNDISTORT_POINTS_ITER_CRITERIA = ( cv2.TERM_CRITERIA_COUNT | cv2.TERM_CRITERIA_EPS, @@ -226,6 +230,33 @@ def _camera_matrix_cached(self) -> np.ndarray: float(self.cy), ) + def get_unproject_lut( + self, + *, + grid_size_wh: tuple[int, int] | None = None, + pixel_stride: float | tuple[float, float] | None = None, + storage_encoding: StorageEncoding = "float64_xy", + ) -> UnprojectLUT: + """Build a lookup table that caches ``normalize_points()`` on a grid. + + Args: + grid_size_wh: Number of cached samples as ``(width, height)``. + pixel_stride: Approximate sample spacing in pixels. Mutually + exclusive with ``grid_size_wh``. + storage_encoding: On-disk payload encoding. + + Returns: + A populated unprojection lookup table. + """ + from lensboy.camera_models.unproject_lut import UnprojectLUT + + return UnprojectLUT.from_camera_model( + self, + grid_size_wh=grid_size_wh, + pixel_stride=pixel_stride, + storage_encoding=storage_encoding, + ) + @property def fov_deg_x(self) -> float: """Horizontal field of view in degrees. diff --git a/src/lensboy/camera_models/pinhole_splined.py b/src/lensboy/camera_models/pinhole_splined.py index 9545dbb..02220d2 100644 --- a/src/lensboy/camera_models/pinhole_splined.py +++ b/src/lensboy/camera_models/pinhole_splined.py @@ -4,11 +4,15 @@ from dataclasses import dataclass from importlib.metadata import version as _package_version from pathlib import Path +from typing import TYPE_CHECKING import numpy as np from lensboy import lensboy_bindings as lbb from lensboy.camera_models.base_model import CameraModel, CameraModelConfig + +if TYPE_CHECKING: + from lensboy.camera_models.unproject_lut import StorageEncoding, UnprojectLUT from lensboy.camera_models.pinhole_remapped import PinholeRemapped @@ -222,6 +226,33 @@ def normalize_points(self, pixel_coords: np.ndarray) -> np.ndarray: pixel_coords=pts, ) + def get_unproject_lut( + self, + *, + grid_size_wh: tuple[int, int] | None = None, + pixel_stride: float | tuple[float, float] | None = None, + storage_encoding: StorageEncoding = "float64_xy", + ) -> UnprojectLUT: + """Build a lookup table that caches ``normalize_points()`` on a grid. + + Args: + grid_size_wh: Number of cached samples as ``(width, height)``. + pixel_stride: Approximate sample spacing in pixels. Mutually + exclusive with ``grid_size_wh``. + storage_encoding: On-disk payload encoding. + + Returns: + A populated unprojection lookup table. + """ + from lensboy.camera_models.unproject_lut import UnprojectLUT + + return UnprojectLUT.from_camera_model( + self, + grid_size_wh=grid_size_wh, + pixel_stride=pixel_stride, + storage_encoding=storage_encoding, + ) + def project_points( self, points_in_cam: np.ndarray, diff --git a/src/lensboy/camera_models/unproject_lut.py b/src/lensboy/camera_models/unproject_lut.py index eff6b75..32ab10f 100644 --- a/src/lensboy/camera_models/unproject_lut.py +++ b/src/lensboy/camera_models/unproject_lut.py @@ -1,14 +1,10 @@ from __future__ import annotations -import hashlib -import json import math -import os -from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass, field from importlib.metadata import version as _package_version from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Literal import numpy as np @@ -38,7 +34,6 @@ "float16_xy": np.dtype(" InterpolationMode: @@ -67,50 +62,6 @@ def _validate_storage_encoding(storage_encoding: str) -> StorageEncoding: return storage_encoding # type: ignore[return-value] -def _snake_case_name(name: str) -> str: - out: list[str] = [] - for i, ch in enumerate(name): - if ch.isupper() and i > 0 and not name[i - 1].isupper(): - out.append("_") - out.append(ch.lower()) - return "".join(out) - - -def _camera_model_type_name(camera_model: CameraModel) -> str: - camera_model_name = getattr(camera_model, "_camera_model_name", None) - if callable(camera_model_name): - return str(camera_model_name()) - - name = _snake_case_name(type(camera_model).__name__) - if name == "open_cv": - return "opencv" - return name - - -def _camera_model_spec(camera_model: CameraModel) -> dict[str, Any] | None: - to_json = getattr(camera_model, "to_json", None) - if not callable(to_json): - return None - - serialized = to_json() - if isinstance(serialized, tuple): - serialized = serialized[0] - if not isinstance(serialized, dict): - return None - return serialized - - -def _serialize_source_model_spec(source_model_spec: dict[str, Any] | None) -> str | None: - if source_model_spec is None: - return None - return json.dumps( - source_model_spec, - separators=(",", ":"), - sort_keys=True, - ensure_ascii=True, - ) - - def _parse_pair_of_ints(text: str, field_name: str) -> tuple[int, int]: parts = [part.strip() for part in text.split(",")] if len(parts) != 2: @@ -134,36 +85,16 @@ def _parse_pair_of_floats(text: str, field_name: str) -> tuple[float, float]: return float(parts[0]), float(parts[1]) -def _parse_optional_str(text: str) -> str | None: - if text == "not_computed": - return None - return text - - def _format_float(value: float) -> str: if math.isnan(value): return "not_computed" return f"{value:.17g}" -def _format_optional_str(value: str | None) -> str: - return "not_computed" if value is None else value - - def _format_csv(values: list[str]) -> str: return ", ".join(values) -def _normalize_num_workers(num_workers: int | None) -> int: - if num_workers is None: - resolved = os.cpu_count() or 1 - else: - resolved = int(num_workers) - if resolved <= 0: - raise ValueError("num_workers must be positive when provided.") - return resolved - - def _catmull_rom_weights(t: np.ndarray) -> np.ndarray: t2 = t * t t3 = t2 * t @@ -333,8 +264,7 @@ def _sample_xy_grid_seeded( ) else: raise TypeError( - f"Seeded normalization not supported for {type(camera_model).__name__}. " - "Use the standard sampling path instead." + "UnprojectLUT is only supported for OpenCV and PinholeSplined models." ) xy = np.asarray(rays[:, :2], dtype=np.float64) @@ -385,8 +315,6 @@ class UnprojectLUT: xy_grid: Cached x/y ray components with shape ``(grid_height, grid_width, 2)``. default_interpolation: Default interpolation mode for runtime queries. default_bounds: Default bounds behavior for runtime queries. - source_model_type: Name of the camera model used to build the LUT. - source_model_spec: Exact serialized source-model specification, if available. lensboy_version: Package version that produced the LUT. Returns: @@ -405,8 +333,6 @@ class UnprojectLUT: xy_grid: np.ndarray default_interpolation: InterpolationMode = "bilinear" default_bounds: BoundsMode = "strict" - source_model_type: str | None = None - source_model_spec: dict[str, Any] | None = None lensboy_version: str = field(default_factory=lambda: _package_version("lensboy")) _grid_scale_x: float = field(init=False, repr=False) _grid_scale_y: float = field(init=False, repr=False) @@ -417,10 +343,6 @@ def __post_init__(self) -> None: self.default_interpolation ) self.default_bounds = _validate_bounds_mode(self.default_bounds) - if self.source_model_spec is not None and not isinstance( - self.source_model_spec, dict - ): - raise ValueError("source_model_spec must be a dict when provided.") if self.image_width <= 0 or self.image_height <= 0: raise ValueError("image dimensions must be positive.") if self.grid_width <= 0 or self.grid_height <= 0: @@ -557,28 +479,12 @@ def supported_bounds(self) -> tuple[BoundsMode, ...]: """ return _SUPPORTED_BOUNDS - @property - def source_model_spec_json_sha256(self) -> str | None: - """Return a stable hash of the serialized source-model specification. - - Returns: - Lowercase SHA-256 hex digest of the canonical source-model JSON, or None - if the LUT does not carry a source-model specification. - """ - source_model_spec_json = self._source_model_spec_json() - if source_model_spec_json is None: - return None - return hashlib.sha256(source_model_spec_json.encode("ascii")).hexdigest() - @staticmethod def _compute_grid_scale(size: int, minimum: float, maximum: float) -> float: if size == 1 or maximum == minimum: return 0.0 return (size - 1) / (maximum - minimum) - def _source_model_spec_json(self) -> str | None: - return _serialize_source_model_spec(self.source_model_spec) - @staticmethod def from_camera_model( camera_model: CameraModel, @@ -586,7 +492,6 @@ def from_camera_model( grid_size_wh: tuple[int, int] | None = None, pixel_stride: float | tuple[float, float] | None = None, storage_encoding: StorageEncoding = "float64_xy", - num_workers: int | None = None, ) -> UnprojectLUT: """Build a LUT from a camera model. @@ -597,13 +502,11 @@ def from_camera_model( pixel_stride: Approximate pixel spacing between cached samples. Mutually exclusive with ``grid_size_wh``. storage_encoding: On-disk payload encoding to use when saving. - num_workers: Number of worker threads to use while sampling the LUT grid. Returns: A populated unprojection LUT. """ storage_encoding = _validate_storage_encoding(storage_encoding) - resolved_num_workers = _normalize_num_workers(num_workers) if grid_size_wh is not None and pixel_stride is not None: raise ValueError("grid_size_wh and pixel_stride are mutually exclusive.") @@ -647,99 +550,9 @@ def from_camera_model( grid_y_max=float(camera_model.image_height - 1), storage_encoding=storage_encoding, xy_grid=xy_grid, - source_model_type=_camera_model_type_name(camera_model), - source_model_spec=_camera_model_spec(camera_model), ) return lut - @staticmethod - def _sample_xy_grid( - camera_model: CameraModel, - *, - x_coords: np.ndarray, - y_coords: np.ndarray, - payload_dtype: np.dtype, - num_workers: int, - ) -> np.ndarray: - grid_width = len(x_coords) - grid_height = len(y_coords) - xy_grid = np.empty((grid_height, grid_width, 2), dtype=np.float64) - target_chunk_rows = max( - 1, - min( - grid_height, - _LUT_BUILD_TARGET_CHUNK_SAMPLES // max(grid_width, 1), - ), - ) - if num_workers > 1: - target_parallel_chunks = min(num_workers, grid_height) - target_chunk_rows = min( - target_chunk_rows, - max(1, int(math.ceil(grid_height / target_parallel_chunks))), - ) - - row_ranges = [ - (row_start, min(row_start + target_chunk_rows, grid_height)) - for row_start in range(0, grid_height, target_chunk_rows) - ] - - if len(row_ranges) == 1 or num_workers == 1: - for row_start, row_stop in row_ranges: - _, _, xy_chunk = UnprojectLUT._sample_xy_grid_chunk( - camera_model, - x_coords=x_coords, - y_coords=y_coords, - payload_dtype=payload_dtype, - row_start=row_start, - row_stop=row_stop, - ) - xy_grid[row_start:row_stop] = xy_chunk - return xy_grid - - worker_count = min(num_workers, len(row_ranges)) - with ThreadPoolExecutor(max_workers=worker_count) as executor: - futures = [ - executor.submit( - UnprojectLUT._sample_xy_grid_chunk, - camera_model, - x_coords=x_coords, - y_coords=y_coords, - payload_dtype=payload_dtype, - row_start=row_start, - row_stop=row_stop, - ) - for row_start, row_stop in row_ranges - ] - for future in futures: - row_start, row_stop, xy_chunk = future.result() - xy_grid[row_start:row_stop] = xy_chunk - - return xy_grid - - @staticmethod - def _sample_xy_grid_chunk( - camera_model: CameraModel, - *, - x_coords: np.ndarray, - y_coords: np.ndarray, - payload_dtype: np.dtype, - row_start: int, - row_stop: int, - ) -> tuple[int, int, np.ndarray]: - y_chunk = y_coords[row_start:row_stop] - grid_width = len(x_coords) - pixel_count = grid_width * len(y_chunk) - pixels = np.empty((pixel_count, 2), dtype=np.float64) - pixels[:, 0] = np.tile(x_coords, len(y_chunk)) - pixels[:, 1] = np.repeat(y_chunk, grid_width) - - rays = camera_model.normalize_points(pixels) - xy_chunk = np.asarray(rays[:, :2], dtype=np.float64) - if payload_dtype != np.dtype(np.float64): - xy_chunk = np.asarray(xy_chunk, dtype=payload_dtype).astype(np.float64) - - return row_start, row_stop, xy_chunk.reshape(len(y_chunk), grid_width, 2) - def save(self, path: Path | str) -> None: """Write the LUT to a `.unproject_LUT` file. @@ -842,25 +655,6 @@ def load(path: Path | str) -> UnprojectLUT: "grid_stride_xy does not match grid_extents_xy and grid_size_wh." ) - source_model_spec_json = header["source_model_spec_json"] - source_model_spec_json_sha256 = header["source_model_spec_json_sha256"] - if source_model_spec_json == "not_computed": - if source_model_spec_json_sha256 != "not_computed": - raise ValueError( - "source_model_spec_json_sha256 must be not_computed when " - "source_model_spec_json is not_computed." - ) - source_model_spec = None - else: - expected_hash = hashlib.sha256( - source_model_spec_json.encode("ascii") - ).hexdigest() - if source_model_spec_json_sha256 != expected_hash: - raise ValueError( - "source_model_spec_json_sha256 does not match source_model_spec_json." - ) - source_model_spec = json.loads(source_model_spec_json) - xy_grid = ( np.frombuffer(payload, dtype=dtype) .astype(np.float64) @@ -882,8 +676,6 @@ def load(path: Path | str) -> UnprojectLUT: header["default_interpolation"] ), default_bounds=_validate_bounds_mode(header["default_bounds"]), - source_model_type=_parse_optional_str(header["source_model_type"]), - source_model_spec=source_model_spec, lensboy_version=header["lensboy_version"], ) @@ -928,9 +720,6 @@ def _validate_header_fields(header: dict[str, str]) -> None: "format", "format_version", "lensboy_version", - "source_model_type", - "source_model_spec_json", - "source_model_spec_json_sha256", "image_size_wh", "grid_size_wh", "grid_extents_xy", @@ -971,15 +760,6 @@ def _validate_header_fields(header: dict[str, str]) -> None: ) def _encode_header(self) -> str: - source_model_spec_json = self._source_model_spec_json() - if source_model_spec_json is None: - source_model_spec_json = "not_computed" - source_model_spec_json_sha256 = "not_computed" - else: - source_model_spec_json_sha256 = hashlib.sha256( - source_model_spec_json.encode("ascii") - ).hexdigest() - stride_x, stride_y = self.grid_stride_xy def build_lines(payload_offset_bytes_text: str) -> list[str]: @@ -988,8 +768,6 @@ def build_lines(payload_offset_bytes_text: str) -> list[str]: f"payload_offset_bytes: {payload_offset_bytes_text}", f"format_version: {_FORMAT_VERSION}", f"lensboy_version: {self.lensboy_version}", - f"source_model_type: {_format_optional_str(self.source_model_type)}", - f"source_model_spec_json_sha256: {source_model_spec_json_sha256}", "image_size_wh: " + _format_csv([str(self.image_width), str(self.image_height)]), "grid_size_wh: " @@ -1010,7 +788,6 @@ def build_lines(payload_offset_bytes_text: str) -> list[str]: f"default_bounds: {self.default_bounds}", f"payload_layout: {_PAYLOAD_LAYOUT}", f"payload_endianness: {_PAYLOAD_ENDIANNESS}", - f"source_model_spec_json: {source_model_spec_json}", _HEADER_END_MARKER, ] diff --git a/tests/test_unproject_lut.py b/tests/test_unproject_lut.py index bfa5b70..21f7b2f 100644 --- a/tests/test_unproject_lut.py +++ b/tests/test_unproject_lut.py @@ -2,7 +2,6 @@ from __future__ import annotations -import hashlib import shutil import subprocess import textwrap @@ -18,18 +17,16 @@ CPP_RUNTIME_DIR = REPO_ROOT / "cpp_runtime" -def _make_linear_pinhole_model() -> lb.PinholeRemapped: - return lb.PinholeRemapped( +def _make_linear_pinhole_model() -> lb.OpenCV: + """Zero-distortion OpenCV model that behaves like a pure pinhole.""" + return lb.OpenCV( image_width=17, image_height=13, fx=23.0, fy=19.0, cx=8.0, cy=6.0, - map_x=np.zeros((13, 17), dtype=np.float32), - map_y=np.zeros((13, 17), dtype=np.float32), - input_image_width=17, - input_image_height=13, + distortion_coeffs=np.zeros(14, dtype=np.float64), ) @@ -41,10 +38,6 @@ def _load_spline_model() -> lb.PinholeSplined: return lb.PinholeSplined.load(DATA_DIR / "spline.json") -def _load_remapped_model() -> lb.PinholeRemapped: - return _load_spline_model().get_pinhole_model() - - def _header_and_payload(file_path: Path) -> tuple[str, bytes]: data = file_path.read_bytes() marker = b"END_HEADER\n" @@ -121,7 +114,7 @@ def _parse_header_text(header: str) -> dict[str, str]: def _random_pixels( - model: lb.PinholeRemapped, + model: lb.CameraModel, n: int = 128, seed: int = 0, ) -> np.ndarray: @@ -159,37 +152,18 @@ def test_unproject_lut_header_is_human_readable(tmp_path: Path) -> None: assert first_lines[1].startswith("payload_offset_bytes: ") assert first_lines[2] == "format_version: 1" assert first_lines[3].startswith("lensboy_version: ") - assert first_lines[4] == "source_model_type: pinhole_remapped" - assert first_lines[5].startswith("source_model_spec_json_sha256: ") - assert first_lines[6] == "image_size_wh: 17, 13" - assert first_lines[7] == "grid_size_wh: 5, 4" - assert first_lines[8] == "grid_extents_xy: 0, 16, 0, 12" - assert first_lines[9] == "grid_stride_xy: 4, 4" - assert first_lines[10] == "storage_encoding: float64_xy" - assert first_lines[11] == "default_interpolation: bilinear" - assert first_lines[12] == "default_bounds: strict" - assert first_lines[13] == "payload_layout: row_major_interleaved_xy" - assert first_lines[14] == "payload_endianness: little" - assert first_lines[15].startswith("source_model_spec_json: ") - assert not any( - line.startswith("supported_interpolations:") for line in header.splitlines() - ) - assert not any(line.startswith("supported_bounds:") for line in header.splitlines()) - assert not any( - line.startswith("estimated_max_angular_error_") for line in header.splitlines() - ) - assert not any( - line.startswith("estimated_median_angular_error_") for line in header.splitlines() - ) - assert not any(line.startswith("error_report_") for line in header.splitlines()) - assert any(line.startswith("source_model_spec_json:") for line in header.splitlines()) + assert first_lines[4] == "image_size_wh: 17, 13" + assert first_lines[5] == "grid_size_wh: 5, 4" + assert first_lines[6] == "grid_extents_xy: 0, 16, 0, 12" + assert first_lines[7] == "grid_stride_xy: 4, 4" + assert first_lines[8] == "storage_encoding: float64_xy" + assert first_lines[9] == "default_interpolation: bilinear" + assert first_lines[10] == "default_bounds: strict" + assert first_lines[11] == "payload_layout: row_major_interleaved_xy" + assert first_lines[12] == "payload_endianness: little" + assert not any(line.startswith("source_model_spec") for line in header.splitlines()) + assert not any(line.startswith("source_model_type") for line in header.splitlines()) assert int(header_fields["payload_offset_bytes"]) == len(header.encode("ascii")) - assert ( - header_fields["source_model_spec_json_sha256"] - == hashlib.sha256( - header_fields["source_model_spec_json"].encode("ascii") - ).hexdigest() - ) def test_unproject_lut_exposes_serialized_file_metadata(tmp_path: Path) -> None: @@ -211,35 +185,26 @@ def test_unproject_lut_exposes_serialized_file_metadata(tmp_path: Path) -> None: @pytest.mark.parametrize( - ("factory", "expected_model_type"), - [ - (_load_opencv_model, "opencv"), - (_load_spline_model, "pinhole_splined"), - (_load_remapped_model, "pinhole_remapped"), - ], + "factory", + [_load_opencv_model, _load_spline_model], ) def test_unproject_lut_round_trip_for_camera_models( tmp_path: Path, factory, - expected_model_type: str, ) -> None: - """OpenCV, spline, and remapped models survive LUT save/load round trips.""" + """OpenCV and spline models survive LUT save/load round trips.""" model = factory() lut = model.get_unproject_lut( grid_size_wh=(11, 9), storage_encoding="float64_xy", ) - file_path = tmp_path / f"{expected_model_type}.unproject_LUT" + file_path = tmp_path / "round_trip.unproject_LUT" lut.save(file_path) loaded = lb.UnprojectLUT.load(file_path) - assert loaded.source_model_type == expected_model_type assert loaded.grid_size_wh == (11, 9) assert loaded.storage_encoding == "float64_xy" - assert loaded.source_model_spec is not None - assert loaded.source_model_spec["type"] == expected_model_type - assert loaded.source_model_spec_json_sha256 is not None x_coords = np.linspace(0.0, model.image_width - 1, loaded.grid_width) y_coords = np.linspace(0.0, model.image_height - 1, loaded.grid_height) @@ -280,12 +245,12 @@ def test_linear_pinhole_lut_encodings(tmp_path: Path) -> None: def test_unproject_lut_analyzer_can_report_multiple_interpolations() -> None: """The analyzer can report multiple interpolation modes at once.""" - from lensboy.analysis import UnprojectLUTAnalyzer + from lensboy.analysis import estimate_lut_accuracy model = _load_opencv_model() lut = model.get_unproject_lut(grid_size_wh=(7, 6)) - report = UnprojectLUTAnalyzer(lut).estimate_accuracy( - interpolations=("nearest", "bilinear", "bicubic") + report = estimate_lut_accuracy( + lut, model, interpolations=("nearest", "bilinear", "bicubic") ) assert report.interpolations == ("nearest", "bilinear", "bicubic") @@ -303,11 +268,11 @@ def test_unproject_lut_analyzer_can_report_multiple_interpolations() -> None: def test_unproject_lut_analyzer_accepts_single_interpolation() -> None: """A single interpolation mode can be passed directly to the analyzer.""" - from lensboy.analysis import UnprojectLUTAnalyzer + from lensboy.analysis import estimate_lut_accuracy model = _load_opencv_model() lut = model.get_unproject_lut(grid_size_wh=(7, 6)) - report = UnprojectLUTAnalyzer(lut).estimate_accuracy(interpolations="bicubic") + report = estimate_lut_accuracy(lut, model, interpolations="bicubic") assert report.interpolations == ("bicubic",) assert set(report.max_angular_error_mdeg) == {"bicubic"} @@ -318,7 +283,7 @@ def test_unproject_lut_analyzer_accepts_single_interpolation() -> None: def test_unproject_lut_analyzer_matches_loaded_and_in_memory_lut(tmp_path: Path) -> None: """Loaded LUTs produce the same analyzer report as in-memory LUTs.""" - from lensboy.analysis import UnprojectLUTAnalyzer + from lensboy.analysis import estimate_lut_accuracy model = _load_opencv_model() lut = model.get_unproject_lut(grid_size_wh=(7, 6)) @@ -326,11 +291,11 @@ def test_unproject_lut_analyzer_matches_loaded_and_in_memory_lut(tmp_path: Path) lut.save(file_path) loaded = lb.UnprojectLUT.load(file_path) - report_before = UnprojectLUTAnalyzer(lut).estimate_accuracy( - interpolations=("nearest", "bilinear", "bicubic") + report_before = estimate_lut_accuracy( + lut, model, interpolations=("nearest", "bilinear", "bicubic") ) - report_after = UnprojectLUTAnalyzer(loaded).estimate_accuracy( - interpolations=("nearest", "bilinear", "bicubic") + report_after = estimate_lut_accuracy( + loaded, model, interpolations=("nearest", "bilinear", "bicubic") ) assert report_after.interpolations == report_before.interpolations @@ -345,13 +310,12 @@ def test_unproject_lut_analyzer_matches_loaded_and_in_memory_lut(tmp_path: Path) def test_unproject_lut_analyzer_can_sample_dense_accuracy_grid() -> None: """The analyzer can compare LUT rays against exact rays on a dense sample grid.""" - from lensboy.analysis import UnprojectLUTAnalyzer + from lensboy.analysis import sample_lut_accuracy model = _load_opencv_model() lut = model.get_unproject_lut(grid_size_wh=(7, 6)) - sample = UnprojectLUTAnalyzer(lut).sample_accuracy_grid( - interpolation="bilinear", - target_sample_count=2500, + sample = sample_lut_accuracy( + lut, model, interpolation="bilinear", target_sample_count=2500 ) expected_sample_count = sample.sample_grid_width * sample.sample_grid_height @@ -375,65 +339,18 @@ def test_unproject_lut_analyzer_can_sample_dense_accuracy_grid() -> None: def test_unproject_lut_analyzer_dense_accuracy_grid_is_exact_for_linear_model() -> None: """A linear pinhole LUT matches the exact source model on dense sampled queries.""" - from lensboy.analysis import UnprojectLUTAnalyzer + from lensboy.analysis import sample_lut_accuracy model = _make_linear_pinhole_model() lut = model.get_unproject_lut(grid_size_wh=(6, 5)) - sample = UnprojectLUTAnalyzer(lut).sample_accuracy_grid( - interpolation="bilinear", - target_sample_count=2500, + sample = sample_lut_accuracy( + lut, model, interpolation="bilinear", target_sample_count=2500 ) np.testing.assert_allclose(sample.approx_rays, sample.exact_rays, atol=1e-12) assert sample.max_angular_error_mdeg < 1e-2 -def test_unproject_lut_parallel_grid_build_matches_serial() -> None: - """Parallel LUT grid sampling matches the serial build exactly.""" - model = _make_linear_pinhole_model() - serial_lut = model.get_unproject_lut( - grid_size_wh=(17, 13), - num_workers=1, - ) - parallel_lut = model.get_unproject_lut( - grid_size_wh=(17, 13), - num_workers=2, - ) - - np.testing.assert_allclose(parallel_lut.xy_grid, serial_lut.xy_grid, atol=0.0) - assert parallel_lut.grid_size_wh == serial_lut.grid_size_wh - assert parallel_lut.grid_extents_xy == serial_lut.grid_extents_xy - - -def test_unproject_lut_analyzer_is_stable_across_num_workers() -> None: - """Changing LUT build workers does not affect analyzer results.""" - from lensboy.analysis import UnprojectLUTAnalyzer - - model = _load_opencv_model() - serial_lut = model.get_unproject_lut( - grid_size_wh=(7, 6), - num_workers=1, - ) - parallel_lut = model.get_unproject_lut( - grid_size_wh=(7, 6), - num_workers=2, - ) - serial_report = UnprojectLUTAnalyzer(serial_lut).estimate_accuracy( - interpolations=("nearest", "bilinear", "bicubic") - ) - parallel_report = UnprojectLUTAnalyzer(parallel_lut).estimate_accuracy( - interpolations=("nearest", "bilinear", "bicubic") - ) - - for mode in ("nearest", "bilinear", "bicubic"): - assert parallel_report.max_angular_error_mdeg[mode] == pytest.approx( - serial_report.max_angular_error_mdeg[mode] - ) - assert parallel_report.median_angular_error_mdeg[mode] == pytest.approx( - serial_report.median_angular_error_mdeg[mode] - ) - - def test_unproject_lut_grid_stride_can_be_fractional() -> None: """The stored sample spacing reflects the actual grid spacing.""" model = _make_linear_pinhole_model() @@ -526,11 +443,11 @@ def test_unproject_lut_bicubic_falls_back_to_bilinear_without_full_stencil() -> def test_unproject_lut_analyzer_report_is_finite_for_nonlinear_model() -> None: """A nonlinear model produces finite observed angular error summaries.""" - from lensboy.analysis import UnprojectLUTAnalyzer + from lensboy.analysis import estimate_lut_accuracy model = _load_opencv_model() lut = model.get_unproject_lut(grid_size_wh=(7, 6)) - report = UnprojectLUTAnalyzer(lut).estimate_accuracy() + report = estimate_lut_accuracy(lut, model) assert report.interpolations == ("bilinear",) assert np.isfinite(report.max_angular_error_mdeg["bilinear"]) @@ -551,31 +468,6 @@ def test_unproject_lut_analyzer_report_is_finite_for_nonlinear_model() -> None: assert dense_error <= (report.max_angular_error_mdeg["bilinear"] / 1.0e3) + 1.0 -def test_unproject_lut_analyzer_requires_source_model_spec() -> None: - """Analyzer methods fail clearly when the LUT lacks an exact source model spec.""" - from lensboy.analysis import UnprojectLUTAnalyzer - - lut = lb.UnprojectLUT( - image_width=5, - image_height=4, - grid_width=5, - grid_height=4, - grid_x_min=0.0, - grid_x_max=4.0, - grid_y_min=0.0, - grid_y_max=3.0, - storage_encoding="float64_xy", - xy_grid=np.zeros((4, 5, 2), dtype=np.float64), - source_model_type=None, - source_model_spec=None, - ) - - with pytest.raises(ValueError, match="source_model_spec_json"): - UnprojectLUTAnalyzer(lut).estimate_accuracy() - with pytest.raises(ValueError, match="source_model_spec_json"): - UnprojectLUTAnalyzer(lut).sample_accuracy_grid() - - def test_unproject_lut_rejects_wrong_suffix(tmp_path: Path) -> None: """Saving requires the `.unproject_LUT` suffix.""" model = _make_linear_pinhole_model() @@ -621,19 +513,6 @@ def test_unproject_lut_rejects_bad_payload_offset(tmp_path: Path) -> None: lb.UnprojectLUT.load(file_path) -def test_unproject_lut_rejects_bad_source_model_spec_hash(tmp_path: Path) -> None: - """Loading validates the source-model spec hash.""" - model = _make_linear_pinhole_model() - lut = model.get_unproject_lut(grid_size_wh=(4, 4)) - file_path = tmp_path / "bad_hash.unproject_LUT" - lut.save(file_path) - - _rewrite_header_field(file_path, "source_model_spec_json_sha256", "deadbeef") - - with pytest.raises(ValueError, match="source_model_spec_json_sha256"): - lb.UnprojectLUT.load(file_path) - - def test_unproject_lut_rejects_legacy_error_report_header_fields(tmp_path: Path) -> None: """Loading rejects the older mixed runtime-and-analysis header shape.""" model = _make_linear_pinhole_model() @@ -700,14 +579,13 @@ def test_unproject_lut_rejects_nonfinite_payload_values( def test_unproject_lut_analyzer_can_save_and_load_error_heatmaps(tmp_path: Path) -> None: """Analyzer heatmaps can be saved and loaded without the LUT header.""" - from lensboy.analysis import UnprojectLUTAnalyzer, UnprojectLUTErrorHeatmap + from lensboy.analysis import UnprojectLUTErrorHeatmap, compute_lut_error_heatmap model = _make_linear_pinhole_model() lut = model.get_unproject_lut(grid_size_wh=(6, 5)) heatmap_path = tmp_path / "bilinear_error_heatmaps.npz" - analyzer = UnprojectLUTAnalyzer(lut) - heatmap = analyzer.compute_error_heatmap(interpolation="bilinear") + heatmap = compute_lut_error_heatmap(lut, model, interpolation="bilinear") heatmap.save(heatmap_path) loaded = UnprojectLUTErrorHeatmap.load(heatmap_path) @@ -723,14 +601,17 @@ def test_plot_unproject_lut_error_heatmap_supports_angular_units(tmp_path: Path) """The heatmap plot helper rescales angular error into the requested units.""" import matplotlib - from lensboy.analysis import UnprojectLUTAnalyzer, plot_unproject_lut_error_heatmap + from lensboy.analysis import ( + compute_lut_error_heatmap, + plot_unproject_lut_error_heatmap, + ) matplotlib.use("Agg") import matplotlib.pyplot as plt model = _make_linear_pinhole_model() lut = model.get_unproject_lut(grid_size_wh=(6, 5)) - heatmap = UnprojectLUTAnalyzer(lut).compute_error_heatmap(interpolation="bilinear") + heatmap = compute_lut_error_heatmap(lut, model, interpolation="bilinear") expected_mdeg = heatmap.max_angular_error_deg * 1.0e3 fig = plot_unproject_lut_error_heatmap( @@ -755,16 +636,19 @@ def test_plot_unproject_lut_error_heatmap_accepts_figsize(tmp_path: Path) -> Non """The heatmap plot helper forwards the requested figure size.""" import matplotlib - from lensboy.analysis import UnprojectLUTAnalyzer, plot_unproject_lut_error_heatmap + from lensboy.analysis import ( + compute_lut_error_heatmap, + plot_unproject_lut_error_heatmap, + ) matplotlib.use("Agg") import matplotlib.pyplot as plt model = _make_linear_pinhole_model() lut = model.get_unproject_lut(grid_size_wh=(6, 5)) - analyzer = UnprojectLUTAnalyzer(lut) heatmap_path = tmp_path / "bilinear_error_heatmaps_figsize.npz" - analyzer.save_error_heatmap(heatmap_path, interpolation="bilinear") + heatmap = compute_lut_error_heatmap(lut, model, interpolation="bilinear") + heatmap.save(heatmap_path) fig = plot_unproject_lut_error_heatmap( heatmap_path, From fb56c247aaa7df85368aa9b9b78603b77a426470 Mon Sep 17 00:00:00 2001 From: Robertleoj Date: Sun, 12 Apr 2026 21:06:25 +0200 Subject: [PATCH 04/27] wip --- .vscode/settings.json | 1 + cpp_runtime/json.hpp | 25526 +++++++++++++++++ cpp_runtime/npy.hpp | 600 + cpp_runtime/unproject_lut.cpp | 430 +- cpp_runtime/unproject_lut.hpp | 9 +- docs/unproject_lut.md | 43 +- src/lensboy/camera_models/opencv.py | 5 +- src/lensboy/camera_models/pinhole_splined.py | 5 +- src/lensboy/camera_models/unproject_lut.py | 404 +- tests/test_unproject_lut.py | 324 +- 10 files changed, 26329 insertions(+), 1018 deletions(-) create mode 100644 cpp_runtime/json.hpp create mode 100644 cpp_runtime/npy.hpp diff --git a/.vscode/settings.json b/.vscode/settings.json index 695a682..6da60c1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "--clang-tidy", "-j=5" ], + "search.useIgnoreFiles": false, "files.associations": { "*.py": "python", "*.rmd": "markdown", diff --git a/cpp_runtime/json.hpp b/cpp_runtime/json.hpp new file mode 100644 index 0000000..82d69f7 --- /dev/null +++ b/cpp_runtime/json.hpp @@ -0,0 +1,25526 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + +/****************************************************************************\ + * Note on documentation: The source files contain links to the online * + * documentation of the public API at https://json.nlohmann.me. This URL * + * contains the most recent documentation and should also be applicable to * + * previous versions; documentation for deprecated functions is not * + * removed, but marked deprecated. See "Generate documentation" section in * + * file docs/README.md. * +\****************************************************************************/ + +#ifndef INCLUDE_NLOHMANN_JSON_HPP_ +#define INCLUDE_NLOHMANN_JSON_HPP_ + +#include // all_of, find, for_each +#include // nullptr_t, ptrdiff_t, size_t +#include // hash, less +#include // initializer_list +#ifndef JSON_NO_IO + #include // istream, ostream +#endif // JSON_NO_IO +#include // random_access_iterator_tag +#include // unique_ptr +#include // string, stoi, to_string +#include // declval, forward, move, pair, swap +#include // vector + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// This file contains all macro definitions affecting or depending on the ABI + +#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK + #if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH) + #if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 12 || NLOHMANN_JSON_VERSION_PATCH != 0 + #warning "Already included a different version of the library!" + #endif + #endif +#endif + +#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum) +#define NLOHMANN_JSON_VERSION_MINOR 12 // NOLINT(modernize-macro-to-enum) +#define NLOHMANN_JSON_VERSION_PATCH 0 // NOLINT(modernize-macro-to-enum) + +#ifndef JSON_DIAGNOSTICS + #define JSON_DIAGNOSTICS 0 +#endif + +#ifndef JSON_DIAGNOSTIC_POSITIONS + #define JSON_DIAGNOSTIC_POSITIONS 0 +#endif + +#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON + #define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0 +#endif + +#if JSON_DIAGNOSTICS + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag +#else + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS +#endif + +#if JSON_DIAGNOSTIC_POSITIONS + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS _dp +#else + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS +#endif + +#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON + #define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp +#else + #define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION + #define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0 +#endif + +// Construct the namespace ABI tags component +#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) json_abi ## a ## b ## c +#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b, c) \ + NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b, c) + +#define NLOHMANN_JSON_ABI_TAGS \ + NLOHMANN_JSON_ABI_TAGS_CONCAT( \ + NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \ + NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON, \ + NLOHMANN_JSON_ABI_TAG_DIAGNOSTIC_POSITIONS) + +// Construct the namespace version component +#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \ + _v ## major ## _ ## minor ## _ ## patch +#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \ + NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) + +#if NLOHMANN_JSON_NAMESPACE_NO_VERSION +#define NLOHMANN_JSON_NAMESPACE_VERSION +#else +#define NLOHMANN_JSON_NAMESPACE_VERSION \ + NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \ + NLOHMANN_JSON_VERSION_MINOR, \ + NLOHMANN_JSON_VERSION_PATCH) +#endif + +// Combine namespace components +#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b +#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \ + NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) + +#ifndef NLOHMANN_JSON_NAMESPACE +#define NLOHMANN_JSON_NAMESPACE \ + nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \ + NLOHMANN_JSON_ABI_TAGS, \ + NLOHMANN_JSON_NAMESPACE_VERSION) +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN +#define NLOHMANN_JSON_NAMESPACE_BEGIN \ + namespace nlohmann \ + { \ + inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \ + NLOHMANN_JSON_ABI_TAGS, \ + NLOHMANN_JSON_NAMESPACE_VERSION) \ + { +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_END +#define NLOHMANN_JSON_NAMESPACE_END \ + } /* namespace (inline namespace) NOLINT(readability/namespace) */ \ + } // namespace nlohmann +#endif + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // transform +#include // array +#include // forward_list +#include // inserter, front_inserter, end +#include // map +#ifdef JSON_HAS_CPP_17 + #include // optional +#endif +#include // string +#include // tuple, make_tuple +#include // is_arithmetic, is_same, is_enum, underlying_type, is_convertible +#include // unordered_map +#include // pair, declval +#include // valarray + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // nullptr_t +#include // exception +#if JSON_DIAGNOSTICS + #include // accumulate +#endif +#include // runtime_error +#include // to_string +#include // vector + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // array +#include // size_t +#include // uint8_t +#include // string + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // declval, pair +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +template struct make_void +{ + using type = void; +}; +template using void_t = typename make_void::type; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +// https://en.cppreference.com/w/cpp/experimental/is_detected +struct nonesuch +{ + nonesuch() = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const&) = delete; + nonesuch(nonesuch const&&) = delete; + void operator=(nonesuch const&) = delete; + void operator=(nonesuch&&) = delete; +}; + +template class Op, + class... Args> +struct detector +{ + using value_t = std::false_type; + using type = Default; +}; + +template class Op, class... Args> +struct detector>, Op, Args...> +{ + using value_t = std::true_type; + using type = Op; +}; + +template class Op, class... Args> +using is_detected = typename detector::value_t; + +template class Op, class... Args> +struct is_detected_lazy : is_detected { }; + +template class Op, class... Args> +using detected_t = typename detector::type; + +template class Op, class... Args> +using detected_or = detector; + +template class Op, class... Args> +using detected_or_t = typename detected_or::type; + +template class Op, class... Args> +using is_detected_exact = std::is_same>; + +template class Op, class... Args> +using is_detected_convertible = + std::is_convertible, To>; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + + +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-FileCopyrightText: 2016 - 2021 Evan Nemerson +// SPDX-License-Identifier: MIT + +/* Hedley - https://nemequ.github.io/hedley + * Created by Evan Nemerson + */ + +#if !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < 15) +#if defined(JSON_HEDLEY_VERSION) + #undef JSON_HEDLEY_VERSION +#endif +#define JSON_HEDLEY_VERSION 15 + +#if defined(JSON_HEDLEY_STRINGIFY_EX) + #undef JSON_HEDLEY_STRINGIFY_EX +#endif +#define JSON_HEDLEY_STRINGIFY_EX(x) #x + +#if defined(JSON_HEDLEY_STRINGIFY) + #undef JSON_HEDLEY_STRINGIFY +#endif +#define JSON_HEDLEY_STRINGIFY(x) JSON_HEDLEY_STRINGIFY_EX(x) + +#if defined(JSON_HEDLEY_CONCAT_EX) + #undef JSON_HEDLEY_CONCAT_EX +#endif +#define JSON_HEDLEY_CONCAT_EX(a,b) a##b + +#if defined(JSON_HEDLEY_CONCAT) + #undef JSON_HEDLEY_CONCAT +#endif +#define JSON_HEDLEY_CONCAT(a,b) JSON_HEDLEY_CONCAT_EX(a,b) + +#if defined(JSON_HEDLEY_CONCAT3_EX) + #undef JSON_HEDLEY_CONCAT3_EX +#endif +#define JSON_HEDLEY_CONCAT3_EX(a,b,c) a##b##c + +#if defined(JSON_HEDLEY_CONCAT3) + #undef JSON_HEDLEY_CONCAT3 +#endif +#define JSON_HEDLEY_CONCAT3(a,b,c) JSON_HEDLEY_CONCAT3_EX(a,b,c) + +#if defined(JSON_HEDLEY_VERSION_ENCODE) + #undef JSON_HEDLEY_VERSION_ENCODE +#endif +#define JSON_HEDLEY_VERSION_ENCODE(major,minor,revision) (((major) * 1000000) + ((minor) * 1000) + (revision)) + +#if defined(JSON_HEDLEY_VERSION_DECODE_MAJOR) + #undef JSON_HEDLEY_VERSION_DECODE_MAJOR +#endif +#define JSON_HEDLEY_VERSION_DECODE_MAJOR(version) ((version) / 1000000) + +#if defined(JSON_HEDLEY_VERSION_DECODE_MINOR) + #undef JSON_HEDLEY_VERSION_DECODE_MINOR +#endif +#define JSON_HEDLEY_VERSION_DECODE_MINOR(version) (((version) % 1000000) / 1000) + +#if defined(JSON_HEDLEY_VERSION_DECODE_REVISION) + #undef JSON_HEDLEY_VERSION_DECODE_REVISION +#endif +#define JSON_HEDLEY_VERSION_DECODE_REVISION(version) ((version) % 1000) + +#if defined(JSON_HEDLEY_GNUC_VERSION) + #undef JSON_HEDLEY_GNUC_VERSION +#endif +#if defined(__GNUC__) && defined(__GNUC_PATCHLEVEL__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#elif defined(__GNUC__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, 0) +#endif + +#if defined(JSON_HEDLEY_GNUC_VERSION_CHECK) + #undef JSON_HEDLEY_GNUC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GNUC_VERSION) + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GNUC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_MSVC_VERSION) + #undef JSON_HEDLEY_MSVC_VERSION +#endif +#if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 140000000) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 10000000, (_MSC_FULL_VER % 10000000) / 100000, (_MSC_FULL_VER % 100000) / 100) +#elif defined(_MSC_FULL_VER) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 1000000, (_MSC_FULL_VER % 1000000) / 10000, (_MSC_FULL_VER % 10000) / 10) +#elif defined(_MSC_VER) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_VER / 100, _MSC_VER % 100, 0) +#endif + +#if defined(JSON_HEDLEY_MSVC_VERSION_CHECK) + #undef JSON_HEDLEY_MSVC_VERSION_CHECK +#endif +#if !defined(JSON_HEDLEY_MSVC_VERSION) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (0) +#elif defined(_MSC_VER) && (_MSC_VER >= 1400) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 10000000) + (minor * 100000) + (patch))) +#elif defined(_MSC_VER) && (_MSC_VER >= 1200) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 1000000) + (minor * 10000) + (patch))) +#else + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_VER >= ((major * 100) + (minor))) +#endif + +#if defined(JSON_HEDLEY_INTEL_VERSION) + #undef JSON_HEDLEY_INTEL_VERSION +#endif +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && !defined(__ICL) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, __INTEL_COMPILER_UPDATE) +#elif defined(__INTEL_COMPILER) && !defined(__ICL) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0) +#endif + +#if defined(JSON_HEDLEY_INTEL_VERSION_CHECK) + #undef JSON_HEDLEY_INTEL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_INTEL_VERSION) + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_INTEL_CL_VERSION) + #undef JSON_HEDLEY_INTEL_CL_VERSION +#endif +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && defined(__ICL) + #define JSON_HEDLEY_INTEL_CL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER, __INTEL_COMPILER_UPDATE, 0) +#endif + +#if defined(JSON_HEDLEY_INTEL_CL_VERSION_CHECK) + #undef JSON_HEDLEY_INTEL_CL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_INTEL_CL_VERSION) + #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_CL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_PGI_VERSION) + #undef JSON_HEDLEY_PGI_VERSION +#endif +#if defined(__PGI) && defined(__PGIC__) && defined(__PGIC_MINOR__) && defined(__PGIC_PATCHLEVEL__) + #define JSON_HEDLEY_PGI_VERSION JSON_HEDLEY_VERSION_ENCODE(__PGIC__, __PGIC_MINOR__, __PGIC_PATCHLEVEL__) +#endif + +#if defined(JSON_HEDLEY_PGI_VERSION_CHECK) + #undef JSON_HEDLEY_PGI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PGI_VERSION) + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PGI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #undef JSON_HEDLEY_SUNPRO_VERSION +#endif +#if defined(__SUNPRO_C) && (__SUNPRO_C > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_C >> 16) & 0xf) * 10) + ((__SUNPRO_C >> 12) & 0xf), (((__SUNPRO_C >> 8) & 0xf) * 10) + ((__SUNPRO_C >> 4) & 0xf), (__SUNPRO_C & 0xf) * 10) +#elif defined(__SUNPRO_C) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_C >> 8) & 0xf, (__SUNPRO_C >> 4) & 0xf, (__SUNPRO_C) & 0xf) +#elif defined(__SUNPRO_CC) && (__SUNPRO_CC > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_CC >> 16) & 0xf) * 10) + ((__SUNPRO_CC >> 12) & 0xf), (((__SUNPRO_CC >> 8) & 0xf) * 10) + ((__SUNPRO_CC >> 4) & 0xf), (__SUNPRO_CC & 0xf) * 10) +#elif defined(__SUNPRO_CC) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_CC >> 8) & 0xf, (__SUNPRO_CC >> 4) & 0xf, (__SUNPRO_CC) & 0xf) +#endif + +#if defined(JSON_HEDLEY_SUNPRO_VERSION_CHECK) + #undef JSON_HEDLEY_SUNPRO_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_SUNPRO_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION +#endif +#if defined(__EMSCRIPTEN__) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION JSON_HEDLEY_VERSION_ENCODE(__EMSCRIPTEN_major__, __EMSCRIPTEN_minor__, __EMSCRIPTEN_tiny__) +#endif + +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_EMSCRIPTEN_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_ARM_VERSION) + #undef JSON_HEDLEY_ARM_VERSION +#endif +#if defined(__CC_ARM) && defined(__ARMCOMPILER_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCOMPILER_VERSION / 1000000, (__ARMCOMPILER_VERSION % 1000000) / 10000, (__ARMCOMPILER_VERSION % 10000) / 100) +#elif defined(__CC_ARM) && defined(__ARMCC_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCC_VERSION / 1000000, (__ARMCC_VERSION % 1000000) / 10000, (__ARMCC_VERSION % 10000) / 100) +#endif + +#if defined(JSON_HEDLEY_ARM_VERSION_CHECK) + #undef JSON_HEDLEY_ARM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_ARM_VERSION) + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_ARM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_IBM_VERSION) + #undef JSON_HEDLEY_IBM_VERSION +#endif +#if defined(__ibmxl__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ibmxl_version__, __ibmxl_release__, __ibmxl_modification__) +#elif defined(__xlC__) && defined(__xlC_ver__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, (__xlC_ver__ >> 8) & 0xff) +#elif defined(__xlC__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, 0) +#endif + +#if defined(JSON_HEDLEY_IBM_VERSION_CHECK) + #undef JSON_HEDLEY_IBM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IBM_VERSION) + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IBM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_VERSION) + #undef JSON_HEDLEY_TI_VERSION +#endif +#if \ + defined(__TI_COMPILER_VERSION__) && \ + ( \ + defined(__TMS470__) || defined(__TI_ARM__) || \ + defined(__MSP430__) || \ + defined(__TMS320C2000__) \ + ) +#if (__TI_COMPILER_VERSION__ >= 16000000) + #define JSON_HEDLEY_TI_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif +#endif + +#if defined(JSON_HEDLEY_TI_VERSION_CHECK) + #undef JSON_HEDLEY_TI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_VERSION) + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #undef JSON_HEDLEY_TI_CL2000_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C2000__) + #define JSON_HEDLEY_TI_CL2000_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL2000_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL2000_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL2000_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #undef JSON_HEDLEY_TI_CL430_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__MSP430__) + #define JSON_HEDLEY_TI_CL430_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL430_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL430_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL430_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #undef JSON_HEDLEY_TI_ARMCL_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && (defined(__TMS470__) || defined(__TI_ARM__)) + #define JSON_HEDLEY_TI_ARMCL_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION_CHECK) + #undef JSON_HEDLEY_TI_ARMCL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_ARMCL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #undef JSON_HEDLEY_TI_CL6X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C6X__) + #define JSON_HEDLEY_TI_CL6X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL6X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL6X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL6X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #undef JSON_HEDLEY_TI_CL7X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__C7000__) + #define JSON_HEDLEY_TI_CL7X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL7X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL7X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL7X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #undef JSON_HEDLEY_TI_CLPRU_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__PRU__) + #define JSON_HEDLEY_TI_CLPRU_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CLPRU_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CLPRU_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_CRAY_VERSION) + #undef JSON_HEDLEY_CRAY_VERSION +#endif +#if defined(_CRAYC) + #if defined(_RELEASE_PATCHLEVEL) + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, _RELEASE_PATCHLEVEL) + #else + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, 0) + #endif +#endif + +#if defined(JSON_HEDLEY_CRAY_VERSION_CHECK) + #undef JSON_HEDLEY_CRAY_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_CRAY_VERSION) + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_CRAY_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_IAR_VERSION) + #undef JSON_HEDLEY_IAR_VERSION +#endif +#if defined(__IAR_SYSTEMS_ICC__) + #if __VER__ > 1000 + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE((__VER__ / 1000000), ((__VER__ / 1000) % 1000), (__VER__ % 1000)) + #else + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE(__VER__ / 100, __VER__ % 100, 0) + #endif +#endif + +#if defined(JSON_HEDLEY_IAR_VERSION_CHECK) + #undef JSON_HEDLEY_IAR_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IAR_VERSION) + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IAR_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TINYC_VERSION) + #undef JSON_HEDLEY_TINYC_VERSION +#endif +#if defined(__TINYC__) + #define JSON_HEDLEY_TINYC_VERSION JSON_HEDLEY_VERSION_ENCODE(__TINYC__ / 1000, (__TINYC__ / 100) % 10, __TINYC__ % 100) +#endif + +#if defined(JSON_HEDLEY_TINYC_VERSION_CHECK) + #undef JSON_HEDLEY_TINYC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TINYC_VERSION) + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TINYC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_DMC_VERSION) + #undef JSON_HEDLEY_DMC_VERSION +#endif +#if defined(__DMC__) + #define JSON_HEDLEY_DMC_VERSION JSON_HEDLEY_VERSION_ENCODE(__DMC__ >> 8, (__DMC__ >> 4) & 0xf, __DMC__ & 0xf) +#endif + +#if defined(JSON_HEDLEY_DMC_VERSION_CHECK) + #undef JSON_HEDLEY_DMC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_DMC_VERSION) + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_DMC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #undef JSON_HEDLEY_COMPCERT_VERSION +#endif +#if defined(__COMPCERT_VERSION__) + #define JSON_HEDLEY_COMPCERT_VERSION JSON_HEDLEY_VERSION_ENCODE(__COMPCERT_VERSION__ / 10000, (__COMPCERT_VERSION__ / 100) % 100, __COMPCERT_VERSION__ % 100) +#endif + +#if defined(JSON_HEDLEY_COMPCERT_VERSION_CHECK) + #undef JSON_HEDLEY_COMPCERT_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_COMPCERT_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_PELLES_VERSION) + #undef JSON_HEDLEY_PELLES_VERSION +#endif +#if defined(__POCC__) + #define JSON_HEDLEY_PELLES_VERSION JSON_HEDLEY_VERSION_ENCODE(__POCC__ / 100, __POCC__ % 100, 0) +#endif + +#if defined(JSON_HEDLEY_PELLES_VERSION_CHECK) + #undef JSON_HEDLEY_PELLES_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PELLES_VERSION) + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PELLES_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_MCST_LCC_VERSION) + #undef JSON_HEDLEY_MCST_LCC_VERSION +#endif +#if defined(__LCC__) && defined(__LCC_MINOR__) + #define JSON_HEDLEY_MCST_LCC_VERSION JSON_HEDLEY_VERSION_ENCODE(__LCC__ / 100, __LCC__ % 100, __LCC_MINOR__) +#endif + +#if defined(JSON_HEDLEY_MCST_LCC_VERSION_CHECK) + #undef JSON_HEDLEY_MCST_LCC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_MCST_LCC_VERSION) + #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_MCST_LCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_GCC_VERSION) + #undef JSON_HEDLEY_GCC_VERSION +#endif +#if \ + defined(JSON_HEDLEY_GNUC_VERSION) && \ + !defined(__clang__) && \ + !defined(JSON_HEDLEY_INTEL_VERSION) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_ARM_VERSION) && \ + !defined(JSON_HEDLEY_CRAY_VERSION) && \ + !defined(JSON_HEDLEY_TI_VERSION) && \ + !defined(JSON_HEDLEY_TI_ARMCL_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL430_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL2000_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL6X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL7X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CLPRU_VERSION) && \ + !defined(__COMPCERT__) && \ + !defined(JSON_HEDLEY_MCST_LCC_VERSION) + #define JSON_HEDLEY_GCC_VERSION JSON_HEDLEY_GNUC_VERSION +#endif + +#if defined(JSON_HEDLEY_GCC_VERSION_CHECK) + #undef JSON_HEDLEY_GCC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GCC_VERSION) + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_ATTRIBUTE +#endif +#if \ + defined(__has_attribute) && \ + ( \ + (!defined(JSON_HEDLEY_IAR_VERSION) || JSON_HEDLEY_IAR_VERSION_CHECK(8,5,9)) \ + ) +# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) __has_attribute(attribute) +#else +# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE +#endif +#if \ + defined(__has_cpp_attribute) && \ + defined(__cplusplus) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS +#endif +#if !defined(__cplusplus) || !defined(__has_cpp_attribute) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#elif \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_IAR_VERSION) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) && \ + (!defined(JSON_HEDLEY_MSVC_VERSION) || JSON_HEDLEY_MSVC_VERSION_CHECK(19,20,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(ns::attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_BUILTIN) + #undef JSON_HEDLEY_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_HAS_BUILTIN(builtin) __has_builtin(builtin) +#else + #define JSON_HEDLEY_HAS_BUILTIN(builtin) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_BUILTIN) + #undef JSON_HEDLEY_GNUC_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) +#else + #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_BUILTIN) + #undef JSON_HEDLEY_GCC_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) +#else + #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_FEATURE) + #undef JSON_HEDLEY_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_HAS_FEATURE(feature) __has_feature(feature) +#else + #define JSON_HEDLEY_HAS_FEATURE(feature) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_FEATURE) + #undef JSON_HEDLEY_GNUC_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) +#else + #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_FEATURE) + #undef JSON_HEDLEY_GCC_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) +#else + #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_EXTENSION) + #undef JSON_HEDLEY_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_HAS_EXTENSION(extension) __has_extension(extension) +#else + #define JSON_HEDLEY_HAS_EXTENSION(extension) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_EXTENSION) + #undef JSON_HEDLEY_GNUC_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) +#else + #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_EXTENSION) + #undef JSON_HEDLEY_GCC_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) +#else + #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_WARNING) + #undef JSON_HEDLEY_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_HAS_WARNING(warning) __has_warning(warning) +#else + #define JSON_HEDLEY_HAS_WARNING(warning) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_WARNING) + #undef JSON_HEDLEY_GNUC_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) +#else + #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_WARNING) + #undef JSON_HEDLEY_GCC_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) +#else + #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + defined(__clang__) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,17) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(8,0,0) || \ + (JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) && defined(__C99_PRAGMA_OPERATOR)) + #define JSON_HEDLEY_PRAGMA(value) _Pragma(#value) +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_PRAGMA(value) __pragma(value) +#else + #define JSON_HEDLEY_PRAGMA(value) +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_PUSH) + #undef JSON_HEDLEY_DIAGNOSTIC_PUSH +#endif +#if defined(JSON_HEDLEY_DIAGNOSTIC_POP) + #undef JSON_HEDLEY_DIAGNOSTIC_POP +#endif +#if defined(__clang__) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("clang diagnostic push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("clang diagnostic pop") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH __pragma(warning(push)) + #define JSON_HEDLEY_DIAGNOSTIC_POP __pragma(warning(pop)) +#elif JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("pop") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,4,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("diag_push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("diag_pop") +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") +#else + #define JSON_HEDLEY_DIAGNOSTIC_PUSH + #define JSON_HEDLEY_DIAGNOSTIC_POP +#endif + +/* JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ is for + HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ +#endif +#if defined(__cplusplus) +# if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat") +# if JSON_HEDLEY_HAS_WARNING("-Wc++17-extensions") +# if JSON_HEDLEY_HAS_WARNING("-Wc++1z-extensions") +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \ + _Pragma("clang diagnostic ignored \"-Wc++1z-extensions\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# endif +# else +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# endif +# endif +#endif +#if !defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(x) x +#endif + +#if defined(JSON_HEDLEY_CONST_CAST) + #undef JSON_HEDLEY_CONST_CAST +#endif +#if defined(__cplusplus) +# define JSON_HEDLEY_CONST_CAST(T, expr) (const_cast(expr)) +#elif \ + JSON_HEDLEY_HAS_WARNING("-Wcast-qual") || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_CONST_CAST(T, expr) (__extension__ ({ \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL \ + ((T) (expr)); \ + JSON_HEDLEY_DIAGNOSTIC_POP \ + })) +#else +# define JSON_HEDLEY_CONST_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_REINTERPRET_CAST) + #undef JSON_HEDLEY_REINTERPRET_CAST +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) (reinterpret_cast(expr)) +#else + #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_STATIC_CAST) + #undef JSON_HEDLEY_STATIC_CAST +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_STATIC_CAST(T, expr) (static_cast(expr)) +#else + #define JSON_HEDLEY_STATIC_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_CPP_CAST) + #undef JSON_HEDLEY_CPP_CAST +#endif +#if defined(__cplusplus) +# if JSON_HEDLEY_HAS_WARNING("-Wold-style-cast") +# define JSON_HEDLEY_CPP_CAST(T, expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wold-style-cast\"") \ + ((T) (expr)) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# elif JSON_HEDLEY_IAR_VERSION_CHECK(8,3,0) +# define JSON_HEDLEY_CPP_CAST(T, expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("diag_suppress=Pe137") \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_CPP_CAST(T, expr) ((T) (expr)) +# endif +#else +# define JSON_HEDLEY_CPP_CAST(T, expr) (expr) +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wdeprecated-declarations") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warning(disable:1478 1786)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:1478 1786)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(20,7,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1216,1444,1445") +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:4996)) +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1291,1718") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && !defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,E_DEPRECATED_ATT,E_DEPRECATED_ATT_MESS)") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,symdeprecated,symdeprecated2)") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress=Pe1444,Pe1215") +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warn(disable:2241)") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("clang diagnostic ignored \"-Wunknown-pragmas\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("warning(disable:161)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:161)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 1675") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("GCC diagnostic ignored \"-Wunknown-pragmas\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:4068)) +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(16,9,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress=Pe161") +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 161") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-attributes") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("clang diagnostic ignored \"-Wunknown-attributes\"") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("warning(disable:1292)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:1292)) +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:5030)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(20,7,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097,1098") +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("error_messages(off,attrskipunsup)") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1173") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress=Pe1097") +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wcast-qual") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("clang diagnostic ignored \"-Wcast-qual\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("warning(disable:2203 2331)") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("GCC diagnostic ignored \"-Wcast-qual\"") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunused-function") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("clang diagnostic ignored \"-Wunused-function\"") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("GCC diagnostic ignored \"-Wunused-function\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(1,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION __pragma(warning(disable:4505)) +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("diag_suppress 3142") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION +#endif + +#if defined(JSON_HEDLEY_DEPRECATED) + #undef JSON_HEDLEY_DEPRECATED +#endif +#if defined(JSON_HEDLEY_DEPRECATED_FOR) + #undef JSON_HEDLEY_DEPRECATED_FOR +#endif +#if \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated("Since " # since)) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated("Since " #since "; use " #replacement)) +#elif \ + (JSON_HEDLEY_HAS_EXTENSION(attribute_deprecated_with_message) && !defined(JSON_HEDLEY_IAR_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__("Since " #since))) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__("Since " #since "; use " #replacement))) +#elif defined(__cplusplus) && (__cplusplus >= 201402L) + #define JSON_HEDLEY_DEPRECATED(since) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since)]]) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since "; use " #replacement)]]) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(deprecated) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__)) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_PELLES_VERSION_CHECK(6,50,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated) +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DEPRECATED(since) _Pragma("deprecated") + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) _Pragma("deprecated") +#else + #define JSON_HEDLEY_DEPRECATED(since) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) +#endif + +#if defined(JSON_HEDLEY_UNAVAILABLE) + #undef JSON_HEDLEY_UNAVAILABLE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(warning) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_UNAVAILABLE(available_since) __attribute__((__warning__("Not available until " #available_since))) +#else + #define JSON_HEDLEY_UNAVAILABLE(available_since) +#endif + +#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT) + #undef JSON_HEDLEY_WARN_UNUSED_RESULT +#endif +#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT_MSG) + #undef JSON_HEDLEY_WARN_UNUSED_RESULT_MSG +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(warn_unused_result) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_WARN_UNUSED_RESULT __attribute__((__warn_unused_result__)) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) __attribute__((__warn_unused_result__)) +#elif (JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) >= 201907L) + #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard(msg)]]) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) + #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) +#elif defined(_Check_return_) /* SAL */ + #define JSON_HEDLEY_WARN_UNUSED_RESULT _Check_return_ + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) _Check_return_ +#else + #define JSON_HEDLEY_WARN_UNUSED_RESULT + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) +#endif + +#if defined(JSON_HEDLEY_SENTINEL) + #undef JSON_HEDLEY_SENTINEL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(sentinel) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_SENTINEL(position) __attribute__((__sentinel__(position))) +#else + #define JSON_HEDLEY_SENTINEL(position) +#endif + +#if defined(JSON_HEDLEY_NO_RETURN) + #undef JSON_HEDLEY_NO_RETURN +#endif +#if JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_NO_RETURN __noreturn +#elif \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) +#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L + #define JSON_HEDLEY_NO_RETURN _Noreturn +#elif defined(__cplusplus) && (__cplusplus >= 201103L) + #define JSON_HEDLEY_NO_RETURN JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[noreturn]]) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(noreturn) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,2,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_NO_RETURN _Pragma("does_not_return") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) + #define JSON_HEDLEY_NO_RETURN _Pragma("FUNC_NEVER_RETURNS;") +#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) + #define JSON_HEDLEY_NO_RETURN __attribute((noreturn)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) + #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) +#else + #define JSON_HEDLEY_NO_RETURN +#endif + +#if defined(JSON_HEDLEY_NO_ESCAPE) + #undef JSON_HEDLEY_NO_ESCAPE +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(noescape) + #define JSON_HEDLEY_NO_ESCAPE __attribute__((__noescape__)) +#else + #define JSON_HEDLEY_NO_ESCAPE +#endif + +#if defined(JSON_HEDLEY_UNREACHABLE) + #undef JSON_HEDLEY_UNREACHABLE +#endif +#if defined(JSON_HEDLEY_UNREACHABLE_RETURN) + #undef JSON_HEDLEY_UNREACHABLE_RETURN +#endif +#if defined(JSON_HEDLEY_ASSUME) + #undef JSON_HEDLEY_ASSUME +#endif +#if \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_ASSUME(expr) __assume(expr) +#elif JSON_HEDLEY_HAS_BUILTIN(__builtin_assume) + #define JSON_HEDLEY_ASSUME(expr) __builtin_assume(expr) +#elif \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) + #if defined(__cplusplus) + #define JSON_HEDLEY_ASSUME(expr) std::_nassert(expr) + #else + #define JSON_HEDLEY_ASSUME(expr) _nassert(expr) + #endif +#endif +#if \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_unreachable) && (!defined(JSON_HEDLEY_ARM_VERSION))) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,10,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,5) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(10,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_UNREACHABLE() __builtin_unreachable() +#elif defined(JSON_HEDLEY_ASSUME) + #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) +#endif +#if !defined(JSON_HEDLEY_ASSUME) + #if defined(JSON_HEDLEY_UNREACHABLE) + #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, ((expr) ? 1 : (JSON_HEDLEY_UNREACHABLE(), 1))) + #else + #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, expr) + #endif +#endif +#if defined(JSON_HEDLEY_UNREACHABLE) + #if \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (JSON_HEDLEY_STATIC_CAST(void, JSON_HEDLEY_ASSUME(0)), (value)) + #else + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) JSON_HEDLEY_UNREACHABLE() + #endif +#else + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (value) +#endif +#if !defined(JSON_HEDLEY_UNREACHABLE) + #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) +#endif + +JSON_HEDLEY_DIAGNOSTIC_PUSH +#if JSON_HEDLEY_HAS_WARNING("-Wpedantic") + #pragma clang diagnostic ignored "-Wpedantic" +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat-pedantic") && defined(__cplusplus) + #pragma clang diagnostic ignored "-Wc++98-compat-pedantic" +#endif +#if JSON_HEDLEY_GCC_HAS_WARNING("-Wvariadic-macros",4,0,0) + #if defined(__clang__) + #pragma clang diagnostic ignored "-Wvariadic-macros" + #elif defined(JSON_HEDLEY_GCC_VERSION) + #pragma GCC diagnostic ignored "-Wvariadic-macros" + #endif +#endif +#if defined(JSON_HEDLEY_NON_NULL) + #undef JSON_HEDLEY_NON_NULL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(nonnull) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) + #define JSON_HEDLEY_NON_NULL(...) __attribute__((__nonnull__(__VA_ARGS__))) +#else + #define JSON_HEDLEY_NON_NULL(...) +#endif +JSON_HEDLEY_DIAGNOSTIC_POP + +#if defined(JSON_HEDLEY_PRINTF_FORMAT) + #undef JSON_HEDLEY_PRINTF_FORMAT +#endif +#if defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && !defined(__USE_MINGW_ANSI_STDIO) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(ms_printf, string_idx, first_to_check))) +#elif defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && defined(__USE_MINGW_ANSI_STDIO) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(gnu_printf, string_idx, first_to_check))) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(format) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(__printf__, string_idx, first_to_check))) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(6,0,0) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __declspec(vaformat(printf,string_idx,first_to_check)) +#else + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) +#endif + +#if defined(JSON_HEDLEY_CONSTEXPR) + #undef JSON_HEDLEY_CONSTEXPR +#endif +#if defined(__cplusplus) + #if __cplusplus >= 201103L + #define JSON_HEDLEY_CONSTEXPR JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(constexpr) + #endif +#endif +#if !defined(JSON_HEDLEY_CONSTEXPR) + #define JSON_HEDLEY_CONSTEXPR +#endif + +#if defined(JSON_HEDLEY_PREDICT) + #undef JSON_HEDLEY_PREDICT +#endif +#if defined(JSON_HEDLEY_LIKELY) + #undef JSON_HEDLEY_LIKELY +#endif +#if defined(JSON_HEDLEY_UNLIKELY) + #undef JSON_HEDLEY_UNLIKELY +#endif +#if defined(JSON_HEDLEY_UNPREDICTABLE) + #undef JSON_HEDLEY_UNPREDICTABLE +#endif +#if JSON_HEDLEY_HAS_BUILTIN(__builtin_unpredictable) + #define JSON_HEDLEY_UNPREDICTABLE(expr) __builtin_unpredictable((expr)) +#endif +#if \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_expect_with_probability) && !defined(JSON_HEDLEY_PGI_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(9,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PREDICT(expr, value, probability) __builtin_expect_with_probability( (expr), (value), (probability)) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) __builtin_expect_with_probability(!!(expr), 1 , (probability)) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) __builtin_expect_with_probability(!!(expr), 0 , (probability)) +# define JSON_HEDLEY_LIKELY(expr) __builtin_expect (!!(expr), 1 ) +# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect (!!(expr), 0 ) +#elif \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_expect) && !defined(JSON_HEDLEY_INTEL_CL_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,27) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PREDICT(expr, expected, probability) \ + (((probability) >= 0.9) ? __builtin_expect((expr), (expected)) : (JSON_HEDLEY_STATIC_CAST(void, expected), (expr))) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) \ + (__extension__ ({ \ + double hedley_probability_ = (probability); \ + ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 1) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 0) : !!(expr))); \ + })) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) \ + (__extension__ ({ \ + double hedley_probability_ = (probability); \ + ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 0) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 1) : !!(expr))); \ + })) +# define JSON_HEDLEY_LIKELY(expr) __builtin_expect(!!(expr), 1) +# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect(!!(expr), 0) +#else +# define JSON_HEDLEY_PREDICT(expr, expected, probability) (JSON_HEDLEY_STATIC_CAST(void, expected), (expr)) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) (!!(expr)) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) (!!(expr)) +# define JSON_HEDLEY_LIKELY(expr) (!!(expr)) +# define JSON_HEDLEY_UNLIKELY(expr) (!!(expr)) +#endif +#if !defined(JSON_HEDLEY_UNPREDICTABLE) + #define JSON_HEDLEY_UNPREDICTABLE(expr) JSON_HEDLEY_PREDICT(expr, 1, 0.5) +#endif + +#if defined(JSON_HEDLEY_MALLOC) + #undef JSON_HEDLEY_MALLOC +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(malloc) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_MALLOC __attribute__((__malloc__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_MALLOC _Pragma("returns_new_memory") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_MALLOC __declspec(restrict) +#else + #define JSON_HEDLEY_MALLOC +#endif + +#if defined(JSON_HEDLEY_PURE) + #undef JSON_HEDLEY_PURE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(pure) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(2,96,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PURE __attribute__((__pure__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) +# define JSON_HEDLEY_PURE _Pragma("does_not_write_global_data") +#elif defined(__cplusplus) && \ + ( \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) \ + ) +# define JSON_HEDLEY_PURE _Pragma("FUNC_IS_PURE;") +#else +# define JSON_HEDLEY_PURE +#endif + +#if defined(JSON_HEDLEY_CONST) + #undef JSON_HEDLEY_CONST +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(const) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(2,5,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_CONST __attribute__((__const__)) +#elif \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_CONST _Pragma("no_side_effect") +#else + #define JSON_HEDLEY_CONST JSON_HEDLEY_PURE +#endif + +#if defined(JSON_HEDLEY_RESTRICT) + #undef JSON_HEDLEY_RESTRICT +#endif +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && !defined(__cplusplus) + #define JSON_HEDLEY_RESTRICT restrict +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,4) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ + defined(__clang__) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_RESTRICT __restrict +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,3,0) && !defined(__cplusplus) + #define JSON_HEDLEY_RESTRICT _Restrict +#else + #define JSON_HEDLEY_RESTRICT +#endif + +#if defined(JSON_HEDLEY_INLINE) + #undef JSON_HEDLEY_INLINE +#endif +#if \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + (defined(__cplusplus) && (__cplusplus >= 199711L)) + #define JSON_HEDLEY_INLINE inline +#elif \ + defined(JSON_HEDLEY_GCC_VERSION) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(6,2,0) + #define JSON_HEDLEY_INLINE __inline__ +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,1,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_INLINE __inline +#else + #define JSON_HEDLEY_INLINE +#endif + +#if defined(JSON_HEDLEY_ALWAYS_INLINE) + #undef JSON_HEDLEY_ALWAYS_INLINE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(always_inline) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) +# define JSON_HEDLEY_ALWAYS_INLINE __attribute__((__always_inline__)) JSON_HEDLEY_INLINE +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_ALWAYS_INLINE __forceinline +#elif defined(__cplusplus) && \ + ( \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) \ + ) +# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("FUNC_ALWAYS_INLINE;") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) +# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("inline=forced") +#else +# define JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_INLINE +#endif + +#if defined(JSON_HEDLEY_NEVER_INLINE) + #undef JSON_HEDLEY_NEVER_INLINE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(noinline) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_NEVER_INLINE __attribute__((__noinline__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(10,2,0) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("noinline") +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("FUNC_CANNOT_INLINE;") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("inline=never") +#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) + #define JSON_HEDLEY_NEVER_INLINE __attribute((noinline)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) + #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) +#else + #define JSON_HEDLEY_NEVER_INLINE +#endif + +#if defined(JSON_HEDLEY_PRIVATE) + #undef JSON_HEDLEY_PRIVATE +#endif +#if defined(JSON_HEDLEY_PUBLIC) + #undef JSON_HEDLEY_PUBLIC +#endif +#if defined(JSON_HEDLEY_IMPORT) + #undef JSON_HEDLEY_IMPORT +#endif +#if defined(_WIN32) || defined(__CYGWIN__) +# define JSON_HEDLEY_PRIVATE +# define JSON_HEDLEY_PUBLIC __declspec(dllexport) +# define JSON_HEDLEY_IMPORT __declspec(dllimport) +#else +# if \ + JSON_HEDLEY_HAS_ATTRIBUTE(visibility) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + ( \ + defined(__TI_EABI__) && \ + ( \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) \ + ) \ + ) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PRIVATE __attribute__((__visibility__("hidden"))) +# define JSON_HEDLEY_PUBLIC __attribute__((__visibility__("default"))) +# else +# define JSON_HEDLEY_PRIVATE +# define JSON_HEDLEY_PUBLIC +# endif +# define JSON_HEDLEY_IMPORT extern +#endif + +#if defined(JSON_HEDLEY_NO_THROW) + #undef JSON_HEDLEY_NO_THROW +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(nothrow) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_NO_THROW __attribute__((__nothrow__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) + #define JSON_HEDLEY_NO_THROW __declspec(nothrow) +#else + #define JSON_HEDLEY_NO_THROW +#endif + +#if defined(JSON_HEDLEY_FALL_THROUGH) + #undef JSON_HEDLEY_FALL_THROUGH +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(fallthrough) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(7,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_FALL_THROUGH __attribute__((__fallthrough__)) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(clang,fallthrough) + #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[clang::fallthrough]]) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(fallthrough) + #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[fallthrough]]) +#elif defined(__fallthrough) /* SAL */ + #define JSON_HEDLEY_FALL_THROUGH __fallthrough +#else + #define JSON_HEDLEY_FALL_THROUGH +#endif + +#if defined(JSON_HEDLEY_RETURNS_NON_NULL) + #undef JSON_HEDLEY_RETURNS_NON_NULL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(returns_nonnull) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_RETURNS_NON_NULL __attribute__((__returns_nonnull__)) +#elif defined(_Ret_notnull_) /* SAL */ + #define JSON_HEDLEY_RETURNS_NON_NULL _Ret_notnull_ +#else + #define JSON_HEDLEY_RETURNS_NON_NULL +#endif + +#if defined(JSON_HEDLEY_ARRAY_PARAM) + #undef JSON_HEDLEY_ARRAY_PARAM +#endif +#if \ + defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && \ + !defined(__STDC_NO_VLA__) && \ + !defined(__cplusplus) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_TINYC_VERSION) + #define JSON_HEDLEY_ARRAY_PARAM(name) (name) +#else + #define JSON_HEDLEY_ARRAY_PARAM(name) +#endif + +#if defined(JSON_HEDLEY_IS_CONSTANT) + #undef JSON_HEDLEY_IS_CONSTANT +#endif +#if defined(JSON_HEDLEY_REQUIRE_CONSTEXPR) + #undef JSON_HEDLEY_REQUIRE_CONSTEXPR +#endif +/* JSON_HEDLEY_IS_CONSTEXPR_ is for + HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ +#if defined(JSON_HEDLEY_IS_CONSTEXPR_) + #undef JSON_HEDLEY_IS_CONSTEXPR_ +#endif +#if \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_constant_p) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,19) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) && !defined(__cplusplus)) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_IS_CONSTANT(expr) __builtin_constant_p(expr) +#endif +#if !defined(__cplusplus) +# if \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_types_compatible_p) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,24) +#if defined(__INTPTR_TYPE__) + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0)), int*) +#else + #include + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((intptr_t) ((expr) * 0)) : (int*) 0)), int*) +#endif +# elif \ + ( \ + defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && \ + !defined(JSON_HEDLEY_SUNPRO_VERSION) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_IAR_VERSION)) || \ + (JSON_HEDLEY_HAS_EXTENSION(c_generic_selections) && !defined(JSON_HEDLEY_IAR_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,3,0) +#if defined(__INTPTR_TYPE__) + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0), int*: 1, void*: 0) +#else + #include + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((intptr_t) * 0) : (int*) 0), int*: 1, void*: 0) +#endif +# elif \ + defined(JSON_HEDLEY_GCC_VERSION) || \ + defined(JSON_HEDLEY_INTEL_VERSION) || \ + defined(JSON_HEDLEY_TINYC_VERSION) || \ + defined(JSON_HEDLEY_TI_ARMCL_VERSION) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(18,12,0) || \ + defined(JSON_HEDLEY_TI_CL2000_VERSION) || \ + defined(JSON_HEDLEY_TI_CL6X_VERSION) || \ + defined(JSON_HEDLEY_TI_CL7X_VERSION) || \ + defined(JSON_HEDLEY_TI_CLPRU_VERSION) || \ + defined(__clang__) +# define JSON_HEDLEY_IS_CONSTEXPR_(expr) ( \ + sizeof(void) != \ + sizeof(*( \ + 1 ? \ + ((void*) ((expr) * 0L) ) : \ +((struct { char v[sizeof(void) * 2]; } *) 1) \ + ) \ + ) \ + ) +# endif +#endif +#if defined(JSON_HEDLEY_IS_CONSTEXPR_) + #if !defined(JSON_HEDLEY_IS_CONSTANT) + #define JSON_HEDLEY_IS_CONSTANT(expr) JSON_HEDLEY_IS_CONSTEXPR_(expr) + #endif + #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (JSON_HEDLEY_IS_CONSTEXPR_(expr) ? (expr) : (-1)) +#else + #if !defined(JSON_HEDLEY_IS_CONSTANT) + #define JSON_HEDLEY_IS_CONSTANT(expr) (0) + #endif + #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (expr) +#endif + +#if defined(JSON_HEDLEY_BEGIN_C_DECLS) + #undef JSON_HEDLEY_BEGIN_C_DECLS +#endif +#if defined(JSON_HEDLEY_END_C_DECLS) + #undef JSON_HEDLEY_END_C_DECLS +#endif +#if defined(JSON_HEDLEY_C_DECL) + #undef JSON_HEDLEY_C_DECL +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_BEGIN_C_DECLS extern "C" { + #define JSON_HEDLEY_END_C_DECLS } + #define JSON_HEDLEY_C_DECL extern "C" +#else + #define JSON_HEDLEY_BEGIN_C_DECLS + #define JSON_HEDLEY_END_C_DECLS + #define JSON_HEDLEY_C_DECL +#endif + +#if defined(JSON_HEDLEY_STATIC_ASSERT) + #undef JSON_HEDLEY_STATIC_ASSERT +#endif +#if \ + !defined(__cplusplus) && ( \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)) || \ + (JSON_HEDLEY_HAS_FEATURE(c_static_assert) && !defined(JSON_HEDLEY_INTEL_CL_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(6,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + defined(_Static_assert) \ + ) +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) _Static_assert(expr, message) +#elif \ + (defined(__cplusplus) && (__cplusplus >= 201103L)) || \ + JSON_HEDLEY_MSVC_VERSION_CHECK(16,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(static_assert(expr, message)) +#else +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) +#endif + +#if defined(JSON_HEDLEY_NULL) + #undef JSON_HEDLEY_NULL +#endif +#if defined(__cplusplus) + #if __cplusplus >= 201103L + #define JSON_HEDLEY_NULL JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(nullptr) + #elif defined(NULL) + #define JSON_HEDLEY_NULL NULL + #else + #define JSON_HEDLEY_NULL JSON_HEDLEY_STATIC_CAST(void*, 0) + #endif +#elif defined(NULL) + #define JSON_HEDLEY_NULL NULL +#else + #define JSON_HEDLEY_NULL ((void*) 0) +#endif + +#if defined(JSON_HEDLEY_MESSAGE) + #undef JSON_HEDLEY_MESSAGE +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") +# define JSON_HEDLEY_MESSAGE(msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ + JSON_HEDLEY_PRAGMA(message msg) \ + JSON_HEDLEY_DIAGNOSTIC_POP +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message msg) +#elif JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(_CRI message msg) +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#else +# define JSON_HEDLEY_MESSAGE(msg) +#endif + +#if defined(JSON_HEDLEY_WARNING) + #undef JSON_HEDLEY_WARNING +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") +# define JSON_HEDLEY_WARNING(msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ + JSON_HEDLEY_PRAGMA(clang warning msg) \ + JSON_HEDLEY_DIAGNOSTIC_POP +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,8,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(GCC warning msg) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#else +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_MESSAGE(msg) +#endif + +#if defined(JSON_HEDLEY_REQUIRE) + #undef JSON_HEDLEY_REQUIRE +#endif +#if defined(JSON_HEDLEY_REQUIRE_MSG) + #undef JSON_HEDLEY_REQUIRE_MSG +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(diagnose_if) +# if JSON_HEDLEY_HAS_WARNING("-Wgcc-compat") +# define JSON_HEDLEY_REQUIRE(expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ + __attribute__((diagnose_if(!(expr), #expr, "error"))) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ + __attribute__((diagnose_if(!(expr), msg, "error"))) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_REQUIRE(expr) __attribute__((diagnose_if(!(expr), #expr, "error"))) +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) __attribute__((diagnose_if(!(expr), msg, "error"))) +# endif +#else +# define JSON_HEDLEY_REQUIRE(expr) +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) +#endif + +#if defined(JSON_HEDLEY_FLAGS) + #undef JSON_HEDLEY_FLAGS +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(flag_enum) && (!defined(__cplusplus) || JSON_HEDLEY_HAS_WARNING("-Wbitfield-enum-conversion")) + #define JSON_HEDLEY_FLAGS __attribute__((__flag_enum__)) +#else + #define JSON_HEDLEY_FLAGS +#endif + +#if defined(JSON_HEDLEY_FLAGS_CAST) + #undef JSON_HEDLEY_FLAGS_CAST +#endif +#if JSON_HEDLEY_INTEL_VERSION_CHECK(19,0,0) +# define JSON_HEDLEY_FLAGS_CAST(T, expr) (__extension__ ({ \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("warning(disable:188)") \ + ((T) (expr)); \ + JSON_HEDLEY_DIAGNOSTIC_POP \ + })) +#else +# define JSON_HEDLEY_FLAGS_CAST(T, expr) JSON_HEDLEY_STATIC_CAST(T, expr) +#endif + +#if defined(JSON_HEDLEY_EMPTY_BASES) + #undef JSON_HEDLEY_EMPTY_BASES +#endif +#if \ + (JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,23918) && !JSON_HEDLEY_MSVC_VERSION_CHECK(20,0,0)) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_EMPTY_BASES __declspec(empty_bases) +#else + #define JSON_HEDLEY_EMPTY_BASES +#endif + +/* Remaining macros are deprecated. */ + +#if defined(JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK) + #undef JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK +#endif +#if defined(__clang__) + #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) (0) +#else + #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_CLANG_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_BUILTIN) + #undef JSON_HEDLEY_CLANG_HAS_BUILTIN +#endif +#define JSON_HEDLEY_CLANG_HAS_BUILTIN(builtin) JSON_HEDLEY_HAS_BUILTIN(builtin) + +#if defined(JSON_HEDLEY_CLANG_HAS_FEATURE) + #undef JSON_HEDLEY_CLANG_HAS_FEATURE +#endif +#define JSON_HEDLEY_CLANG_HAS_FEATURE(feature) JSON_HEDLEY_HAS_FEATURE(feature) + +#if defined(JSON_HEDLEY_CLANG_HAS_EXTENSION) + #undef JSON_HEDLEY_CLANG_HAS_EXTENSION +#endif +#define JSON_HEDLEY_CLANG_HAS_EXTENSION(extension) JSON_HEDLEY_HAS_EXTENSION(extension) + +#if defined(JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_DECLSPEC_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_WARNING) + #undef JSON_HEDLEY_CLANG_HAS_WARNING +#endif +#define JSON_HEDLEY_CLANG_HAS_WARNING(warning) JSON_HEDLEY_HAS_WARNING(warning) + +#endif /* !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < X) */ + + +// This file contains all internal macro definitions (except those affecting ABI) +// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them + +// #include + + +// exclude unsupported compilers +#if !defined(JSON_SKIP_UNSUPPORTED_COMPILER_CHECK) + #if defined(__clang__) + #if (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) < 30400 + #error "unsupported Clang version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #elif defined(__GNUC__) && !(defined(__ICC) || defined(__INTEL_COMPILER)) + #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) < 40800 + #error "unsupported GCC version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #endif +#endif + +// C++ language standard detection +// if the user manually specified the used c++ version this is skipped +#if !defined(JSON_HAS_CPP_23) && !defined(JSON_HAS_CPP_20) && !defined(JSON_HAS_CPP_17) && !defined(JSON_HAS_CPP_14) && !defined(JSON_HAS_CPP_11) + #if (defined(__cplusplus) && __cplusplus > 202002L) || (defined(_MSVC_LANG) && _MSVC_LANG > 202002L) + #define JSON_HAS_CPP_23 + #define JSON_HAS_CPP_20 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 + #elif (defined(__cplusplus) && __cplusplus > 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG > 201703L) + #define JSON_HAS_CPP_20 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 + #elif (defined(__cplusplus) && __cplusplus > 201402L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 + #elif (defined(__cplusplus) && __cplusplus > 201103L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) + #define JSON_HAS_CPP_14 + #endif + // the cpp 11 flag is always specified because it is the minimal required version + #define JSON_HAS_CPP_11 +#endif + +#ifdef __has_include + #if __has_include() + #include + #endif +#endif + +#if !defined(JSON_HAS_FILESYSTEM) && !defined(JSON_HAS_EXPERIMENTAL_FILESYSTEM) + #ifdef JSON_HAS_CPP_17 + #if defined(__cpp_lib_filesystem) + #define JSON_HAS_FILESYSTEM 1 + #elif defined(__cpp_lib_experimental_filesystem) + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 + #elif !defined(__has_include) + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 + #elif __has_include() + #define JSON_HAS_FILESYSTEM 1 + #elif __has_include() + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 + #endif + + // std::filesystem does not work on MinGW GCC 8: https://sourceforge.net/p/mingw-w64/bugs/737/ + #if defined(__MINGW32__) && defined(__GNUC__) && __GNUC__ == 8 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before GCC 8: https://en.cppreference.com/w/cpp/compiler_support + #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 8 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before Clang 7: https://en.cppreference.com/w/cpp/compiler_support + #if defined(__clang_major__) && __clang_major__ < 7 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before MSVC 19.14: https://en.cppreference.com/w/cpp/compiler_support + #if defined(_MSC_VER) && _MSC_VER < 1914 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before iOS 13 + #if defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < 130000 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before macOS Catalina + #if defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + #endif +#endif + +#ifndef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 0 +#endif + +#ifndef JSON_HAS_FILESYSTEM + #define JSON_HAS_FILESYSTEM 0 +#endif + +#ifndef JSON_HAS_THREE_WAY_COMPARISON + #if defined(__cpp_impl_three_way_comparison) && __cpp_impl_three_way_comparison >= 201907L \ + && defined(__cpp_lib_three_way_comparison) && __cpp_lib_three_way_comparison >= 201907L + #define JSON_HAS_THREE_WAY_COMPARISON 1 + #else + #define JSON_HAS_THREE_WAY_COMPARISON 0 + #endif +#endif + +#ifndef JSON_HAS_RANGES + // ranges header shipping in GCC 11.1.0 (released 2021-04-27) has syntax error + #if defined(__GLIBCXX__) && __GLIBCXX__ == 20210427 + #define JSON_HAS_RANGES 0 + #elif defined(__cpp_lib_ranges) + #define JSON_HAS_RANGES 1 + #else + #define JSON_HAS_RANGES 0 + #endif +#endif + +#ifndef JSON_HAS_STATIC_RTTI + #if !defined(_HAS_STATIC_RTTI) || _HAS_STATIC_RTTI != 0 + #define JSON_HAS_STATIC_RTTI 1 + #else + #define JSON_HAS_STATIC_RTTI 0 + #endif +#endif + +#ifdef JSON_HAS_CPP_17 + #define JSON_INLINE_VARIABLE inline +#else + #define JSON_INLINE_VARIABLE +#endif + +#if JSON_HEDLEY_HAS_ATTRIBUTE(no_unique_address) + #define JSON_NO_UNIQUE_ADDRESS [[no_unique_address]] +#else + #define JSON_NO_UNIQUE_ADDRESS +#endif + +// disable documentation warnings on clang +#if defined(__clang__) + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdocumentation" + #pragma clang diagnostic ignored "-Wdocumentation-unknown-command" +#endif + +// allow disabling exceptions +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + #define JSON_THROW(exception) throw exception + #define JSON_TRY try + #define JSON_CATCH(exception) catch(exception) + #define JSON_INTERNAL_CATCH(exception) catch(exception) +#else + #include + #define JSON_THROW(exception) std::abort() + #define JSON_TRY if(true) + #define JSON_CATCH(exception) if(false) + #define JSON_INTERNAL_CATCH(exception) if(false) +#endif + +// override exception macros +#if defined(JSON_THROW_USER) + #undef JSON_THROW + #define JSON_THROW JSON_THROW_USER +#endif +#if defined(JSON_TRY_USER) + #undef JSON_TRY + #define JSON_TRY JSON_TRY_USER +#endif +#if defined(JSON_CATCH_USER) + #undef JSON_CATCH + #define JSON_CATCH JSON_CATCH_USER + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_CATCH_USER +#endif +#if defined(JSON_INTERNAL_CATCH_USER) + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_INTERNAL_CATCH_USER +#endif + +// allow overriding assert +#if !defined(JSON_ASSERT) + #include // assert + #define JSON_ASSERT(x) assert(x) +#endif + +// allow to access some private functions (needed by the test suite) +#if defined(JSON_TESTS_PRIVATE) + #define JSON_PRIVATE_UNLESS_TESTED public +#else + #define JSON_PRIVATE_UNLESS_TESTED private +#endif + +/*! +@brief macro to briefly define a mapping between an enum and JSON +@def NLOHMANN_JSON_SERIALIZE_ENUM +@since version 3.4.0 +*/ +#define NLOHMANN_JSON_SERIALIZE_ENUM(ENUM_TYPE, ...) \ + template \ + inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \ + { \ + /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on */ \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [e](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.first == e; \ + }); \ + j = ((it != std::end(m)) ? it : std::begin(m))->second; \ + } \ + template \ + inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ + { \ + /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on */ \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [&j](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.second == j; \ + }); \ + e = ((it != std::end(m)) ? it : std::begin(m))->first; \ + } + +// Ugly macros to avoid uglier copy-paste when specializing basic_json. They +// may be removed in the future once the class is split. + +#define NLOHMANN_BASIC_JSON_TPL_DECLARATION \ + template class ObjectType, \ + template class ArrayType, \ + class StringType, class BooleanType, class NumberIntegerType, \ + class NumberUnsignedType, class NumberFloatType, \ + template class AllocatorType, \ + template class JSONSerializer, \ + class BinaryType, \ + class CustomBaseClass> + +#define NLOHMANN_BASIC_JSON_TPL \ + basic_json + +// Macros to simplify conversion from/to types + +#define NLOHMANN_JSON_EXPAND( x ) x +#define NLOHMANN_JSON_GET_MACRO(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, NAME,...) NAME +#define NLOHMANN_JSON_PASTE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_GET_MACRO(__VA_ARGS__, \ + NLOHMANN_JSON_PASTE64, \ + NLOHMANN_JSON_PASTE63, \ + NLOHMANN_JSON_PASTE62, \ + NLOHMANN_JSON_PASTE61, \ + NLOHMANN_JSON_PASTE60, \ + NLOHMANN_JSON_PASTE59, \ + NLOHMANN_JSON_PASTE58, \ + NLOHMANN_JSON_PASTE57, \ + NLOHMANN_JSON_PASTE56, \ + NLOHMANN_JSON_PASTE55, \ + NLOHMANN_JSON_PASTE54, \ + NLOHMANN_JSON_PASTE53, \ + NLOHMANN_JSON_PASTE52, \ + NLOHMANN_JSON_PASTE51, \ + NLOHMANN_JSON_PASTE50, \ + NLOHMANN_JSON_PASTE49, \ + NLOHMANN_JSON_PASTE48, \ + NLOHMANN_JSON_PASTE47, \ + NLOHMANN_JSON_PASTE46, \ + NLOHMANN_JSON_PASTE45, \ + NLOHMANN_JSON_PASTE44, \ + NLOHMANN_JSON_PASTE43, \ + NLOHMANN_JSON_PASTE42, \ + NLOHMANN_JSON_PASTE41, \ + NLOHMANN_JSON_PASTE40, \ + NLOHMANN_JSON_PASTE39, \ + NLOHMANN_JSON_PASTE38, \ + NLOHMANN_JSON_PASTE37, \ + NLOHMANN_JSON_PASTE36, \ + NLOHMANN_JSON_PASTE35, \ + NLOHMANN_JSON_PASTE34, \ + NLOHMANN_JSON_PASTE33, \ + NLOHMANN_JSON_PASTE32, \ + NLOHMANN_JSON_PASTE31, \ + NLOHMANN_JSON_PASTE30, \ + NLOHMANN_JSON_PASTE29, \ + NLOHMANN_JSON_PASTE28, \ + NLOHMANN_JSON_PASTE27, \ + NLOHMANN_JSON_PASTE26, \ + NLOHMANN_JSON_PASTE25, \ + NLOHMANN_JSON_PASTE24, \ + NLOHMANN_JSON_PASTE23, \ + NLOHMANN_JSON_PASTE22, \ + NLOHMANN_JSON_PASTE21, \ + NLOHMANN_JSON_PASTE20, \ + NLOHMANN_JSON_PASTE19, \ + NLOHMANN_JSON_PASTE18, \ + NLOHMANN_JSON_PASTE17, \ + NLOHMANN_JSON_PASTE16, \ + NLOHMANN_JSON_PASTE15, \ + NLOHMANN_JSON_PASTE14, \ + NLOHMANN_JSON_PASTE13, \ + NLOHMANN_JSON_PASTE12, \ + NLOHMANN_JSON_PASTE11, \ + NLOHMANN_JSON_PASTE10, \ + NLOHMANN_JSON_PASTE9, \ + NLOHMANN_JSON_PASTE8, \ + NLOHMANN_JSON_PASTE7, \ + NLOHMANN_JSON_PASTE6, \ + NLOHMANN_JSON_PASTE5, \ + NLOHMANN_JSON_PASTE4, \ + NLOHMANN_JSON_PASTE3, \ + NLOHMANN_JSON_PASTE2, \ + NLOHMANN_JSON_PASTE1)(__VA_ARGS__)) +#define NLOHMANN_JSON_PASTE2(func, v1) func(v1) +#define NLOHMANN_JSON_PASTE3(func, v1, v2) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE2(func, v2) +#define NLOHMANN_JSON_PASTE4(func, v1, v2, v3) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE3(func, v2, v3) +#define NLOHMANN_JSON_PASTE5(func, v1, v2, v3, v4) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE4(func, v2, v3, v4) +#define NLOHMANN_JSON_PASTE6(func, v1, v2, v3, v4, v5) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE5(func, v2, v3, v4, v5) +#define NLOHMANN_JSON_PASTE7(func, v1, v2, v3, v4, v5, v6) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE6(func, v2, v3, v4, v5, v6) +#define NLOHMANN_JSON_PASTE8(func, v1, v2, v3, v4, v5, v6, v7) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE7(func, v2, v3, v4, v5, v6, v7) +#define NLOHMANN_JSON_PASTE9(func, v1, v2, v3, v4, v5, v6, v7, v8) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE8(func, v2, v3, v4, v5, v6, v7, v8) +#define NLOHMANN_JSON_PASTE10(func, v1, v2, v3, v4, v5, v6, v7, v8, v9) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE9(func, v2, v3, v4, v5, v6, v7, v8, v9) +#define NLOHMANN_JSON_PASTE11(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE10(func, v2, v3, v4, v5, v6, v7, v8, v9, v10) +#define NLOHMANN_JSON_PASTE12(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE11(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) +#define NLOHMANN_JSON_PASTE13(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE12(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) +#define NLOHMANN_JSON_PASTE14(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE13(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) +#define NLOHMANN_JSON_PASTE15(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE14(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) +#define NLOHMANN_JSON_PASTE16(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE15(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) +#define NLOHMANN_JSON_PASTE17(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE16(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) +#define NLOHMANN_JSON_PASTE18(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE17(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) +#define NLOHMANN_JSON_PASTE19(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE18(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) +#define NLOHMANN_JSON_PASTE20(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE19(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) +#define NLOHMANN_JSON_PASTE21(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE20(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) +#define NLOHMANN_JSON_PASTE22(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE21(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) +#define NLOHMANN_JSON_PASTE23(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE22(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) +#define NLOHMANN_JSON_PASTE24(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE23(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) +#define NLOHMANN_JSON_PASTE25(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE24(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) +#define NLOHMANN_JSON_PASTE26(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE25(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) +#define NLOHMANN_JSON_PASTE27(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE26(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) +#define NLOHMANN_JSON_PASTE28(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE27(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) +#define NLOHMANN_JSON_PASTE29(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE28(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) +#define NLOHMANN_JSON_PASTE30(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE29(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) +#define NLOHMANN_JSON_PASTE31(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE30(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) +#define NLOHMANN_JSON_PASTE32(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE31(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) +#define NLOHMANN_JSON_PASTE33(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE32(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) +#define NLOHMANN_JSON_PASTE34(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE33(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) +#define NLOHMANN_JSON_PASTE35(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE34(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) +#define NLOHMANN_JSON_PASTE36(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE35(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) +#define NLOHMANN_JSON_PASTE37(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE36(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) +#define NLOHMANN_JSON_PASTE38(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE37(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) +#define NLOHMANN_JSON_PASTE39(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE38(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) +#define NLOHMANN_JSON_PASTE40(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE39(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) +#define NLOHMANN_JSON_PASTE41(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE40(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) +#define NLOHMANN_JSON_PASTE42(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE41(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) +#define NLOHMANN_JSON_PASTE43(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE42(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) +#define NLOHMANN_JSON_PASTE44(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE43(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) +#define NLOHMANN_JSON_PASTE45(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE44(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) +#define NLOHMANN_JSON_PASTE46(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE45(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) +#define NLOHMANN_JSON_PASTE47(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE46(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) +#define NLOHMANN_JSON_PASTE48(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE47(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) +#define NLOHMANN_JSON_PASTE49(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE48(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) +#define NLOHMANN_JSON_PASTE50(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE49(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) +#define NLOHMANN_JSON_PASTE51(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE50(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) +#define NLOHMANN_JSON_PASTE52(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE51(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) +#define NLOHMANN_JSON_PASTE53(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE52(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) +#define NLOHMANN_JSON_PASTE54(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE53(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) +#define NLOHMANN_JSON_PASTE55(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE54(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) +#define NLOHMANN_JSON_PASTE56(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE55(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) +#define NLOHMANN_JSON_PASTE57(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE56(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) +#define NLOHMANN_JSON_PASTE58(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE57(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) +#define NLOHMANN_JSON_PASTE59(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE58(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) +#define NLOHMANN_JSON_PASTE60(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE59(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) +#define NLOHMANN_JSON_PASTE61(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE60(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) +#define NLOHMANN_JSON_PASTE62(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE61(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) +#define NLOHMANN_JSON_PASTE63(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE62(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) +#define NLOHMANN_JSON_PASTE64(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE63(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) + +#define NLOHMANN_JSON_TO(v1) nlohmann_json_j[#v1] = nlohmann_json_t.v1; +#define NLOHMANN_JSON_FROM(v1) nlohmann_json_j.at(#v1).get_to(nlohmann_json_t.v1); +#define NLOHMANN_JSON_FROM_WITH_DEFAULT(v1) nlohmann_json_t.v1 = !nlohmann_json_j.is_null() ? nlohmann_json_j.value(#v1, nlohmann_json_default_obj.v1) : nlohmann_json_default_obj.v1; + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_INTRUSIVE +@since version 3.9.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_type_intrusive/ +*/ +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE(Type, ...) \ + template::value, int> = 0> \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT +@since version 3.11.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_type_intrusive/ +*/ +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Type, ...) \ + template::value, int> = 0> \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE +@since version 3.11.3 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_type_intrusive/ +*/ +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ + template::value, int> = 0> \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE +@since version 3.9.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_type_non_intrusive/ +*/ +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Type, ...) \ + template::value, int> = 0> \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT +@since version 3.11.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_type_non_intrusive/ +*/ +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Type, ...) \ + template::value, int> = 0> \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE +@since version 3.11.3 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_type_non_intrusive/ +*/ +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ + template::value, int> = 0> \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE +@since version 3.12.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_derived_type/ +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE(Type, BaseType, ...) \ + template::value, int> = 0> \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT +@since version 3.12.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_derived_type/ +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT(Type, BaseType, ...) \ + template::value, int> = 0> \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_ONLY_SERIALIZE +@since version 3.12.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_derived_type/ +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_ONLY_SERIALIZE(Type, BaseType, ...) \ + template::value, int> = 0> \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE +@since version 3.12.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_derived_type/ +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE(Type, BaseType, ...) \ + template::value, int> = 0> \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT +@since version 3.12.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_derived_type/ +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Type, BaseType, ...) \ + template::value, int> = 0> \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + template::value, int> = 0> \ + void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE +@since version 3.12.0 +@sa https://json.nlohmann.me/api/macros/nlohmann_define_derived_type/ +*/ +#define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE(Type, BaseType, ...) \ + template::value, int> = 0> \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + +// inspired from https://stackoverflow.com/a/26745591 +// allows calling any std function as if (e.g., with begin): +// using std::begin; begin(x); +// +// it allows using the detected idiom to retrieve the return type +// of such an expression +#define NLOHMANN_CAN_CALL_STD_FUNC_IMPL(std_name) \ + namespace detail { \ + using std::std_name; \ + \ + template \ + using result_of_##std_name = decltype(std_name(std::declval()...)); \ + } \ + \ + namespace detail2 { \ + struct std_name##_tag \ + { \ + }; \ + \ + template \ + std_name##_tag std_name(T&&...); \ + \ + template \ + using result_of_##std_name = decltype(std_name(std::declval()...)); \ + \ + template \ + struct would_call_std_##std_name \ + { \ + static constexpr auto const value = ::nlohmann::detail:: \ + is_detected_exact::value; \ + }; \ + } /* namespace detail2 */ \ + \ + template \ + struct would_call_std_##std_name : detail2::would_call_std_##std_name \ + { \ + } + +#ifndef JSON_USE_IMPLICIT_CONVERSIONS + #define JSON_USE_IMPLICIT_CONVERSIONS 1 +#endif + +#if JSON_USE_IMPLICIT_CONVERSIONS + #define JSON_EXPLICIT +#else + #define JSON_EXPLICIT explicit +#endif + +#ifndef JSON_DISABLE_ENUM_SERIALIZATION + #define JSON_DISABLE_ENUM_SERIALIZATION 0 +#endif + +#ifndef JSON_USE_GLOBAL_UDLS + #define JSON_USE_GLOBAL_UDLS 1 +#endif + +#if JSON_HAS_THREE_WAY_COMPARISON + #include // partial_ordering +#endif + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/////////////////////////// +// JSON type enumeration // +/////////////////////////// + +/*! +@brief the JSON type enumeration + +This enumeration collects the different JSON types. It is internally used to +distinguish the stored values, and the functions @ref basic_json::is_null(), +@ref basic_json::is_object(), @ref basic_json::is_array(), +@ref basic_json::is_string(), @ref basic_json::is_boolean(), +@ref basic_json::is_number() (with @ref basic_json::is_number_integer(), +@ref basic_json::is_number_unsigned(), and @ref basic_json::is_number_float()), +@ref basic_json::is_discarded(), @ref basic_json::is_primitive(), and +@ref basic_json::is_structured() rely on it. + +@note There are three enumeration entries (number_integer, number_unsigned, and +number_float), because the library distinguishes these three types for numbers: +@ref basic_json::number_unsigned_t is used for unsigned integers, +@ref basic_json::number_integer_t is used for signed integers, and +@ref basic_json::number_float_t is used for floating-point numbers or to +approximate integers which do not fit in the limits of their respective type. + +@sa see @ref basic_json::basic_json(const value_t value_type) -- create a JSON +value with the default value for a given type + +@since version 1.0.0 +*/ +enum class value_t : std::uint8_t +{ + null, ///< null value + object, ///< object (unordered set of name/value pairs) + array, ///< array (ordered collection of values) + string, ///< string value + boolean, ///< boolean value + number_integer, ///< number value (signed integer) + number_unsigned, ///< number value (unsigned integer) + number_float, ///< number value (floating-point) + binary, ///< binary array (ordered collection of bytes) + discarded ///< discarded by the parser callback function +}; + +/*! +@brief comparison operator for JSON types + +Returns an ordering that is similar to Python: +- order: null < boolean < number < object < array < string < binary +- furthermore, each type is not smaller than itself +- discarded values are not comparable +- binary is represented as a b"" string in python and directly comparable to a + string; however, making a binary array directly comparable with a string would + be surprising behavior in a JSON file. + +@since version 1.0.0 +*/ +#if JSON_HAS_THREE_WAY_COMPARISON + inline std::partial_ordering operator<=>(const value_t lhs, const value_t rhs) noexcept // *NOPAD* +#else + inline bool operator<(const value_t lhs, const value_t rhs) noexcept +#endif +{ + static constexpr std::array order = {{ + 0 /* null */, 3 /* object */, 4 /* array */, 5 /* string */, + 1 /* boolean */, 2 /* integer */, 2 /* unsigned */, 2 /* float */, + 6 /* binary */ + } + }; + + const auto l_index = static_cast(lhs); + const auto r_index = static_cast(rhs); +#if JSON_HAS_THREE_WAY_COMPARISON + if (l_index < order.size() && r_index < order.size()) + { + return order[l_index] <=> order[r_index]; // *NOPAD* + } + return std::partial_ordering::unordered; +#else + return l_index < order.size() && r_index < order.size() && order[l_index] < order[r_index]; +#endif +} + +// GCC selects the built-in operator< over an operator rewritten from +// a user-defined spaceship operator +// Clang, MSVC, and ICC select the rewritten candidate +// (see GCC bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105200) +#if JSON_HAS_THREE_WAY_COMPARISON && defined(__GNUC__) +inline bool operator<(const value_t lhs, const value_t rhs) noexcept +{ + return std::is_lt(lhs <=> rhs); // *NOPAD* +} +#endif + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/*! +@brief replace all occurrences of a substring by another string + +@param[in,out] s the string to manipulate; changed so that all + occurrences of @a f are replaced with @a t +@param[in] f the substring to replace with @a t +@param[in] t the string to replace @a f + +@pre The search string @a f must not be empty. **This precondition is +enforced with an assertion.** + +@since version 2.0.0 +*/ +template +inline void replace_substring(StringType& s, const StringType& f, + const StringType& t) +{ + JSON_ASSERT(!f.empty()); + for (auto pos = s.find(f); // find first occurrence of f + pos != StringType::npos; // make sure f was found + s.replace(pos, f.size(), t), // replace with t, and + pos = s.find(f, pos + t.size())) // find next occurrence of f + {} +} + +/*! + * @brief string escaping as described in RFC 6901 (Sect. 4) + * @param[in] s string to escape + * @return escaped string + * + * Note the order of escaping "~" to "~0" and "/" to "~1" is important. + */ +template +inline StringType escape(StringType s) +{ + replace_substring(s, StringType{"~"}, StringType{"~0"}); + replace_substring(s, StringType{"/"}, StringType{"~1"}); + return s; +} + +/*! + * @brief string unescaping as described in RFC 6901 (Sect. 4) + * @param[in] s string to unescape + * @return unescaped string + * + * Note the order of escaping "~1" to "/" and "~0" to "~" is important. + */ +template +static void unescape(StringType& s) +{ + replace_substring(s, StringType{"~1"}, StringType{"/"}); + replace_substring(s, StringType{"~0"}, StringType{"~"}); +} + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // size_t + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/// struct to capture the start position of the current token +struct position_t +{ + /// the total number of characters read + std::size_t chars_read_total = 0; + /// the number of characters read in the current line + std::size_t chars_read_current_line = 0; + /// the number of lines read + std::size_t lines_read = 0; + + /// conversion to size_t to preserve SAX interface + constexpr operator size_t() const + { + return chars_read_total; + } +}; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-FileCopyrightText: 2018 The Abseil Authors +// SPDX-License-Identifier: MIT + + + +#include // array +#include // size_t +#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type +#include // index_sequence, make_index_sequence, index_sequence_for + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +template +using uncvref_t = typename std::remove_cv::type>::type; + +#ifdef JSON_HAS_CPP_14 + +// the following utilities are natively available in C++14 +using std::enable_if_t; +using std::index_sequence; +using std::make_index_sequence; +using std::index_sequence_for; + +#else + +// alias templates to reduce boilerplate +template +using enable_if_t = typename std::enable_if::type; + +// The following code is taken from https://github.com/abseil/abseil-cpp/blob/10cb35e459f5ecca5b2ff107635da0bfa41011b4/absl/utility/utility.h +// which is part of Google Abseil (https://github.com/abseil/abseil-cpp), licensed under the Apache License 2.0. + +//// START OF CODE FROM GOOGLE ABSEIL + +// integer_sequence +// +// Class template representing a compile-time integer sequence. An instantiation +// of `integer_sequence` has a sequence of integers encoded in its +// type through its template arguments (which is a common need when +// working with C++11 variadic templates). `absl::integer_sequence` is designed +// to be a drop-in replacement for C++14's `std::integer_sequence`. +// +// Example: +// +// template< class T, T... Ints > +// void user_function(integer_sequence); +// +// int main() +// { +// // user_function's `T` will be deduced to `int` and `Ints...` +// // will be deduced to `0, 1, 2, 3, 4`. +// user_function(make_integer_sequence()); +// } +template +struct integer_sequence +{ + using value_type = T; + static constexpr std::size_t size() noexcept + { + return sizeof...(Ints); + } +}; + +// index_sequence +// +// A helper template for an `integer_sequence` of `size_t`, +// `absl::index_sequence` is designed to be a drop-in replacement for C++14's +// `std::index_sequence`. +template +using index_sequence = integer_sequence; + +namespace utility_internal +{ + +template +struct Extend; + +// Note that SeqSize == sizeof...(Ints). It's passed explicitly for efficiency. +template +struct Extend, SeqSize, 0> +{ + using type = integer_sequence < T, Ints..., (Ints + SeqSize)... >; +}; + +template +struct Extend, SeqSize, 1> +{ + using type = integer_sequence < T, Ints..., (Ints + SeqSize)..., 2 * SeqSize >; +}; + +// Recursion helper for 'make_integer_sequence'. +// 'Gen::type' is an alias for 'integer_sequence'. +template +struct Gen +{ + using type = + typename Extend < typename Gen < T, N / 2 >::type, N / 2, N % 2 >::type; +}; + +template +struct Gen +{ + using type = integer_sequence; +}; + +} // namespace utility_internal + +// Compile-time sequences of integers + +// make_integer_sequence +// +// This template alias is equivalent to +// `integer_sequence`, and is designed to be a drop-in +// replacement for C++14's `std::make_integer_sequence`. +template +using make_integer_sequence = typename utility_internal::Gen::type; + +// make_index_sequence +// +// This template alias is equivalent to `index_sequence<0, 1, ..., N-1>`, +// and is designed to be a drop-in replacement for C++14's +// `std::make_index_sequence`. +template +using make_index_sequence = make_integer_sequence; + +// index_sequence_for +// +// Converts a typename pack into an index sequence of the same length, and +// is designed to be a drop-in replacement for C++14's +// `std::index_sequence_for()` +template +using index_sequence_for = make_index_sequence; + +//// END OF CODE FROM GOOGLE ABSEIL + +#endif + +// dispatch utility (taken from ranges-v3) +template struct priority_tag : priority_tag < N - 1 > {}; +template<> struct priority_tag<0> {}; + +// taken from ranges-v3 +template +struct static_const +{ + static JSON_INLINE_VARIABLE constexpr T value{}; +}; + +#ifndef JSON_HAS_CPP_17 + template + constexpr T static_const::value; +#endif + +template +constexpr std::array make_array(Args&& ... args) +{ + return std::array {{static_cast(std::forward(args))...}}; +} + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // numeric_limits +#include // char_traits +#include // tuple +#include // false_type, is_constructible, is_integral, is_same, true_type +#include // declval + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // random_access_iterator_tag + +// #include + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +template +struct iterator_types {}; + +template +struct iterator_types < + It, + void_t> +{ + using difference_type = typename It::difference_type; + using value_type = typename It::value_type; + using pointer = typename It::pointer; + using reference = typename It::reference; + using iterator_category = typename It::iterator_category; +}; + +// This is required as some compilers implement std::iterator_traits in a way that +// doesn't work with SFINAE. See https://github.com/nlohmann/json/issues/1341. +template +struct iterator_traits +{ +}; + +template +struct iterator_traits < T, enable_if_t < !std::is_pointer::value >> + : iterator_types +{ +}; + +template +struct iterator_traits::value>> +{ + using iterator_category = std::random_access_iterator_tag; + using value_type = T; + using difference_type = ptrdiff_t; + using pointer = T*; + using reference = T&; +}; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +NLOHMANN_CAN_CALL_STD_FUNC_IMPL(begin); + +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +NLOHMANN_CAN_CALL_STD_FUNC_IMPL(end); + +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013 - 2025 Niels Lohmann +// SPDX-License-Identifier: MIT + +#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_ + #define INCLUDE_NLOHMANN_JSON_FWD_HPP_ + + #include // int64_t, uint64_t + #include // map + #include // allocator + #include // string + #include // vector + + // #include + + + /*! + @brief namespace for Niels Lohmann + @see https://github.com/nlohmann + @since version 1.0.0 + */ + NLOHMANN_JSON_NAMESPACE_BEGIN + + /*! + @brief default JSONSerializer template argument + + This serializer ignores the template arguments and uses ADL + ([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl)) + for serialization. + */ + template + struct adl_serializer; + + /// a class to store JSON values + /// @sa https://json.nlohmann.me/api/basic_json/ + template class ObjectType = + std::map, + template class ArrayType = std::vector, + class StringType = std::string, class BooleanType = bool, + class NumberIntegerType = std::int64_t, + class NumberUnsignedType = std::uint64_t, + class NumberFloatType = double, + template class AllocatorType = std::allocator, + template class JSONSerializer = + adl_serializer, + class BinaryType = std::vector, // cppcheck-suppress syntaxError + class CustomBaseClass = void> + class basic_json; + + /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document + /// @sa https://json.nlohmann.me/api/json_pointer/ + template + class json_pointer; + + /*! + @brief default specialization + @sa https://json.nlohmann.me/api/json/ + */ + using json = basic_json<>; + + /// @brief a minimal map-like container that preserves insertion order + /// @sa https://json.nlohmann.me/api/ordered_map/ + template + struct ordered_map; + + /// @brief specialization that maintains the insertion order of object keys + /// @sa https://json.nlohmann.me/api/ordered_json/ + using ordered_json = basic_json; + + NLOHMANN_JSON_NAMESPACE_END + +#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_ + + +NLOHMANN_JSON_NAMESPACE_BEGIN +/*! +@brief detail namespace with internal helper functions + +This namespace collects functions that should not be exposed, +implementations of some @ref basic_json methods, and meta-programming helpers. + +@since version 2.1.0 +*/ +namespace detail +{ + +///////////// +// helpers // +///////////// + +// Note to maintainers: +// +// Every trait in this file expects a non CV-qualified type. +// The only exceptions are in the 'aliases for detected' section +// (i.e. those of the form: decltype(T::member_function(std::declval()))) +// +// In this case, T has to be properly CV-qualified to constraint the function arguments +// (e.g. to_json(BasicJsonType&, const T&)) + +template struct is_basic_json : std::false_type {}; + +NLOHMANN_BASIC_JSON_TPL_DECLARATION +struct is_basic_json : std::true_type {}; + +// used by exceptions create() member functions +// true_type for pointer to possibly cv-qualified basic_json or std::nullptr_t +// false_type otherwise +template +struct is_basic_json_context : + std::integral_constant < bool, + is_basic_json::type>::type>::value + || std::is_same::value > +{}; + +////////////////////// +// json_ref helpers // +////////////////////// + +template +class json_ref; + +template +struct is_json_ref : std::false_type {}; + +template +struct is_json_ref> : std::true_type {}; + +////////////////////////// +// aliases for detected // +////////////////////////// + +template +using mapped_type_t = typename T::mapped_type; + +template +using key_type_t = typename T::key_type; + +template +using value_type_t = typename T::value_type; + +template +using difference_type_t = typename T::difference_type; + +template +using pointer_t = typename T::pointer; + +template +using reference_t = typename T::reference; + +template +using iterator_category_t = typename T::iterator_category; + +template +using to_json_function = decltype(T::to_json(std::declval()...)); + +template +using from_json_function = decltype(T::from_json(std::declval()...)); + +template +using get_template_function = decltype(std::declval().template get()); + +// trait checking if JSONSerializer::from_json(json const&, udt&) exists +template +struct has_from_json : std::false_type {}; + +// trait checking if j.get is valid +// use this trait instead of std::is_constructible or std::is_convertible, +// both rely on, or make use of implicit conversions, and thus fail when T +// has several constructors/operator= (see https://github.com/nlohmann/json/issues/958) +template +struct is_getable +{ + static constexpr bool value = is_detected::value; +}; + +template +struct has_from_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +// This trait checks if JSONSerializer::from_json(json const&) exists +// this overload is used for non-default-constructible user-defined-types +template +struct has_non_default_from_json : std::false_type {}; + +template +struct has_non_default_from_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +// This trait checks if BasicJsonType::json_serializer::to_json exists +// Do not evaluate the trait when T is a basic_json type, to avoid template instantiation infinite recursion. +template +struct has_to_json : std::false_type {}; + +template +struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +template +using detect_key_compare = typename T::key_compare; + +template +struct has_key_compare : std::integral_constant::value> {}; + +// obtains the actual object key comparator +template +struct actual_object_comparator +{ + using object_t = typename BasicJsonType::object_t; + using object_comparator_t = typename BasicJsonType::default_object_comparator_t; + using type = typename std::conditional < has_key_compare::value, + typename object_t::key_compare, object_comparator_t>::type; +}; + +template +using actual_object_comparator_t = typename actual_object_comparator::type; + +///////////////// +// char_traits // +///////////////// + +// Primary template of char_traits calls std char_traits +template +struct char_traits : std::char_traits +{}; + +// Explicitly define char traits for unsigned char since it is not standard +template<> +struct char_traits : std::char_traits +{ + using char_type = unsigned char; + using int_type = uint64_t; + + // Redefine to_int_type function + static int_type to_int_type(char_type c) noexcept + { + return static_cast(c); + } + + static char_type to_char_type(int_type i) noexcept + { + return static_cast(i); + } + + static constexpr int_type eof() noexcept + { + return static_cast(std::char_traits::eof()); + } +}; + +// Explicitly define char traits for signed char since it is not standard +template<> +struct char_traits : std::char_traits +{ + using char_type = signed char; + using int_type = uint64_t; + + // Redefine to_int_type function + static int_type to_int_type(char_type c) noexcept + { + return static_cast(c); + } + + static char_type to_char_type(int_type i) noexcept + { + return static_cast(i); + } + + static constexpr int_type eof() noexcept + { + return static_cast(std::char_traits::eof()); + } +}; + +/////////////////// +// is_ functions // +/////////////////// + +// https://en.cppreference.com/w/cpp/types/conjunction +template struct conjunction : std::true_type { }; +template struct conjunction : B { }; +template +struct conjunction +: std::conditional(B::value), conjunction, B>::type {}; + +// https://en.cppreference.com/w/cpp/types/negation +template struct negation : std::integral_constant < bool, !B::value > { }; + +// Reimplementation of is_constructible and is_default_constructible, due to them being broken for +// std::pair and std::tuple until LWG 2367 fix (see https://cplusplus.github.io/LWG/lwg-defects.html#2367). +// This causes compile errors in e.g. clang 3.5 or gcc 4.9. +template +struct is_default_constructible : std::is_default_constructible {}; + +template +struct is_default_constructible> + : conjunction, is_default_constructible> {}; + +template +struct is_default_constructible> + : conjunction, is_default_constructible> {}; + +template +struct is_default_constructible> + : conjunction...> {}; + +template +struct is_default_constructible> + : conjunction...> {}; + +template +struct is_constructible : std::is_constructible {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_iterator_traits : std::false_type {}; + +template +struct is_iterator_traits> +{ + private: + using traits = iterator_traits; + + public: + static constexpr auto value = + is_detected::value && + is_detected::value && + is_detected::value && + is_detected::value && + is_detected::value; +}; + +template +struct is_range +{ + private: + using t_ref = typename std::add_lvalue_reference::type; + + using iterator = detected_t; + using sentinel = detected_t; + + // to be 100% correct, it should use https://en.cppreference.com/w/cpp/iterator/input_or_output_iterator + // and https://en.cppreference.com/w/cpp/iterator/sentinel_for + // but reimplementing these would be too much work, as a lot of other concepts are used underneath + static constexpr auto is_iterator_begin = + is_iterator_traits>::value; + + public: + static constexpr bool value = !std::is_same::value && !std::is_same::value && is_iterator_begin; +}; + +template +using iterator_t = enable_if_t::value, result_of_begin())>>; + +template +using range_value_t = value_type_t>>; + +// The following implementation of is_complete_type is taken from +// https://blogs.msdn.microsoft.com/vcblog/2015/12/02/partial-support-for-expression-sfinae-in-vs-2015-update-1/ +// and is written by Xiang Fan who agreed to using it in this library. + +template +struct is_complete_type : std::false_type {}; + +template +struct is_complete_type : std::true_type {}; + +template +struct is_compatible_object_type_impl : std::false_type {}; + +template +struct is_compatible_object_type_impl < + BasicJsonType, CompatibleObjectType, + enable_if_t < is_detected::value&& + is_detected::value >> +{ + using object_t = typename BasicJsonType::object_t; + + // macOS's is_constructible does not play well with nonesuch... + static constexpr bool value = + is_constructible::value && + is_constructible::value; +}; + +template +struct is_compatible_object_type + : is_compatible_object_type_impl {}; + +template +struct is_constructible_object_type_impl : std::false_type {}; + +template +struct is_constructible_object_type_impl < + BasicJsonType, ConstructibleObjectType, + enable_if_t < is_detected::value&& + is_detected::value >> +{ + using object_t = typename BasicJsonType::object_t; + + static constexpr bool value = + (is_default_constructible::value && + (std::is_move_assignable::value || + std::is_copy_assignable::value) && + (is_constructible::value && + std::is_same < + typename object_t::mapped_type, + typename ConstructibleObjectType::mapped_type >::value)) || + (has_from_json::value || + has_non_default_from_json < + BasicJsonType, + typename ConstructibleObjectType::mapped_type >::value); +}; + +template +struct is_constructible_object_type + : is_constructible_object_type_impl {}; + +template +struct is_compatible_string_type +{ + static constexpr auto value = + is_constructible::value; +}; + +template +struct is_constructible_string_type +{ + // launder type through decltype() to fix compilation failure on ICPC +#ifdef __INTEL_COMPILER + using laundered_type = decltype(std::declval()); +#else + using laundered_type = ConstructibleStringType; +#endif + + static constexpr auto value = + conjunction < + is_constructible, + is_detected_exact>::value; +}; + +template +struct is_compatible_array_type_impl : std::false_type {}; + +template +struct is_compatible_array_type_impl < + BasicJsonType, CompatibleArrayType, + enable_if_t < + is_detected::value&& + is_iterator_traits>>::value&& +// special case for types like std::filesystem::path whose iterator's value_type are themselves +// c.f. https://github.com/nlohmann/json/pull/3073 + !std::is_same>::value >> +{ + static constexpr bool value = + is_constructible>::value; +}; + +template +struct is_compatible_array_type + : is_compatible_array_type_impl {}; + +template +struct is_constructible_array_type_impl : std::false_type {}; + +template +struct is_constructible_array_type_impl < + BasicJsonType, ConstructibleArrayType, + enable_if_t::value >> + : std::true_type {}; + +template +struct is_constructible_array_type_impl < + BasicJsonType, ConstructibleArrayType, + enable_if_t < !std::is_same::value&& + !is_compatible_string_type::value&& + is_default_constructible::value&& +(std::is_move_assignable::value || + std::is_copy_assignable::value)&& +is_detected::value&& +is_iterator_traits>>::value&& +is_detected::value&& +// special case for types like std::filesystem::path whose iterator's value_type are themselves +// c.f. https://github.com/nlohmann/json/pull/3073 +!std::is_same>::value&& +is_complete_type < +detected_t>::value >> +{ + using value_type = range_value_t; + + static constexpr bool value = + std::is_same::value || + has_from_json::value || + has_non_default_from_json < + BasicJsonType, + value_type >::value; +}; + +template +struct is_constructible_array_type + : is_constructible_array_type_impl {}; + +template +struct is_compatible_integer_type_impl : std::false_type {}; + +template +struct is_compatible_integer_type_impl < + RealIntegerType, CompatibleNumberIntegerType, + enable_if_t < std::is_integral::value&& + std::is_integral::value&& + !std::is_same::value >> +{ + // is there an assert somewhere on overflows? + using RealLimits = std::numeric_limits; + using CompatibleLimits = std::numeric_limits; + + static constexpr auto value = + is_constructible::value && + CompatibleLimits::is_integer && + RealLimits::is_signed == CompatibleLimits::is_signed; +}; + +template +struct is_compatible_integer_type + : is_compatible_integer_type_impl {}; + +template +struct is_compatible_type_impl: std::false_type {}; + +template +struct is_compatible_type_impl < + BasicJsonType, CompatibleType, + enable_if_t::value >> +{ + static constexpr bool value = + has_to_json::value; +}; + +template +struct is_compatible_type + : is_compatible_type_impl {}; + +template +struct is_constructible_tuple : std::false_type {}; + +template +struct is_constructible_tuple> : conjunction...> {}; + +template +struct is_json_iterator_of : std::false_type {}; + +template +struct is_json_iterator_of : std::true_type {}; + +template +struct is_json_iterator_of : std::true_type +{}; + +// checks if a given type T is a template specialization of Primary +template