From 9006e8ba15968ee72240fe2513ccbcbbd4610313 Mon Sep 17 00:00:00 2001 From: fy59 Date: Tue, 1 Sep 2026 17:45:02 +0200 Subject: [PATCH] calibration: freeze tooling v1 --- docs/architecture/calibration_bootstrap.md | 28 +++ docs/roadmap/roadmap.md | 6 + include/lardon3d/calibration_tooling.h | 141 +++++++++++++ meson.build | 18 ++ src/calibration_tooling.c | 234 +++++++++++++++++++++ tests/test_calibration_tooling.c | 132 ++++++++++++ 6 files changed, 559 insertions(+) create mode 100644 include/lardon3d/calibration_tooling.h create mode 100644 src/calibration_tooling.c create mode 100644 tests/test_calibration_tooling.c diff --git a/docs/architecture/calibration_bootstrap.md b/docs/architecture/calibration_bootstrap.md index 75ae47d..1bbea67 100644 --- a/docs/architecture/calibration_bootstrap.md +++ b/docs/architecture/calibration_bootstrap.md @@ -145,4 +145,32 @@ advances the selected-execution cursor itself, and retains its published `image_ generic Task progress can become durable. Its reservation owns one CPU thread, one I/O slot and a conservative 2 GiB working allowance for the bounded 40 MP decoder and publication buffers only for the callback lifetime; this operational admission bound is not a scientific Capture-count limit. + +## Calibration Tooling v1 + +**PASS / FROZEN.** `calibration_tooling.h` is the bounded production bridge +between a completed external `CALIBRATION_SCIENCE_V1` evidence bundle and this +importer. It does not solve calibration, parse an unbounded user document, own +an acquisition session, or add a persistent schema. The caller supplies a +borrowed manifest bounded to 4,096 views and 4,096 selected image bindings, +with hashes for the target, optical state and four required evidence artifacts. +It declares and checks the frozen ChArUco family, `9 x 7` geometry, +`DICT_5X5_100`, 30.000 mm squares, 21.000 mm markers, 270.000 x 210.000 mm +active area and at least a 30.000 mm white border; the target hash binds the +corresponding immutable physical-evidence record. +Each view records an accepted/rejected decision and rejected views retain a +nonzero rejection reason; accepted statistics alone are evaluated. Each entry +carries the same optical-state manifest hash and remains in the exact selected +item order required by the importer (it is not reordered by image id). +The validator applies every hard Science v1 acceptance rule before allocating +or calling Project DB. The producer writes the exact existing `L3DCALB1` v1 +little-endian record into caller-owned storage (at most 600,000 bytes), hashes +it deterministically, and invokes only +`lardon3d_calibration_bootstrap_import(...)`. + +Invalid evidence therefore creates no calibration row and never reaches +`READY`. A valid exact retry reuses the frozen importer’s immutable +calibrations, scope and selected-execution attachment. Tooling stops at +`READY`; it never creates a Sparse SfM Task, and it cannot retro-calibrate the +historical S21 campaign. A higher-level selected-execution coordinator remains separate scope. diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index bd7b872..c99661a 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -147,6 +147,12 @@ provenance requis pour les **futures** campagnes connues-calibrées. Il ne réhabilite aucune campagne historique : S21 Engine Bay reste définitivement non rétro-calibrable. Voir [Calibration Science v1](../architecture/calibration_science_v1.md). +`CALIBRATION_TOOLING_V1=PASS/FROZEN` ajoute le pont opérationnel borné entre +un bundle d'évidence Science v1 déjà acquis et l'importeur immuable `L3DCALB1` +v1. Il valide les HARD REJECTS, produit l'artefact déterministe et s'arrête à +la transition `CALIBRATION → READY`; il ne résout aucune calibration, ne lance +pas Sparse SfM et ne rétro-calibre pas S21. + ## PHOTO QUALITY TRIAGE / ACQUISITION SELECTION — PASS / FROZEN L'étape qualité canonique implémentée se place après la découverte bornée, les diff --git a/include/lardon3d/calibration_tooling.h b/include/lardon3d/calibration_tooling.h new file mode 100644 index 0000000..865672e --- /dev/null +++ b/include/lardon3d/calibration_tooling.h @@ -0,0 +1,141 @@ +#ifndef LARDON3D_CALIBRATION_TOOLING_H +#define LARDON3D_CALIBRATION_TOOLING_H + +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + LARDON3D_CALIBRATION_TOOLING_VERSION = 1, + LARDON3D_CALIBRATION_TOOLING_MAX_VIEWS = 4096, + LARDON3D_CALIBRATION_TOOLING_MAX_COORDINATE_CHECKS = 81920, + LARDON3D_CALIBRATION_TOOLING_TARGET_MEASUREMENTS = 10, + LARDON3D_CALIBRATION_TOOLING_VALIDATION_FLAGS = 15, + LARDON3D_CALIBRATION_TOOLING_TARGET_CHARUCO_9X7_DICT_5X5_100 = 1, +}; + +typedef enum { + LARDON3D_CALIBRATION_TOOLING_OK = 0, + LARDON3D_CALIBRATION_TOOLING_INVALID_ARGUMENT, + LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED, + LARDON3D_CALIBRATION_TOOLING_CAPACITY, + LARDON3D_CALIBRATION_TOOLING_ENCODING_ERROR, + LARDON3D_CALIBRATION_TOOLING_IMPORT_ERROR, +} Lardon3DCalibrationToolingResult; + +/* One solver-reported view. `quadrant` is 0..3 for frame quadrants and 4 for + * the centre. `distance_band` is the declared near/mid/far band 0..2. + * Rejected views retain their source identity and a nonzero rejection reason; + * only accepted views contribute to the frozen acceptance statistics. The + * manifest is caller-owned and borrowed only during validation/production. */ +typedef struct { + unsigned char source_sha256[32]; + uint32_t accepted; + uint32_t rejection_reason; + uint32_t holdout; + uint32_t quadrant; + uint32_t distance_band; + uint32_t orientation_degrees; + uint32_t target_corner_quadrant_mask; + uint32_t corner_count; + uint32_t residual_count; + uint32_t high_residual_count; + double target_occupancy; + double normal_angle_degrees; + double distance_metres; + double corner_rms_px; + double clipped_fraction; + double reprojection_rmse_px; + double maximum_residual_px; +} Lardon3DCalibrationToolingView; + +/* Every selected image has exactly one artifact entry in selected-item order. + * `fit_parameters` are the deterministic 80% fit result used to verify `maximum_parameter_delta`; the + * published parameters are the complete-set result. */ +typedef struct { + uint64_t image_id; + unsigned char representation_sha256[32]; + unsigned char optical_state_sha256[32]; + uint32_t width; + uint32_t height; + double fx, fy, cx, cy, k1, k2, p1, p2; + double fit_fx, fit_fy, fit_cx, fit_cy, fit_k1, fit_k2, fit_p1, fit_p2; + double repeated_parameters[3][8]; + uint32_t support_images; + uint32_t support_observations; + double reprojection_rmse_px; + double maximum_parameter_delta; + uint32_t validation_flags; +} Lardon3DCalibrationToolingEntry; + +typedef struct { + unsigned char source_sha256[32]; + uint32_t orientation_degrees; + double dx_px; + double dy_px; +} Lardon3DCalibrationToolingCoordinateCheck; + +/* This is an operational, bounded view of a completed Science v1 bundle. WHY: + * the solver remains external, so this API validates its immutable evidence + * rather than importing a solver-private format. CONTRACT: all pointers are + * borrowed; each digest is SHA-256; no field is persisted by this API except + * through the frozen bootstrap importer. INVARIANT: entries are in selected + * item order and never include Tracks, poses, or reconstruction results. */ +typedef struct { + unsigned char target_sha256[32]; + unsigned char optical_state_sha256[32]; + unsigned char solver_executable_sha256[32]; + unsigned char solver_configuration_sha256[32]; + unsigned char initialization_evidence_sha256[32]; + unsigned char validation_evidence_sha256[32]; + uint32_t target_family; + uint32_t target_squares_x; + uint32_t target_squares_y; + double target_square_length_mm; + double target_marker_length_mm; + double target_active_width_mm; + double target_active_height_mm; + double target_white_border_mm; + double target_measurements_mm[LARDON3D_CALIBRATION_TOOLING_TARGET_MEASUREMENTS]; + double measurement_resolution_mm; + double target_flatness_mm; + double holdout_rmse_px; + double holdout_maximum_residual_px; + uint32_t extra_distortion_coefficient_count; + const Lardon3DCalibrationToolingView *views; + size_t view_count; + const Lardon3DCalibrationToolingEntry *entries; + size_t entry_count; + const Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks; + size_t coordinate_check_count; +} Lardon3DCalibrationToolingEvidence; + +/* Validate all CALIBRATION_SCIENCE_V1 hard rejects. No allocations, DB access, + * solver execution, or persistent writes occur. */ +Lardon3DCalibrationToolingResult lardon3d_calibration_tooling_validate( + const Lardon3DCalibrationToolingEvidence *evidence); + +/* Encode exactly L3DCALB1 v1 into caller storage and return its SHA-256. The + * output is deterministic for identical evidence. `written` may be NULL. */ +Lardon3DCalibrationToolingResult lardon3d_calibration_tooling_produce( + const Lardon3DCalibrationToolingEvidence *evidence, unsigned char *artifact, + size_t artifact_capacity, size_t *written, unsigned char artifact_sha256[32]); + +/* Validate, produce, then invoke only the frozen production importer. A failed + * validation/encoding never reaches the DB. Import semantics, including its + * permitted immutable-row recovery behavior, remain owned by that importer. */ +Lardon3DCalibrationToolingResult lardon3d_calibration_tooling_import( + Lardon3DProjectDb *database, uint64_t execution_id, + const Lardon3DCalibrationToolingEvidence *evidence, unsigned char *artifact, + size_t artifact_capacity, size_t *written, Lardon3DCalibrationBootstrapOutput *output); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/meson.build b/meson.build index 7a5c911..0404a6b 100644 --- a/meson.build +++ b/meson.build @@ -11,6 +11,8 @@ project( ], ) +cc = meson.get_compiler('c') + add_project_arguments( '-D_POSIX_C_SOURCE=200809L', '-Wpedantic', @@ -177,6 +179,7 @@ lardon3d_app = executable( 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/optical_profiles.c', 'src/calibration_bootstrap.c', + 'src/calibration_tooling.c', 'src/sparse_sfm_geometry.cpp', 'src/sparse_sfm_incremental.cpp', 'src/sparse_sfm_bundle_adjustment.cpp', @@ -877,6 +880,21 @@ calibration_bootstrap_test = executable( test('calibration-bootstrap', calibration_bootstrap_test, timeout: 30) +calibration_tooling_test = executable( + 'test-calibration-tooling', + sources: [ + 'tests/test_calibration_tooling.c', + 'src/calibration_tooling.c', 'src/calibration_bootstrap.c', + 'src/project_db.c', 'src/project_db_sparse_sfm.c', + 'src/task.c', 'src/resource_governor.c', 'src/resource_snapshot.c', + ], + c_args: ['-DLARDON3D_PROJECT_DB_TESTING'], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl, cc.find_library('m')], +) + +test('calibration-tooling', calibration_tooling_test, timeout: 30) + optical_profiles_test = executable( 'test-optical-profiles', sources: [ diff --git a/src/calibration_tooling.c b/src/calibration_tooling.c new file mode 100644 index 0000000..4dc7da3 --- /dev/null +++ b/src/calibration_tooling.c @@ -0,0 +1,234 @@ +#include + +#include +#include +#include +#include + +enum { kHeaderSize = 152, kEntrySize = 140, kMinimumViews = 40, kMinimumCorners = 1600 }; + +static bool nonzero(const unsigned char value[32]) { + unsigned char any = 0; + for (size_t i = 0; i < 32; ++i) any |= value[i]; + return any != 0; +} +static bool finite_value(double value) { return isfinite(value); } +static void put_u32(unsigned char *p, uint32_t value) { + for (size_t i = 0; i < 4; ++i) p[i] = (unsigned char)(value >> (8u * i)); +} +static void put_u64(unsigned char *p, uint64_t value) { + for (size_t i = 0; i < 8; ++i) p[i] = (unsigned char)(value >> (8u * i)); +} +static void put_f64(unsigned char *p, double value) { + uint64_t bits = 0; memcpy(&bits, &value, sizeof(bits)); put_u64(p, bits); +} +static bool digest(const unsigned char *p, size_t n, unsigned char out[32]) { + unsigned int length = 0; + return EVP_Digest(p, n, out, &length, EVP_sha256(), NULL) == 1 && length == 32; +} +static bool parameters_valid(const Lardon3DCalibrationToolingEntry *v, bool fit) { + const double *p = fit ? &v->fit_fx : &v->fx; + return p[0] > 0.0 && p[1] > 0.0 && p[2] >= 0.0 && p[3] >= 0.0 && + p[2] < (double)v->width && p[3] < (double)v->height && + finite_value(p[0]) && finite_value(p[1]) && finite_value(p[2]) && finite_value(p[3]) && + finite_value(p[4]) && finite_value(p[5]) && finite_value(p[6]) && finite_value(p[7]); +} +static int angle_class(double degrees) { + if (degrees < 20.0 || degrees > 60.0) return -1; + return degrees <= 35.0 ? 0 : (degrees <= 50.0 ? 1 : 2); +} +static unsigned int popcount4(uint32_t value) { + unsigned int count = 0; + for (unsigned int bit = 0; bit < 4; ++bit) count += (value >> bit) & 1u; + return count; +} +static bool projected_delta_ok(const Lardon3DCalibrationToolingEntry *v) { + static const double rays[5][2] = {{0, 0}, {-0.7, -0.7}, {0.7, -0.7}, + {-0.7, 0.7}, {0.7, 0.7}}; + const double *a = &v->fx, *b = &v->fit_fx; + double maximum = 0.0; + for (size_t i = 0; i < 5; ++i) { + double x = rays[i][0], y = rays[i][1], r2 = x*x + y*y; + double ax = x*(1+a[4]*r2+a[5]*r2*r2)+2*a[6]*x*y+a[7]*(r2+2*x*x); + double ay = y*(1+a[4]*r2+a[5]*r2*r2)+a[6]*(r2+2*y*y)+2*a[7]*x*y; + double bx = x*(1+b[4]*r2+b[5]*r2*r2)+2*b[6]*x*y+b[7]*(r2+2*x*x); + double by = y*(1+b[4]*r2+b[5]*r2*r2)+b[6]*(r2+2*y*y)+2*b[7]*x*y; + double delta = hypot(a[0]*ax+a[2] - (b[0]*bx+b[2]), + a[1]*ay+a[3] - (b[1]*by+b[3])); + if (!finite_value(delta)) return false; + if (delta > maximum) maximum = delta; + } + return finite_value(v->maximum_parameter_delta) && + fabs(v->maximum_parameter_delta - maximum) <= 1e-12 && maximum <= 0.10; +} + +Lardon3DCalibrationToolingResult lardon3d_calibration_tooling_validate( + const Lardon3DCalibrationToolingEvidence *e) { + if (!e || !e->views || !e->entries || !e->coordinate_checks || e->view_count > LARDON3D_CALIBRATION_TOOLING_MAX_VIEWS || + e->entry_count == 0 || e->entry_count > 4096 || e->coordinate_check_count > LARDON3D_CALIBRATION_TOOLING_MAX_COORDINATE_CHECKS) + return LARDON3D_CALIBRATION_TOOLING_INVALID_ARGUMENT; + if (!nonzero(e->target_sha256) || !nonzero(e->optical_state_sha256) || + !nonzero(e->solver_executable_sha256) || !nonzero(e->solver_configuration_sha256) || + !nonzero(e->initialization_evidence_sha256) || !nonzero(e->validation_evidence_sha256) || + e->target_family != LARDON3D_CALIBRATION_TOOLING_TARGET_CHARUCO_9X7_DICT_5X5_100 || + e->target_squares_x != 9 || e->target_squares_y != 7 || + !finite_value(e->target_square_length_mm) || e->target_square_length_mm != 30.0 || + !finite_value(e->target_marker_length_mm) || e->target_marker_length_mm != 21.0 || + !finite_value(e->target_active_width_mm) || e->target_active_width_mm != 270.0 || + !finite_value(e->target_active_height_mm) || e->target_active_height_mm != 210.0 || + !finite_value(e->target_white_border_mm) || e->target_white_border_mm < 30.0 || + e->extra_distortion_coefficient_count != 0 || !finite_value(e->measurement_resolution_mm) || + e->measurement_resolution_mm <= 0 || e->measurement_resolution_mm > 0.1 || + !finite_value(e->target_flatness_mm) || e->target_flatness_mm < 0 || e->target_flatness_mm > 0.20) + return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + double lo = 30.0, hi = 30.0; + for (size_t i = 0; i < LARDON3D_CALIBRATION_TOOLING_TARGET_MEASUREMENTS; ++i) { + if (!finite_value(e->target_measurements_mm[i]) || fabs(e->target_measurements_mm[i] - 30.0) > 0.30) + return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + if (e->target_measurements_mm[i] < lo) lo = e->target_measurements_mm[i]; + if (e->target_measurements_mm[i] > hi) hi = e->target_measurements_mm[i]; + } + if (hi - lo > 0.20 || !finite_value(e->holdout_rmse_px) || !finite_value(e->holdout_maximum_residual_px) || + e->holdout_rmse_px < 0 || e->holdout_maximum_residual_px < 0 || + e->holdout_rmse_px > 0.75 || e->holdout_maximum_residual_px > 1.50) + return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + uint64_t corners = 0, residuals = 0, high = 0; + size_t accepted_views = 0; + uint32_t quadrant[5] = {0}, distance[3] = {0}, angles[3] = {0}, inclined = 0; + size_t fit_views = 0, holdout_views = 0, fit_quadrant[4] = {0}; + double min_distance = INFINITY, max_distance = 0.0; + for (size_t i = 0; i < e->view_count; ++i) { + const Lardon3DCalibrationToolingView *v = &e->views[i]; + if (!nonzero(v->source_sha256) || v->accepted > 1 || v->holdout > 1 || + (!v->accepted && v->rejection_reason == 0) || + (v->accepted && v->rejection_reason != 0)) + return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + if (!v->accepted) { + if (v->holdout != 0) return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + continue; + } + if (v->quadrant > 4 || v->distance_band > 2 || + (v->orientation_degrees != 0 && v->orientation_degrees != 90 && + v->orientation_degrees != 180 && v->orientation_degrees != 270) || + popcount4(v->target_corner_quadrant_mask) < 3 || + v->corner_count < 16 || !finite_value(v->target_occupancy) || v->target_occupancy < .20 || v->target_occupancy > .80 || + !finite_value(v->normal_angle_degrees) || !finite_value(v->distance_metres) || v->distance_metres <= 0 || + !finite_value(v->corner_rms_px) || v->corner_rms_px < 0 || v->corner_rms_px > .25 || !finite_value(v->clipped_fraction) || v->clipped_fraction < 0 || v->clipped_fraction > .01 || + !finite_value(v->reprojection_rmse_px) || v->reprojection_rmse_px < 0 || v->reprojection_rmse_px > .75 || + !finite_value(v->maximum_residual_px) || v->maximum_residual_px < 0 || v->maximum_residual_px > 1.50) + return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + if (v->high_residual_count > v->residual_count || UINT64_MAX - corners < v->corner_count || UINT64_MAX - residuals < v->residual_count || UINT64_MAX - high < v->high_residual_count) + return LARDON3D_CALIBRATION_TOOLING_CAPACITY; + ++accepted_views; + corners += v->corner_count; residuals += v->residual_count; high += v->high_residual_count; + ++quadrant[v->quadrant]; ++distance[v->distance_band]; + int class_index = angle_class(v->normal_angle_degrees); + if (class_index >= 0) { ++angles[class_index]; ++inclined; } + if (v->holdout) ++holdout_views; + else { ++fit_views; if (v->quadrant < 4) ++fit_quadrant[v->quadrant]; } + if (v->distance_metres < min_distance) min_distance = v->distance_metres; + if (v->distance_metres > max_distance) max_distance = v->distance_metres; + } + if (accepted_views < kMinimumViews || corners < kMinimumCorners || quadrant[0] < 6 || quadrant[1] < 6 || quadrant[2] < 6 || quadrant[3] < 6 || quadrant[4] < 8 || + distance[0] < 8 || distance[1] < 8 || distance[2] < 8 || max_distance / min_distance < 1.5 || + inclined < 24 || angles[0] < 6 || angles[1] < 6 || angles[2] < 6 || fit_views < 32 || holdout_views < 8 || + fit_quadrant[0] < 4 || fit_quadrant[1] < 4 || fit_quadrant[2] < 4 || fit_quadrant[3] < 4 || + residuals == 0 || high * 100 > residuals) + return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + for (size_t i = 0; i < e->view_count; ++i) { + const Lardon3DCalibrationToolingView *v = &e->views[i]; + if (!v->accepted) continue; + size_t rank = 0; + int class_index = angle_class(v->normal_angle_degrees); + for (size_t other = 0; other < e->view_count; ++other) { + const Lardon3DCalibrationToolingView *candidate = &e->views[other]; + if (!candidate->accepted || candidate->quadrant != v->quadrant || + candidate->distance_band != v->distance_band || + angle_class(candidate->normal_angle_degrees) != class_index) continue; + int compare = memcmp(candidate->source_sha256, v->source_sha256, 32); + if (compare == 0 && other != i) return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + if (compare < 0) ++rank; + } + if (v->holdout != (rank % 5 == 4)) return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + } + for (size_t i = 0; i < e->coordinate_check_count; ++i) { + const Lardon3DCalibrationToolingCoordinateCheck *c = &e->coordinate_checks[i]; + if (!nonzero(c->source_sha256) || (c->orientation_degrees != 0 && c->orientation_degrees != 90 && + c->orientation_degrees != 180 && c->orientation_degrees != 270) || + !finite_value(c->dx_px) || !finite_value(c->dy_px) || fabs(c->dx_px) > .01 || fabs(c->dy_px) > .01) + return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + } + for (size_t view_index = 0; view_index < e->view_count; ++view_index) { + const Lardon3DCalibrationToolingView *v = &e->views[view_index]; + if (!v->accepted) continue; + size_t matches = 0; + for (size_t check_index = 0; check_index < e->coordinate_check_count; ++check_index) { + const Lardon3DCalibrationToolingCoordinateCheck *c = &e->coordinate_checks[check_index]; + if (memcmp(c->source_sha256, v->source_sha256, 32) == 0) { + if (c->orientation_degrees != v->orientation_degrees) + return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + ++matches; + } + } + if (matches < 20) return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + } + for (size_t i = 0; i < e->entry_count; ++i) { + const Lardon3DCalibrationToolingEntry *v = &e->entries[i]; + if (!v->image_id || !nonzero(v->representation_sha256) || + memcmp(v->optical_state_sha256, e->optical_state_sha256, 32) != 0 || + !v->width || !v->height || + !parameters_valid(v, false) || !parameters_valid(v, true) || !v->support_images || !v->support_observations || + !finite_value(v->reprojection_rmse_px) || v->reprojection_rmse_px < 0 || v->reprojection_rmse_px > .50 || + v->validation_flags != LARDON3D_CALIBRATION_TOOLING_VALIDATION_FLAGS || !projected_delta_ok(v)) + return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + for (size_t previous = 0; previous < i; ++previous) + if (v->image_id == e->entries[previous].image_id) + return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + const double published[8] = {v->fx,v->fy,v->cx,v->cy,v->k1,v->k2,v->p1,v->p2}; + for (size_t repeat = 0; repeat < 3; ++repeat) + for (size_t parameter = 0; parameter < 8; ++parameter) + if (!finite_value(v->repeated_parameters[repeat][parameter]) || + v->repeated_parameters[repeat][parameter] != published[parameter]) + return LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED; + } + return LARDON3D_CALIBRATION_TOOLING_OK; +} + +Lardon3DCalibrationToolingResult lardon3d_calibration_tooling_produce( + const Lardon3DCalibrationToolingEvidence *e, unsigned char *artifact, + size_t capacity, size_t *written, unsigned char hash[32]) { + if (written) *written = 0; + if (!artifact || !hash) return LARDON3D_CALIBRATION_TOOLING_INVALID_ARGUMENT; + Lardon3DCalibrationToolingResult valid = lardon3d_calibration_tooling_validate(e); + if (valid != LARDON3D_CALIBRATION_TOOLING_OK) return valid; + if (e->entry_count > (SIZE_MAX - kHeaderSize) / kEntrySize) return LARDON3D_CALIBRATION_TOOLING_CAPACITY; + size_t size = kHeaderSize + e->entry_count * kEntrySize; + if (size > LARDON3D_CALIBRATION_BOOTSTRAP_MAX_BYTES || capacity < size) return LARDON3D_CALIBRATION_TOOLING_CAPACITY; + size_t at = 0; memcpy(artifact + at, "L3DCALB1", 8); at += 8; + put_u32(artifact + at, 1); at += 4; put_u32(artifact + at, 1); at += 4; put_u32(artifact + at, 1); at += 4; put_u32(artifact + at, (uint32_t)e->entry_count); at += 4; + memcpy(artifact + at, e->solver_executable_sha256, 32); at += 32; memcpy(artifact + at, e->solver_configuration_sha256, 32); at += 32; + memcpy(artifact + at, e->initialization_evidence_sha256, 32); at += 32; memcpy(artifact + at, e->validation_evidence_sha256, 32); at += 32; + for (size_t i = 0; i < e->entry_count; ++i) { + const Lardon3DCalibrationToolingEntry *v = &e->entries[i]; + put_u64(artifact + at, v->image_id); at += 8; memcpy(artifact + at, v->representation_sha256, 32); at += 32; + put_u32(artifact + at, v->width); at += 4; put_u32(artifact + at, v->height); at += 4; + const double values[8] = {v->fx,v->fy,v->cx,v->cy,v->k1,v->k2,v->p1,v->p2}; + for (size_t j = 0; j < 8; ++j) { put_f64(artifact + at, values[j]); at += 8; } + put_u32(artifact + at, v->support_images); at += 4; put_u32(artifact + at, v->support_observations); at += 4; + put_f64(artifact + at, v->reprojection_rmse_px); at += 8; put_f64(artifact + at, v->maximum_parameter_delta); at += 8; + put_u32(artifact + at, v->validation_flags); at += 4; + } + if (at != size || !digest(artifact, size, hash)) return LARDON3D_CALIBRATION_TOOLING_ENCODING_ERROR; + if (written) *written = size; + return LARDON3D_CALIBRATION_TOOLING_OK; +} + +Lardon3DCalibrationToolingResult lardon3d_calibration_tooling_import( + Lardon3DProjectDb *db, uint64_t execution_id, const Lardon3DCalibrationToolingEvidence *e, + unsigned char *artifact, size_t capacity, size_t *written, Lardon3DCalibrationBootstrapOutput *out) { + unsigned char hash[32]; + Lardon3DCalibrationToolingResult result = lardon3d_calibration_tooling_produce(e, artifact, capacity, written, hash); + if (result != LARDON3D_CALIBRATION_TOOLING_OK || !db || !execution_id || !out) return result == LARDON3D_CALIBRATION_TOOLING_OK ? LARDON3D_CALIBRATION_TOOLING_INVALID_ARGUMENT : result; + return lardon3d_calibration_bootstrap_import(db, execution_id, artifact, *written, hash, out) == LARDON3D_CALIBRATION_BOOTSTRAP_OK ? + LARDON3D_CALIBRATION_TOOLING_OK : LARDON3D_CALIBRATION_TOOLING_IMPORT_ERROR; +} diff --git a/tests/test_calibration_tooling.c b/tests/test_calibration_tooling.c new file mode 100644 index 0000000..790ea68 --- /dev/null +++ b/tests/test_calibration_tooling.c @@ -0,0 +1,132 @@ +#include +#include +#include +#include +#include +#include + +#include + +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "tooling failure %d: %s\n", __LINE__, #x); return false; } } while (0) + +static bool sql(const char *path, const char *text) { + sqlite3 *db = NULL; if (sqlite3_open(path, &db) != SQLITE_OK) return false; + int rc = sqlite3_exec(db, text, NULL, NULL, NULL); return sqlite3_close(db) == SQLITE_OK && rc == SQLITE_OK; +} +static bool scalar(const char *path, const char *text, int *value) { + sqlite3 *db = NULL; sqlite3_stmt *statement = NULL; + if (sqlite3_open(path, &db) != SQLITE_OK || sqlite3_prepare_v2(db, text, -1, &statement, NULL) != SQLITE_OK) { + if (statement) sqlite3_finalize(statement); + if (db) sqlite3_close(db); + return false; + } + bool ok = sqlite3_step(statement) == SQLITE_ROW; + if (ok) *value = sqlite3_column_int(statement, 0); + return sqlite3_finalize(statement) == SQLITE_OK && sqlite3_close(db) == SQLITE_OK && ok; +} +static void hashes(unsigned char value[32], unsigned char seed) { memset(value, seed, 32); } +static void params(double output[8]) { const double p[8] = {3000,3001,2000,1100,.01,-.01,.001,-.001}; memcpy(output,p,sizeof(p)); } + +/* Synthetic only: these values model an already-completed external session and + * never represent a physical calibration or a real project. */ +static void fixture(Lardon3DCalibrationToolingEvidence *e, Lardon3DCalibrationToolingView views[60], + Lardon3DCalibrationToolingEntry entries[1], Lardon3DCalibrationToolingCoordinateCheck coordinates[1200]) { + memset(e, 0, sizeof(*e)); memset(views, 0, sizeof(Lardon3DCalibrationToolingView)*60); + memset(entries, 0, sizeof(Lardon3DCalibrationToolingEntry)); memset(coordinates, 0, sizeof(Lardon3DCalibrationToolingCoordinateCheck)*1200); + hashes(e->target_sha256,1); hashes(e->optical_state_sha256,2); hashes(e->solver_executable_sha256,3); hashes(e->solver_configuration_sha256,4); hashes(e->initialization_evidence_sha256,5); hashes(e->validation_evidence_sha256,6); + e->target_family=LARDON3D_CALIBRATION_TOOLING_TARGET_CHARUCO_9X7_DICT_5X5_100; e->target_squares_x=9; e->target_squares_y=7; + e->target_square_length_mm=30; e->target_marker_length_mm=21; e->target_active_width_mm=270; e->target_active_height_mm=210; e->target_white_border_mm=30; + for (size_t i=0;i<10;++i) e->target_measurements_mm[i]=30.0; + e->measurement_resolution_mm=.1; e->target_flatness_mm=.1; e->holdout_rmse_px=.4; e->holdout_maximum_residual_px=.8; + for (size_t i=0;i<60;++i) { + static const uint32_t quadrants[10]={0,0,1,1,2,2,3,3,4,4}; + static const uint32_t distances[10]={0,1,2,0,1,2,0,1,2,0}; + static const double angles[10]={25,40,55,25,40,55,25,40,55,25}; + size_t group=i/6; hashes(views[i].source_sha256,(unsigned char)(i+20)); views[i].accepted=1; views[i].holdout=(i%6)==4; views[i].quadrant=quadrants[group]; + views[i].distance_band=distances[group]; views[i].target_corner_quadrant_mask=7; views[i].corner_count=40; views[i].residual_count=40; views[i].target_occupancy=.4; + views[i].normal_angle_degrees=angles[group]; views[i].distance_metres=1.0+(double)distances[group]*.3; + views[i].corner_rms_px=.1; views[i].clipped_fraction=0; views[i].reprojection_rmse_px=.4; views[i].maximum_residual_px=.8; + } + entries[0].image_id=1; hashes(entries[0].representation_sha256,0x11); hashes(entries[0].optical_state_sha256,2); entries[0].width=4000; entries[0].height=2250; + double p[8]; params(p); memcpy(&entries[0].fx,p,sizeof(p)); memcpy(&entries[0].fit_fx,p,sizeof(p)); + for (size_t repeat=0;repeat<3;++repeat) memcpy(entries[0].repeated_parameters[repeat],p,sizeof(p)); + entries[0].support_images=60; entries[0].support_observations=2400; entries[0].reprojection_rmse_px=.4; entries[0].maximum_parameter_delta=0; entries[0].validation_flags=15; + for(size_t i=0;i<1200;++i) { memcpy(coordinates[i].source_sha256,views[i/20].source_sha256,32); coordinates[i].orientation_degrees=0; coordinates[i].dx_px=.001; coordinates[i].dy_px=-.001; } + e->views=views; e->view_count=60; e->entries=entries; e->entry_count=1; e->coordinate_checks=coordinates; e->coordinate_check_count=1200; +} + +static bool rejects_major_cases(void) { + Lardon3DCalibrationToolingEvidence e; Lardon3DCalibrationToolingView v[60]; Lardon3DCalibrationToolingEntry x[1]; Lardon3DCalibrationToolingCoordinateCheck c[1200]; + fixture(&e,v,x,c); CHECK(lardon3d_calibration_tooling_validate(&e)==LARDON3D_CALIBRATION_TOOLING_OK); + e.view_count=39; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); e.view_count=60; + v[0].corner_count=1; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); v[0].corner_count=40; + for (size_t i=0;i<3;++i) v[i].quadrant=4; + CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); + for (size_t i=0;i<3;++i) v[i].quadrant=0; + for (size_t i=0;i<9;++i) v[i*3].normal_angle_degrees=1; + CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); + for (size_t i=0;i<9;++i) v[i*3].normal_angle_degrees=25; + for (size_t i=0;i<7;++i) v[i*3].distance_band=1; + CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); + for (size_t i=0;i<7;++i) v[i*3].distance_band=0; + e.target_marker_length_mm=20; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); e.target_marker_length_mm=21; + e.target_measurements_mm[0]=31; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); e.target_measurements_mm[0]=30; + v[0].corner_rms_px=.3; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); v[0].corner_rms_px=.1; + x[0].reprojection_rmse_px=.6; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); x[0].reprojection_rmse_px=.4; + v[0].reprojection_rmse_px=.8; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); v[0].reprojection_rmse_px=.4; + v[0].maximum_residual_px=2; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); v[0].maximum_residual_px=.8; + v[0].high_residual_count=40; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); v[0].high_residual_count=0; + e.holdout_rmse_px=.8; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); e.holdout_rmse_px=.4; + x[0].repeated_parameters[1][0]+=1; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); x[0].repeated_parameters[1][0]-=1; + x[0].maximum_parameter_delta=.2; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); x[0].maximum_parameter_delta=0; + fixture(&e,v,x,c); for (size_t i=18;i<60;++i) v[i].normal_angle_degrees=0; + CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); + fixture(&e,v,x,c); v[4].holdout=0; + CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); + fixture(&e,v,x,c); v[0].corner_rms_px=-.1; + CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); + fixture(&e,v,x,c); x[0].reprojection_rmse_px=-.1; + CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); + fixture(&e,v,x,c); memset(e.solver_configuration_sha256,0,32); + CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); + fixture(&e,v,x,c); + v[0].accepted=0; v[0].rejection_reason=7; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); v[0].accepted=1; v[0].rejection_reason=0; + memset(e.optical_state_sha256,0,32); CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); hashes(e.optical_state_sha256,2); + e.coordinate_check_count=1199; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); e.coordinate_check_count=1200; + x[0].width=0; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); x[0].width=4000; + x[0].fx=0; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); x[0].fx=3000; + x[0].cx=4000; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); x[0].cx=2000; + e.extra_distortion_coefficient_count=1; CHECK(lardon3d_calibration_tooling_validate(&e)!=LARDON3D_CALIBRATION_TOOLING_OK); + return true; +} + +static bool run(void) { + CHECK(rejects_major_cases()); + Lardon3DCalibrationToolingEvidence e; Lardon3DCalibrationToolingView views[60]; Lardon3DCalibrationToolingEntry entries[1]; Lardon3DCalibrationToolingCoordinateCheck coords[1200]; fixture(&e,views,entries,coords); + unsigned char a[292], b[292], ha[32], hb[32]; size_t na=0, nb=0; + CHECK(lardon3d_calibration_tooling_produce(&e,a,sizeof(a),&na,ha)==LARDON3D_CALIBRATION_TOOLING_OK); + CHECK(lardon3d_calibration_tooling_produce(&e,b,sizeof(b),&nb,hb)==LARDON3D_CALIBRATION_TOOLING_OK && na==292 && na==nb && memcmp(a,b,na)==0 && memcmp(ha,hb,32)==0); + CHECK(lardon3d_calibration_tooling_produce(&e,b,291,&nb,hb)==LARDON3D_CALIBRATION_TOOLING_CAPACITY); + char dir[]="/tmp/lardon3d-calibration-tooling-XXXXXX"; CHECK(mkdtemp(dir)); char path[512]; CHECK(snprintf(path,sizeof(path),"%s/project.db",dir)>0); + Lardon3DProjectDb *db=NULL; char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; CHECK(lardon3d_project_db_open(path,&db,error)==LARDON3D_PROJECT_DB_OK); lardon3d_project_db_close(db); + CHECK(sql(path,"INSERT INTO scansets VALUES(1,'s',1,1); INSERT INTO tasks VALUES(1,'q','photo_quality.triage',1,5,5,100,1,0,0,0,0,1); INSERT INTO tasks VALUES(2,'c','acquisition_campaign.run',1,5,5,100,1,0,0,0,0,1); INSERT INTO photo_quality_triage_tasks VALUES(1,1,2,1,X'01'); INSERT INTO photo_quality_triage_results VALUES(1,1,0,1,0,1,0,100,100,100,100,1,1,0,0,1,1,0,'GOOD'); INSERT INTO acquisition_campaign_tasks VALUES(2,1,2,2,X'02'); INSERT INTO captures VALUES(1,1,1); INSERT INTO acquisition_campaign_captures VALUES(2,2,1); INSERT INTO image_assets VALUES(1,X'1111111111111111111111111111111111111111111111111111111111111111','assets/images/11/1111111111111111111111111111111111111111111111111111111111111111',1,1,1); INSERT INTO images VALUES(1,1,1,'a.jpg','/a.jpg',NULL,1); INSERT INTO capture_images VALUES(1,1);")); + CHECK(lardon3d_project_db_open(path,&db,error)==LARDON3D_PROJECT_DB_OK); Lardon3DProjectDbSelectedExecutionItem item={.item_index=0,.quality_group_id=1,.campaign_group_id=2,.capture_id=1,.representation_source=LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE}; Lardon3DProjectDbSelectedExecution execution; + CHECK(lardon3d_project_db_create_selected_execution(db,1,2,&item,1,1,&execution)==LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_record_selected_representation(db,execution.execution_id,0,1,1)==LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbSelectedExecutionItem selected; Lardon3DProjectDbImage image; Lardon3DProjectDbImageAsset asset; + CHECK(lardon3d_project_db_load_selected_execution_item(db,execution.execution_id,0,&selected)==LARDON3D_PROJECT_DB_OK && selected.has_image && selected.image_id==1); + CHECK(lardon3d_project_db_load_image(db,1,&image,&asset)==LARDON3D_PROJECT_DB_OK && memcmp(asset.sha256,entries[0].representation_sha256,32)==0); + entries[0].fx=0; + CHECK(lardon3d_calibration_tooling_import(db,execution.execution_id,&e,a,sizeof(a),&na,&(Lardon3DCalibrationBootstrapOutput){0})==LARDON3D_CALIBRATION_TOOLING_SCIENCE_REJECTED); + entries[0].fx=3000; + lardon3d_project_db_close(db); + int count=0; CHECK(scalar(path,"SELECT COUNT(*) FROM sparse_calibrations",&count) && count==0); + CHECK(scalar(path,"SELECT COUNT(*) FROM sparse_calibration_scopes",&count) && count==0); + CHECK(lardon3d_project_db_open(path,&db,error)==LARDON3D_PROJECT_DB_OK); + Lardon3DCalibrationBootstrapOutput output; CHECK(lardon3d_calibration_tooling_import(db,execution.execution_id,&e,a,sizeof(a),&na,&output)==LARDON3D_CALIBRATION_TOOLING_OK); uint64_t scope=output.scope.scope_id; + CHECK(lardon3d_calibration_tooling_import(db,execution.execution_id,&e,a,sizeof(a),&na,&output)==LARDON3D_CALIBRATION_TOOLING_OK && output.scope.scope_id==scope); + Lardon3DSparseCalibrationMember member; size_t member_count=0; uint64_t next=0; + CHECK(lardon3d_sparse_calibration_scope_list_members(db,scope,0,&member,1,&member_count,&next)==LARDON3D_PROJECT_DB_OK && member_count==1 && next==1 && member.image_id==1); + CHECK(lardon3d_project_db_load_selected_execution(db,execution.execution_id,&execution)==LARDON3D_PROJECT_DB_OK && execution.stage==LARDON3D_SELECTED_EXECUTION_READY && execution.calibration_scope_id==scope); + lardon3d_project_db_close(db); CHECK(unlink(path)==0); CHECK(rmdir(dir)==0); return true; +} +int main(void) { return run()?EXIT_SUCCESS:EXIT_FAILURE; }