From 138b13babcf414af61efa0d248991382a8827503 Mon Sep 17 00:00:00 2001 From: fy59 Date: Tue, 25 Aug 2026 23:27:14 +0200 Subject: [PATCH] feat: complete incremental reconstruction phase H --- docs/architecture/overview.md | 7 + docs/architecture/project_database.md | 45 +- docs/architecture/reconstruction_pipeline.md | 122 ++- docs/architecture/task_kind_registry.md | 7 + docs/roadmap/roadmap.md | 4 +- include/lardon3d/app_state.h | 6 +- include/lardon3d/incremental_reconstruction.h | 102 +++ .../incremental_reconstruction_task.h | 42 + include/lardon3d/project_db.h | 21 +- include/lardon3d/sparse_sfm_model.h | 24 + meson.build | 22 + src/incremental_reconstruction.cpp | 786 ++++++++++++++++++ src/incremental_reconstruction_identity.cpp | 122 +++ src/incremental_reconstruction_internal.h | 22 + src/incremental_reconstruction_task.cpp | 764 +++++++++++++++++ src/project_db.c | 270 +++++- src/project_db_sparse_sfm.c | 244 +++++- src/sparse_sfm_bundle_adjustment.cpp | 74 +- src/sparse_sfm_gate_f.cpp | 27 + src/sparse_sfm_gate_f_internal.h | 5 + src/sparse_sfm_task.cpp | 30 +- src/task_kinds.c | 10 + tests/test_geometric_verification.c | 10 +- tests/test_geometric_verifier_task.c | 4 +- tests/test_incremental_reconstruction.cpp | 473 +++++++++++ tests/test_match_result.c | 12 +- tests/test_matcher_task.c | 4 +- tests/test_project_db.c | 124 ++- tests/test_sparse_sfm_model.c | 120 ++- tests/test_sparse_sfm_payload.cpp | 33 +- tests/test_sparse_sfm_task.cpp | 290 ++++++- 31 files changed, 3708 insertions(+), 118 deletions(-) create mode 100644 include/lardon3d/incremental_reconstruction.h create mode 100644 include/lardon3d/incremental_reconstruction_task.h create mode 100644 src/incremental_reconstruction.cpp create mode 100644 src/incremental_reconstruction_identity.cpp create mode 100644 src/incremental_reconstruction_internal.h create mode 100644 src/incremental_reconstruction_task.cpp create mode 100644 tests/test_incremental_reconstruction.cpp diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 96cea1f..15ae726 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -142,6 +142,13 @@ et à la publication Project DB v17 atomique et idempotente. **Statut :** GATE F — PASS / FROZEN. Gate G est **PASS / FROZEN**. +La phase de pipeline H `incremental_reconstruction.run` v1 enrichit un snapshot +publié et produit un nouveau snapshot complet avec provenance prédécesseur dans +Project DB v18. Elle reste scientifiquement distincte de Gate F et ne modifie +ni F0 ni les contrats Gate D/E. + +**Statut :** PHASE H V1 — PASS / FROZEN. + ## Résultats et publication live Les traitements fonctionnent par séquences adaptatives : lire un lot borné, diff --git a/docs/architecture/project_database.md b/docs/architecture/project_database.md index ddeb088..6ed00ef 100644 --- a/docs/architecture/project_database.md +++ b/docs/architecture/project_database.md @@ -1,6 +1,33 @@ # Base de données projet Lardon3D -## Gate F — décision Project DB v17 +## Phase H — décision Project DB v18 + +**PASS / FROZEN.** La migration transactionnelle v17→v18 +ajoute `derivation_identity` au parent `sparse_reconstructions`, puis crée +`incremental_reconstructions` et `incremental_reconstruction_tasks`. +Les lignes Gate F ont `derivation_identity IS NULL` et gardent exactement leur +`parameter_fingerprint`. Leur tuple candidat historique est imposé par un +index UNIQUE partiel. Les lignes dérivées H stockent le vrai fingerprint H +dans `parameter_fingerprint` et l'identité scientifique H canonique dans +`derivation_identity`, elle-même protégée par un second index UNIQUE partiel. +La reconstruction transactionnelle du parent et de ses tables filles conserve +les IDs, lignes, clés étrangères et cascades v17 sans réécrire les données +scientifiques. +La première table associe atomiquement un snapshot Sparse SfM complet à son +prédécesseur, au Track Set d'extension, au scope de calibration, au kind/version +H, au fingerprint H et à son identité scientifique SHA-256 unique. La seconde +conserve le payload immuable de la tâche durable H. Aucun état de réservation, +snapshot de ressources, géométrie partielle, sous-ensemble d'observations, +remappage de composante ou graphe de filiation n'est persisté. Un redémarrage +recalcule depuis les entrées immuables. + +La publication H réutilise sans le relâcher le validateur structurel historique +du snapshot complet. Géométrie et métadonnées H sont insérées dans une même +transaction ; une erreur tardive de métadonnées annule aussi toute la nouvelle +géométrie. Les lignes Gate F v16/v17 restent inchangées et leurs recherches +exactes conservent leur sémantique. + +## Gate F — décision historique Project DB v17 **PASS / FROZEN.** La migration historique v15→v16 et le modèle de reconstruction Sparse SfM v16 restent inchangés. Gate F fait avancer @@ -38,7 +65,9 @@ persistée exactement ; seule une composante de graphe non reconstruite est omise. Les comptes et métriques globaux décrivent exclusivement la géométrie effectivement persistée. Aucun placeholder ni table diagnostique n'est ajouté. -> Version courante : **v17**. La migration transactionnelle v16→v17 ajoute le +> Version courante : **v18**. La migration transactionnelle v17→v18 ajoute le +> discriminateur générique nullable et les deux tables H minimales décrites +> ci-dessus. La migration v16→v17 ajoute le > payload durable typé `sparse_sfm_tasks`. La migration transactionnelle v15→v16 ajoute > le modèle persistant Sparse SfM (calibrations, scopes et reconstructions). > La migration transactionnelle v14→v15 ajoute @@ -779,8 +808,8 @@ ouvert. ## Statut -**IMPLEMENTED** — SQLite système, schéma v15 et migrations séquentielles -v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11→v12→v13→v14→v15, identité projet, transactions +**IMPLEMENTED** — SQLite système, schéma v18 et migrations séquentielles +v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11→v12→v13→v14→v15→v16→v17→v18, identité projet, transactions tâche+checkpoint, pagination de reprise et artefacts génériques. **IMPLEMENTED** — ouverture/fermeture avec le projet, identité INI/DB cohérente, @@ -837,7 +866,7 @@ orphelins et compaction Visual Index. Feature Store et Visual Index v1 sont impl Visual Index v1 borne un index à 256 segments de 16 memberships, soit 4096 Feature Sets; la couverture de 50 000 Feature Sets nécessitera la compaction ou une évolution v2. -**PLANNED** — dépendances d'artefacts, graphe géométrique et orchestration -projet/tâche de la reconstruction incrémentale. Le noyau Sparse SfM -incrémental synchrone en mémoire Gate D est **IMPLEMENTED / PASS** hors -Project DB v16. +**PASS / FROZEN** — Phase H v1 : métadonnées d'identité et +de prédécesseur dans `incremental_reconstructions`, payload durable dans +`incremental_reconstruction_tasks`, snapshot complet publié atomiquement et +réutilisation par identité H. Aucun graphe de dépendances n'est introduit. diff --git a/docs/architecture/reconstruction_pipeline.md b/docs/architecture/reconstruction_pipeline.md index 23ed939..24d82e6 100644 --- a/docs/architecture/reconstruction_pipeline.md +++ b/docs/architecture/reconstruction_pipeline.md @@ -144,13 +144,112 @@ tâche Gate F est l'ajout v17. ### H. Reconstruction incrémentale -| Aspect | Description | -|--------|-------------| -| **Indexation** | À chaque vague d'images, la reconstruction existante est indexée (positions approximatives des points 3D, orientations des caméras). | -| **Comparaison aux acquisitions précédentes** | Les nouvelles images sont comparées à la reconstruction existante : localisation des caméras, triangulation de nouveaux points, mise à jour des tracks existants. | -| **Enrichissement local** | Seules les régions couvertes par les nouvelles images sont recalculées. Le reste de la reconstruction reste inchangé et valide. | +La phase H est une étape du pipeline, pas un « Gate H ». Sa version 1 enrichit +scientifiquement un snapshot Sparse SfM publié, sans réexécuter Gate F depuis +zéro. Ses entrées immuables sont l'identité de la reconstruction de base, +l'identité d'un Track Set d'extension et l'identité d'un scope de calibration. +Son identité scientifique est : -**Statut :** PLANNED — aucune implémentation existante. +``` +(base_reconstruction_identity, extension_track_set_identity, + calibration_scope_identity, incremental_kind, incremental_version, + parameter_fingerprint) +``` + +L'enregistrement canonique `L3DHIDV1` est encodé little-endian puis condensé +par SHA-256. Le fingerprint H v1, distinct de F0, encode `L3DHPRM1`, sa version +et les politiques versionnées suivantes : filiation stricte par identité +d'observation `(feature_set_id, feature_index)`, absence de split/merge, +`STRICT_DESCENDANT_REGISTERED_OBSERVATIONS_V1`, +`PRESERVE_BASE_COMPONENT_KEY_V1`, absence de fusion de composantes de base, +politiques scientifiques Gate D v1 pour PnP/triangulation/raffinement, BA +complète Gate E v1 de chaque composante affectée, calibration historique +exacte, préservation du gauge historique, snapshot complet immuable, +ordre canonique et absence de nouvelle composante déconnectée. Matériel, +ressources, tâche, chemins, horodatages et télémétrie sont exclus. +L'enregistrement de fingerprint fait exactement 80 octets ; son vecteur doré +SHA-256 v1 est +`f44a89b23b520481701848ecf175638e82f2c9e25b70a01f3eb767bf28446cd8`. +Après le magic et les deux versions `u32`, ses seize champs `u32` versionnés +sont, dans l'ordre : `STRICT_OBSERVATION_LINEAGE`, `NO_SPLIT`, `NO_MERGE`, +`STRICT_DESCENDANT_REGISTERED_OBSERVATIONS`, `PRESERVE_BASE_COMPONENT_KEY`, +`NO_BASE_COMPONENT_MERGE`, `GATE_D_PNP_REGISTRATION`, +`GATE_D_TRIANGULATION_REFINEMENT`, `FULL_AFFECTED_COMPONENT_BA`, +`GATE_E_POLICY`, `EXACT_HISTORICAL_CALIBRATION`, `HISTORICAL_BASE_GAUGE`, +`COMPLETE_IMMUTABLE_SNAPSHOT`, `CANONICAL_ID_ORDER`, +`NO_DISCONNECTED_COMPONENT` et `TERMINAL_BA_FAILURE`. Chaque valeur vaut 1 +en H v1 ; la position identifie la politique sans chaîne dépendante de locale. + +La filiation est dérivée par indexation déterministe des observations, jamais +par égalité de `track_id` entre générations. Chaque Track historique publié a +un unique descendant contenant exactement toutes ses observations historiques. +Un historique manquant, dupliqué, scindé, fusionné ou reliant deux composantes +de base invalide toute la mise à jour. Une observation ajoutée à un descendant +historique dont l'image nouvelle reste non enregistrée dans sa composante +invalide également toute la mise à jour ; une image non enregistrée qui ne +porte que des Tracks nouveaux reste admissible et ne crée pas de composante. + +Chaque caméra nouvelle est localisée dans exactement une composante historique. +Les composantes de base ne fusionnent pas et gardent leur `component_key`, égal +au plus petit `image_id` historique enregistré. Une caméra géométriquement +enregistrée dont l'ID est inférieur à cette clé rend la mise à jour +incompatible ; elle n'est pas maquillée en rejet géométrique. Les landmarks +existants reçoivent leurs observations descendantes admissibles, les Tracks +nouveaux entièrement ancrés peuvent être triangulés, puis chaque composante +affectée subit une BA complète avec les ancres historiques. Une composante non +affectée est copiée sans modification scientifique. + +La sortie est un snapshot complet, autonome et immuable. Le prédécesseur est +une provenance, jamais une dépendance de lecture en chaîne. Publication et +métadonnées H sont atomiques ; toute erreur de filiation, calibration, +compatibilité de clé, géométrie, BA, annulation ou base de données ne publie +rien et ne modifie pas le prédécesseur. Une identité H déjà publiée est +réutilisée avant matérialisation lourde. Une extension sans effet scientifique +retourne explicitement le prédécesseur sans publier une génération factice. +Les métriques globales sont recalculées après la BA sur chaque observation +finalement retenue, avec la projection pixel Gate F. Une profondeur invalide, +un résidu non fini ou un ensemble vide rend la publication impossible. + +L'API scientifique publique valide aussi la cohérence complète du snapshot de +base : landmark, Track, caméra, composante et position canonique de chaque +observation doivent correspondre. Son résultat doit être initialisé à zéro ou +avoir été préalablement détruit avant un nouvel appel. + +L'orchestration appartient à la tâche durable `incremental_reconstruction.run` +v1, atomique (lot 1..1), sans DAG ni état scientifique intermédiaire durable. +Après redémarrage elle recommence depuis les entrées immuables. Son estimation +opérationnelle H v1 est, avec arithmétique `uint64_t` vérifiée et arrondi au MiB +supérieur : + +``` +raw = 268435456 + + base_cameras * 131072 + + base_landmarks * 4096 + + base_observations * 1024 + + extension_images * 131072 + + extension_tracks * 4096 + + extension_observations * 1024 +``` + +Elle est CPU, fixe, `min_batch_size=max_batch_size=1`, demande un thread CPU, +zéro slot GPU et un slot IO. Elle est persistée avec la tâche, exclue de +l'identité et admise uniquement par Queue → Governor → Reservation. Pression +mémoire et ressources ne modifient jamais la science. + +Project DB v18 ajoute seulement les métadonnées d'identité/prédécesseur H et +le payload de tâche H. Le parent générique conserve le vrai fingerprint de +paramètres et porte l'identité H dans son `derivation_identity` nullable ; les +lignes Gate F gardent ce champ à `NULL` et leur unicité historique. La +géométrie reste un snapshot complet conforme au +modèle historique ; aucun sous-ensemble d'observations, remappage de clé ou +graphe de filiation générique n'est introduit. + +**Statut :** PASS / FROZEN — contrat H v1 implémenté et validé. Validation +finale : suite normale 42/42 PASS ; suite ciblée ASan/UBSan/LSan 5/5 PASS ; +revue finale indépendante PASS ; `git diff --check` PASS. Restent différés : +généalogie complexe, split/merge légitime, +fusion de composantes, BA locale, DAG, étapes scientifiques intermédiaires +durables, scratch/SSD, GPU SfM/BA et nouvelles composantes déconnectées. --- @@ -259,12 +358,11 @@ Import, Image Catalog, Feature Extraction, Feature Store, Visual Index, Candidate Pair, Matching v1, Geometric Verification, Track Model/Builder v1 and Sparse SfM Gate C geometry are **IMPLEMENTED**. The synchronous in-memory incremental Sparse SfM Gate D core is **IMPLEMENTED / PASS**, and final -per-component Gate E BA is **PASS / FROZEN**. Gate F orchestration is -**PASS / FROZEN**. Gate G Governor integration, MVS, mesh, -texturing and viewer remain **PLANNED**. +per-component Gate E BA is **PASS / FROZEN**. Gate F orchestration and Gate G +Governor integration are **PASS / FROZEN**. Phase H v1 is **PASS / FROZEN**. +MVS, mesh, texturing and viewer remain **PLANNED**. Ce document décrit la vision architecturale cible du pipeline de reconstruction. Les modules listés ici ne sont pas tous implémentés. -L'implémentation suit la feuille de route définie dans `.opencode/context.md` -et progresse par tickets successifs en respectant les invariants -d'indépendance et de reprise. +L'implémentation suit la feuille de route canonique de `docs/roadmap/roadmap.md` +et progresse par phases respectant les invariants d'indépendance et de reprise. diff --git a/docs/architecture/task_kind_registry.md b/docs/architecture/task_kind_registry.md index 11abac0..e2e0533 100644 --- a/docs/architecture/task_kind_registry.md +++ b/docs/architecture/task_kind_registry.md @@ -84,3 +84,10 @@ version, fingerprint, checksum, tri, unicité ou L3DTSIS1 incohérents. payload scientifique explicite Project DB v17, restaure l'estimation générique persistée et rejoue D puis E depuis les références Track Set/calibration. Le fingerprint F0 est recalculé ; le checkpoint générique v1 reste inchangé. + +**PASS / FROZEN** — `incremental_reconstruction.run`, +version 1, recharge le payload Project DB v18 composé du prédécesseur, du Track +Set d'extension, du scope de calibration et du fingerprint H. La tâche atomique +recalcule depuis ces entrées après redémarrage, passe par la Queue et le +Governor avec son estimation H immuable, et ne persiste aucun état de solveur. +Elle n'ajoute ni DAG ni dépendance implicite. diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index 96d924f..35a8206 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -56,8 +56,8 @@ Lardon3D suit une feuille de route ordonnée qui privilégie la stabilité et la - ✅ Sparse SfM Gate F : orchestration durable et publication atomique PASS / FROZEN - ✅ Sparse SfM Gate G : politique et cœur **PASS / FROZEN** -### Phase 5 : Reconstruction (PLANNED) -- 📋 Orchestration de reconstruction incrémentale +### Phase 5 : Reconstruction +- ✅ Orchestration de reconstruction incrémentale H v1 — **PASS / FROZEN** - 📋 MVS / dense - 📋 Mesh - 📋 Contraintes externes diff --git a/include/lardon3d/app_state.h b/include/lardon3d/app_state.h index 12233a0..1ecc6b1 100644 --- a/include/lardon3d/app_state.h +++ b/include/lardon3d/app_state.h @@ -12,6 +12,8 @@ typedef struct Lardon3DResourceGovernor Lardon3DResourceGovernor; typedef struct Lardon3DProjectDb Lardon3DProjectDb; typedef struct Lardon3DOrbVulkanBackend Lardon3DOrbVulkanBackend; +enum { LARDON3D_APP_STATE_PATH_CAPACITY = 4096 }; + typedef enum { LARDON3D_SCREEN_HOME = 0, LARDON3D_SCREEN_PROJECTS, @@ -22,12 +24,12 @@ typedef enum { LARDON3D_SCREEN_RESOURCES } Lardon3DScreen; -typedef struct { +typedef struct Lardon3DAppState { Lardon3DScreen screen; bool running; bool project_loaded; char project_name[128]; - char project_path[PATH_MAX]; + char project_path[LARDON3D_APP_STATE_PATH_CAPACITY]; char project_stable_id[65]; char status_message[256]; Lardon3DImageCatalog *image_catalog; diff --git a/include/lardon3d/incremental_reconstruction.h b/include/lardon3d/incremental_reconstruction.h new file mode 100644 index 0000000..dfa6457 --- /dev/null +++ b/include/lardon3d/incremental_reconstruction.h @@ -0,0 +1,102 @@ +#ifndef LARDON3D_INCREMENTAL_RECONSTRUCTION_H +#define LARDON3D_INCREMENTAL_RECONSTRUCTION_H + +#include +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + LARDON3D_INCREMENTAL_RECONSTRUCTION_KIND = 1, + LARDON3D_INCREMENTAL_RECONSTRUCTION_VERSION = 1, + LARDON3D_INCREMENTAL_RECONSTRUCTION_SHA256_SIZE = 32, + LARDON3D_INCREMENTAL_RECONSTRUCTION_FINGERPRINT_RECORD_SIZE = 80, + LARDON3D_INCREMENTAL_RECONSTRUCTION_IDENTITY_RECORD_SIZE = 76, +}; + +typedef enum { + LARDON3D_INCREMENTAL_RECONSTRUCTION_OK = 0, + LARDON3D_INCREMENTAL_RECONSTRUCTION_NO_CHANGE, + LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_ARGUMENT, + LARDON3D_INCREMENTAL_RECONSTRUCTION_OUT_OF_MEMORY, + LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_BASE, + LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_MISSING, + LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_SPLIT, + LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_MERGE, + LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_DUPLICATE, + LARDON3D_INCREMENTAL_RECONSTRUCTION_CROSS_COMPONENT_BRIDGE, + LARDON3D_INCREMENTAL_RECONSTRUCTION_DESCENDANT_UNREGISTERED_OBSERVATION, + LARDON3D_INCREMENTAL_RECONSTRUCTION_COMPONENT_KEY_VIOLATION, + LARDON3D_INCREMENTAL_RECONSTRUCTION_GEOMETRY_FAILED, + LARDON3D_INCREMENTAL_RECONSTRUCTION_BUNDLE_ADJUSTMENT_FAILED, + LARDON3D_INCREMENTAL_RECONSTRUCTION_CANCELLED, +} Lardon3DIncrementalReconstructionStatus; + +typedef struct { + uint64_t base_reconstruction_id; + uint64_t extension_track_set_id; + uint64_t calibration_scope_id; + uint32_t incremental_kind; + uint32_t incremental_version; + unsigned char parameter_fingerprint[32]; +} Lardon3DIncrementalReconstructionIdentity; + +typedef struct { + uint64_t base_camera_count; + uint64_t base_landmark_count; + uint64_t base_observation_count; + uint64_t extension_image_count; + uint64_t extension_track_count; + uint64_t extension_observation_count; +} Lardon3DIncrementalReconstructionShape; + +typedef struct { + uint64_t base_reconstruction_id; + const Lardon3DSparseIncrementalResult *base; + const Lardon3DSparseIncrementalObservation *base_track_observations; + size_t base_track_observation_count; + const Lardon3DSparseIncrementalInput *extension; + const Lardon3DSparseIncrementalParameters *parameters; + bool (*checkpoint)(void *context); + void *checkpoint_context; +} Lardon3DIncrementalReconstructionInput; + +typedef struct { + Lardon3DIncrementalReconstructionStatus status; + bool changed; + Lardon3DSparseIncrementalResult snapshot; +} Lardon3DIncrementalReconstructionResult; + +bool lardon3d_incremental_reconstruction_parameter_fingerprint( + unsigned char digest[32]); +bool lardon3d_incremental_reconstruction_parameter_fingerprint_record( + unsigned char record[ + LARDON3D_INCREMENTAL_RECONSTRUCTION_FINGERPRINT_RECORD_SIZE]); +bool lardon3d_incremental_reconstruction_identity_record( + const Lardon3DIncrementalReconstructionIdentity *identity, + unsigned char record[LARDON3D_INCREMENTAL_RECONSTRUCTION_IDENTITY_RECORD_SIZE]); +bool lardon3d_incremental_reconstruction_identity_digest( + const Lardon3DIncrementalReconstructionIdentity *identity, + unsigned char digest[32]); +bool lardon3d_incremental_reconstruction_resource_estimate( + const Lardon3DIncrementalReconstructionShape *shape, + Lardon3DResourceEstimate *estimate); + +/* result must be zero-initialized or previously destroyed before run(). */ +Lardon3DIncrementalReconstructionStatus lardon3d_incremental_reconstruction_run( + const Lardon3DIncrementalReconstructionInput *input, + Lardon3DIncrementalReconstructionResult *result); +void lardon3d_incremental_reconstruction_result_destroy( + Lardon3DIncrementalReconstructionResult *result); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/lardon3d/incremental_reconstruction_task.h b/include/lardon3d/incremental_reconstruction_task.h new file mode 100644 index 0000000..15498ce --- /dev/null +++ b/include/lardon3d/incremental_reconstruction_task.h @@ -0,0 +1,42 @@ +#ifndef LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_H +#define LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_H + +#include +#include + +#include + +typedef struct Lardon3DAppState Lardon3DAppState; + +#ifdef __cplusplus +extern "C" { +#endif + +#define LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_KIND \ + "incremental_reconstruction.run" + +enum { LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_KIND_VERSION = 1 }; + +typedef struct { + uint64_t base_reconstruction_id; + uint64_t extension_track_set_id; + uint64_t calibration_scope_id; +} Lardon3DIncrementalReconstructionTaskConfiguration; + +Lardon3DTask *lardon3d_project_create_incremental_reconstruction_task( + Lardon3DAppState *state, + const Lardon3DIncrementalReconstructionTaskConfiguration *configuration, + uint64_t *task_id); +bool lardon3d_project_enqueue_incremental_reconstruction_task( + Lardon3DAppState *state, + const Lardon3DIncrementalReconstructionTaskConfiguration *configuration, + uint64_t *task_id); +bool lardon3d_incremental_reconstruction_task_reconstruct( + const Lardon3DTaskDurableSnapshot *snapshot, void *context, + Lardon3DTaskKindBinding *binding); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/lardon3d/project_db.h b/include/lardon3d/project_db.h index cfaf50a..0625416 100644 --- a/include/lardon3d/project_db.h +++ b/include/lardon3d/project_db.h @@ -10,7 +10,7 @@ #include enum { - LARDON3D_PROJECT_DB_SCHEMA_VERSION = 17, + LARDON3D_PROJECT_DB_SCHEMA_VERSION = 18, LARDON3D_PROJECT_DB_ID_CAPACITY = 65, LARDON3D_PROJECT_DB_KIND_CAPACITY = 65, LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096, @@ -401,6 +401,16 @@ typedef struct { Lardon3DSparseIncrementalParameters parameters; } Lardon3DProjectDbSparseSfmTask; +typedef struct { + uint64_t task_id; + uint64_t base_reconstruction_id; + uint64_t extension_track_set_id; + uint64_t calibration_scope_id; + uint32_t incremental_kind; + uint32_t incremental_version; + unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE]; +} Lardon3DProjectDbIncrementalReconstructionTask; + void lardon3d_project_db_free_track(Lardon3DProjectDbTrack *track); Lardon3DProjectDbResult lardon3d_project_db_create_track_set( Lardon3DProjectDb *database, const Lardon3DProjectDbTrackSet *configuration, @@ -620,6 +630,15 @@ Lardon3DProjectDbResult lardon3d_project_db_record_sparse_sfm_task( Lardon3DProjectDbResult lardon3d_project_db_load_sparse_sfm_task( Lardon3DProjectDb *database, uint64_t task_id, Lardon3DProjectDbSparseSfmTask *parameters); +Lardon3DProjectDbResult lardon3d_project_db_record_incremental_reconstruction_task( + Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, + const char *kind, uint32_t version, + const Lardon3DProjectDbCheckpoint *checkpoint, + const Lardon3DProjectDbIncrementalReconstructionTask *parameters, + int64_t updated_at); +Lardon3DProjectDbResult lardon3d_project_db_load_incremental_reconstruction_task( + Lardon3DProjectDb *database, uint64_t task_id, + Lardon3DProjectDbIncrementalReconstructionTask *parameters); Lardon3DProjectDbResult lardon3d_project_db_create_match_result( Lardon3DProjectDb *database, uint64_t candidate_pair_id, uint64_t feature_set_id_a, diff --git a/include/lardon3d/sparse_sfm_model.h b/include/lardon3d/sparse_sfm_model.h index 0fcb372..a1eeeb5 100644 --- a/include/lardon3d/sparse_sfm_model.h +++ b/include/lardon3d/sparse_sfm_model.h @@ -60,6 +60,8 @@ typedef struct { uint32_t sfm_kind; uint32_t sfm_version; unsigned char parameter_fingerprint[32]; + bool has_derivation_identity; + unsigned char derivation_identity[32]; uint64_t component_count; uint64_t registered_image_count; uint64_t landmark_count; @@ -120,6 +122,17 @@ typedef struct { int64_t created_at; } Lardon3DSparsePublication; +typedef struct { + uint64_t reconstruction_id; + uint64_t base_reconstruction_id; + uint64_t extension_track_set_id; + uint64_t calibration_scope_id; + uint32_t incremental_kind; + uint32_t incremental_version; + unsigned char parameter_fingerprint[32]; + unsigned char scientific_identity[32]; +} Lardon3DIncrementalReconstructionMetadata; + typedef struct { uint64_t after_id; size_t capacity; @@ -200,6 +213,17 @@ Lardon3DProjectDbResult lardon3d_sparse_calibration_scope_list_members( Lardon3DProjectDbResult lardon3d_sparse_reconstruction_publish( Lardon3DProjectDb *database, const Lardon3DSparsePublication *publication, Lardon3DSparseReconstruction *output); +Lardon3DProjectDbResult lardon3d_incremental_reconstruction_publish( + Lardon3DProjectDb *database, const Lardon3DSparsePublication *publication, + const Lardon3DIncrementalReconstructionMetadata *metadata, + Lardon3DSparseReconstruction *output); +Lardon3DProjectDbResult lardon3d_incremental_reconstruction_find_exact( + Lardon3DProjectDb *database, const unsigned char scientific_identity[32], + Lardon3DIncrementalReconstructionMetadata *metadata, + Lardon3DSparseReconstruction *reconstruction); +Lardon3DProjectDbResult lardon3d_incremental_reconstruction_load_metadata( + Lardon3DProjectDb *database, uint64_t reconstruction_id, + Lardon3DIncrementalReconstructionMetadata *metadata); Lardon3DProjectDbResult lardon3d_sparse_reconstruction_find_exact( Lardon3DProjectDb *database, uint64_t track_set_id, uint64_t calibration_scope_id, uint32_t sfm_kind, uint32_t sfm_version, diff --git a/meson.build b/meson.build index f8f8358..8743722 100644 --- a/meson.build +++ b/meson.build @@ -155,6 +155,9 @@ executable( 'src/sparse_sfm_bundle_adjustment.cpp', 'src/sparse_sfm_gate_f.cpp', 'src/sparse_sfm_task.cpp', + 'src/incremental_reconstruction_identity.cpp', + 'src/incremental_reconstruction.cpp', + 'src/incremental_reconstruction_task.cpp', 'src/task.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c', @@ -174,6 +177,7 @@ executable( c_args: [ '-DLARDON3D_TRACK_BUILDER_TASK_AVAILABLE', '-DLARDON3D_SPARSE_SFM_TASK_AVAILABLE', + '-DLARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_AVAILABLE', ], dependencies: [ncursesw, threads, sqlite3, openssl, opencv, opencv_geometry, ceres] + matcher_backend_dependencies, @@ -945,6 +949,9 @@ sparse_sfm_task_test = executable( 'src/sparse_sfm_incremental.cpp', 'src/sparse_sfm_geometry.cpp', 'src/sparse_sfm_bundle_adjustment.cpp', + 'src/incremental_reconstruction_identity.cpp', + 'src/incremental_reconstruction.cpp', + 'src/incremental_reconstruction_task.cpp', 'src/project_db.c', 'src/project_db_sparse_sfm.c', 'src/feature_store.c', 'src/feature_extractor_opencv.cpp', 'src/app_state.c', @@ -958,6 +965,21 @@ sparse_sfm_task_test = executable( ) test('sparse-sfm-task', sparse_sfm_task_test, timeout: 120) +incremental_reconstruction_test = executable( + 'test-incremental-reconstruction', + sources: [ + 'tests/test_incremental_reconstruction.cpp', + 'src/incremental_reconstruction_identity.cpp', + 'src/incremental_reconstruction.cpp', + 'src/sparse_sfm_geometry.cpp', + 'src/sparse_sfm_incremental.cpp', + 'src/sparse_sfm_bundle_adjustment.cpp', + ], + include_directories: include_directories('include'), + dependencies: [openssl, opencv_geometry, ceres], +) +test('incremental-reconstruction', incremental_reconstruction_test, timeout: 120) + sparse_sfm_payload_test = executable( 'test-sparse-sfm-payload', sources: [ diff --git a/src/incremental_reconstruction.cpp b/src/incremental_reconstruction.cpp new file mode 100644 index 0000000..6ee3431 --- /dev/null +++ b/src/incremental_reconstruction.cpp @@ -0,0 +1,786 @@ +#include "incremental_reconstruction_internal.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +struct ObservationKey { + uint64_t feature_set_id; + uint32_t feature_index; + + bool operator<(const ObservationKey &other) const { + return std::tie(feature_set_id, feature_index) < + std::tie(other.feature_set_id, other.feature_index); + } +}; + +struct HistoricalObservationKey { + uint64_t track_id; + uint64_t image_id; + uint64_t feature_set_id; + uint32_t feature_index; + + bool operator<(const HistoricalObservationKey &other) const { + return std::tie(track_id, image_id, feature_set_id, feature_index) < + std::tie(other.track_id, other.image_id, other.feature_set_id, + other.feature_index); + } +}; + +struct Lineage { + uint64_t base_track_id; + uint64_t extension_track_id; + uint64_t component_key; + uint64_t landmark_id; + std::set historical; +}; + +bool finite_calibration(const Lardon3DSparseGeometryCalibration &value) { + return value.width > 0 && value.height > 0 && std::isfinite(value.fx) && + std::isfinite(value.fy) && std::isfinite(value.cx) && + std::isfinite(value.cy) && std::isfinite(value.k1) && + std::isfinite(value.k2) && std::isfinite(value.p1) && + std::isfinite(value.p2) && value.fx > 0.0 && value.fy > 0.0; +} + +bool finite_pose(const Lardon3DSparseGeometryPose &value) { + for (double item : value.rotation_cw) + if (!std::isfinite(item)) return false; + for (double item : value.translation_cw) + if (!std::isfinite(item)) return false; + return true; +} + +bool finite_point(const Lardon3DSparseGeometryPoint3 &value) { + return std::isfinite(value.x) && std::isfinite(value.y) && + std::isfinite(value.z); +} + +bool checkpoint(const Lardon3DIncrementalReconstructionInput &input) { + return !input.checkpoint || input.checkpoint(input.checkpoint_context); +} + +template +bool copy_array(const std::vector &source, T **output) { + *output = nullptr; + if (source.empty()) return true; + if (source.size() > SIZE_MAX / sizeof(T)) return false; + *output = static_cast(std::malloc(source.size() * sizeof(T))); + if (!*output) return false; + std::copy(source.begin(), source.end(), *output); + return true; +} + +void destroy_snapshot(Lardon3DSparseIncrementalResult *snapshot) { + if (!snapshot) return; + std::free(snapshot->components); + std::free(snapshot->cameras); + std::free(snapshot->landmarks); + std::free(snapshot->observations); + std::free(snapshot->unregistered_images); + *snapshot = {}; +} + +bool valid_base(const Lardon3DSparseIncrementalResult &base) { + if ((base.status != LARDON3D_SPARSE_INCREMENTAL_COMPLETE && + base.status != LARDON3D_SPARSE_INCREMENTAL_PARTIAL) || + base.component_count == 0 || base.camera_count < 2 || + base.landmark_count == 0 || base.observation_count == 0 || + !base.components || !base.cameras || !base.landmarks || !base.observations) + return false; + std::map component_min_image; + std::map component_camera_count; + std::map component_landmark_count; + std::map camera_components; + std::map landmarks_by_track; + std::set landmark_ids; + for (size_t index = 0; index < base.component_count; ++index) { + const auto &component = base.components[index]; + if (component.component_key == 0 || component.registered_image_count == 0 || + component.landmark_count == 0 || + (index && base.components[index - 1].component_key >= component.component_key)) + return false; + component_min_image.emplace(component.component_key, UINT64_MAX); + } + for (size_t index = 0; index < base.camera_count; ++index) { + const auto &camera = base.cameras[index]; + auto minimum = component_min_image.find(camera.component_key); + if (camera.image_id == 0 || minimum == component_min_image.end() || + !finite_pose(camera.pose_cw) || + !camera_components.emplace(camera.image_id, camera.component_key).second) + return false; + minimum->second = std::min(minimum->second, camera.image_id); + ++component_camera_count[camera.component_key]; + } + for (size_t index = 0; index < base.landmark_count; ++index) { + const auto &landmark = base.landmarks[index]; + if (landmark.landmark_id == 0 || landmark.track_id == 0 || + component_min_image.find(landmark.component_key) == component_min_image.end() || + !finite_point(landmark.point) || landmark.observation_count < 2 || + !landmark_ids.insert(landmark.landmark_id).second || + !landmarks_by_track.emplace(landmark.track_id, &landmark).second) + return false; + ++component_landmark_count[landmark.component_key]; + } + for (size_t index = 0; index < base.component_count; ++index) { + const auto &component = base.components[index]; + if (component_min_image[component.component_key] != component.component_key || + component_camera_count[component.component_key] != + component.registered_image_count || + component_landmark_count[component.component_key] != component.landmark_count) + return false; + } + std::map observation_counts; + std::map> positions; + std::set keys; + for (size_t index = 0; index < base.observation_count; ++index) { + const auto &observation = base.observations[index]; + auto landmark = landmarks_by_track.find(observation.track_id); + auto camera = camera_components.find(observation.image_id); + if (landmark == landmarks_by_track.end() || + observation.landmark_id != landmark->second->landmark_id || + camera == camera_components.end() || + camera->second != landmark->second->component_key || + observation.feature_set_id == 0 || + observation.position_in_track >= landmark->second->observation_count || + !positions[observation.track_id] + .insert(observation.position_in_track) + .second || + !keys.insert({observation.feature_set_id, observation.feature_index}).second) + return false; + ++observation_counts[observation.track_id]; + } + for (size_t index = 0; index < base.landmark_count; ++index) + if (observation_counts[base.landmarks[index].track_id] != + base.landmarks[index].observation_count || + positions[base.landmarks[index].track_id].size() != + base.landmarks[index].observation_count) + return false; + return true; +} + +Lardon3DIncrementalReconstructionStatus build_lineage( + const Lardon3DIncrementalReconstructionInput &input, + std::vector *lineages, + std::map *extension_lineage, + std::map> + *extension_tracks, + std::map *extension_keys) { + const auto &base = *input.base; + std::map base_landmarks; + for (size_t index = 0; index < base.landmark_count; ++index) + base_landmarks.emplace(base.landmarks[index].track_id, &base.landmarks[index]); + + std::map> base_tracks; + std::map + snapshot_historical; + std::map + supplied_historical; + for (size_t index = 0; index < base.observation_count; ++index) { + const auto &observation = base.observations[index]; + const HistoricalObservationKey key{observation.track_id, observation.image_id, + observation.feature_set_id, + observation.feature_index}; + if (!snapshot_historical.emplace(key, &observation).second) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_BASE; + } + std::set historical_keys; + for (size_t index = 0; index < input.base_track_observation_count; ++index) { + const auto &observation = input.base_track_observations[index]; + const HistoricalObservationKey key{observation.track_id, observation.image_id, + observation.feature_set_id, + observation.feature_index}; + if (!base_landmarks.count(observation.track_id) || observation.image_id == 0 || + observation.feature_set_id == 0 || observation.feature_count == 0 || + observation.feature_index >= observation.feature_count || + !std::isfinite(observation.x) || !std::isfinite(observation.y) || + !supplied_historical.emplace(key, &observation).second || + !historical_keys.insert({observation.feature_set_id, + observation.feature_index}).second) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_BASE; + base_tracks[observation.track_id].push_back(&observation); + } + if (base_tracks.size() != base.landmark_count || + supplied_historical.size() != snapshot_historical.size()) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_BASE; + for (const auto &[key, observation] : snapshot_historical) { + (void)observation; + if (!supplied_historical.count(key)) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_BASE; + } + + for (size_t index = 0; index < input.extension->observation_count; ++index) { + const auto &observation = input.extension->observations[index]; + if (observation.track_id == 0 || observation.image_id == 0 || + observation.feature_set_id == 0 || observation.feature_count == 0 || + observation.feature_index >= observation.feature_count || + !std::isfinite(observation.x) || !std::isfinite(observation.y) || + !extension_keys->emplace(ObservationKey{observation.feature_set_id, + observation.feature_index}, + &observation).second) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_DUPLICATE; + (*extension_tracks)[observation.track_id].push_back(&observation); + } + + lineages->reserve(base.landmark_count); + for (const auto &[base_track_id, historical] : base_tracks) { + uint64_t descendant = 0; + std::set base_keys; + for (const auto *observation : historical) { + ObservationKey key{observation->feature_set_id, observation->feature_index}; + base_keys.insert(key); + auto found = extension_keys->find(key); + if (found == extension_keys->end()) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_MISSING; + if (descendant == 0) + descendant = found->second->track_id; + else if (descendant != found->second->track_id) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_SPLIT; + } + if (descendant == 0) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_MISSING; + const auto *landmark = base_landmarks.at(base_track_id); + Lineage lineage{base_track_id, descendant, landmark->component_key, + landmark->landmark_id, std::move(base_keys)}; + auto inserted = extension_lineage->emplace(descendant, lineages->size()); + if (!inserted.second) { + const auto &other = (*lineages)[inserted.first->second]; + return other.component_key == lineage.component_key + ? LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_MERGE + : LARDON3D_INCREMENTAL_RECONSTRUCTION_CROSS_COMPONENT_BRIDGE; + } + lineages->push_back(std::move(lineage)); + } + return LARDON3D_INCREMENTAL_RECONSTRUCTION_OK; +} + +struct Candidate { + uint64_t image_id; + uint64_t component_key; + size_t support; + std::vector points; + std::vector pixels; +}; + +bool register_cameras( + const Lardon3DIncrementalReconstructionInput &input, + const std::vector &lineages, + const std::map &extension_lineage, + std::vector *cameras, + std::set *affected, + Lardon3DIncrementalReconstructionStatus *failure) { + std::map landmarks; + for (size_t index = 0; index < input.base->landmark_count; ++index) { + const auto &landmark = input.base->landmarks[index]; + landmarks.emplace(landmark.track_id, &landmark); + } + std::set registered; + std::map component_keys; + for (const auto &camera : *cameras) { + registered.insert(camera.image_id); + component_keys.emplace(camera.component_key, camera.component_key); + } + std::set remaining; + std::map images_by_id; + std::map> + observations_by_image; + for (size_t index = 0; index < input.extension->observation_count; ++index) { + const auto &observation = input.extension->observations[index]; + observations_by_image[observation.image_id].push_back(&observation); + } + for (size_t index = 0; index < input.extension->image_count; ++index) { + const auto &image = input.extension->images[index]; + if (image.image_id == 0 || !finite_calibration(image.calibration) || + !images_by_id.emplace(image.image_id, &image).second) + return false; + if (!registered.count(image.image_id)) remaining.insert(image.image_id); + } + + for (uint32_t round = 0; + round < input.parameters->maximum_registration_rounds && !remaining.empty(); + ++round) { + std::vector candidates; + for (uint64_t image_id : remaining) { + std::map by_component; + const auto image_observations = observations_by_image.find(image_id); + if (image_observations == observations_by_image.end()) continue; + for (const auto *observation_pointer : image_observations->second) { + const auto &observation = *observation_pointer; + auto lineage_index = extension_lineage.find(observation.track_id); + if (lineage_index == extension_lineage.end()) continue; + const auto &lineage = lineages[lineage_index->second]; + const auto *landmark = landmarks.at(lineage.base_track_id); + auto &candidate = by_component[lineage.component_key]; + candidate.image_id = image_id; + candidate.component_key = lineage.component_key; + candidate.points.push_back(landmark->point); + candidate.pixels.push_back({observation.x, observation.y}); + } + size_t eligible_components = 0; + for (auto &[component, candidate] : by_component) { + candidate.support = candidate.points.size(); + if (candidate.support >= input.parameters->minimum_pnp_correspondences) { + ++eligible_components; + candidates.push_back(std::move(candidate)); + } + } + if (eligible_components > 1) { + *failure = LARDON3D_INCREMENTAL_RECONSTRUCTION_CROSS_COMPONENT_BRIDGE; + return false; + } + } + std::sort(candidates.begin(), candidates.end(), [](const auto &left, const auto &right) { + return left.support != right.support ? left.support > right.support + : left.image_id < right.image_id; + }); + bool grew = false; + for (const auto &candidate : candidates) { + if (!remaining.count(candidate.image_id)) continue; + auto image_entry = images_by_id.find(candidate.image_id); + const auto *image = image_entry == images_by_id.end() + ? nullptr + : image_entry->second; + std::vector mask(candidate.support); + Lardon3DSparseGeometryPnPResult pnp{}; + pnp.inlier_mask = mask.data(); + pnp.inlier_mask_capacity = mask.size(); + if (!image || lardon3d_sparse_geometry_pnp( + &image->calibration, candidate.points.data(), + candidate.pixels.data(), candidate.support, + &input.parameters->pnp, &pnp) != LARDON3D_SPARSE_GEOMETRY_OK || + !finite_pose(pnp.pose_cw)) + continue; + if (candidate.image_id < candidate.component_key) { + *failure = LARDON3D_INCREMENTAL_RECONSTRUCTION_COMPONENT_KEY_VIOLATION; + return false; + } + cameras->push_back({candidate.image_id, candidate.component_key, pnp.pose_cw}); + remaining.erase(candidate.image_id); + affected->insert(candidate.component_key); + grew = true; + } + if (!grew) break; + } + std::sort(cameras->begin(), cameras->end(), + [](const auto &left, const auto &right) { + return left.image_id < right.image_id; + }); + return true; +} + +bool project_ok(const Lardon3DSparseGeometryCalibration &calibration, + const Lardon3DSparseGeometryPose &pose, + const Lardon3DSparseGeometryPoint3 &point, + const Lardon3DSparseGeometryPoint2 &observed, double threshold) { + const double *r = pose.rotation_cw; + const double *t = pose.translation_cw; + const double x = r[0] * point.x + r[1] * point.y + r[2] * point.z + t[0]; + const double y = r[3] * point.x + r[4] * point.y + r[5] * point.z + t[1]; + const double z = r[6] * point.x + r[7] * point.y + r[8] * point.z + t[2]; + if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z) || z <= 1e-9) + return false; + const double xn = x / z; + const double yn = y / z; + const double r2 = xn * xn + yn * yn; + const double radial = 1.0 + calibration.k1 * r2 + calibration.k2 * r2 * r2; + const double px = calibration.fx * + (xn * radial + 2.0 * calibration.p1 * xn * yn + + calibration.p2 * (r2 + 2.0 * xn * xn)) + + calibration.cx; + const double py = calibration.fy * + (yn * radial + calibration.p1 * (r2 + 2.0 * yn * yn) + + 2.0 * calibration.p2 * xn * yn) + + calibration.cy; + return std::isfinite(px) && std::isfinite(py) && + std::hypot(px - observed.x, py - observed.y) <= threshold; +} + +bool select_base_anchor(const Lardon3DSparseIncrementalResult &base, + uint64_t component_key, + Lardon3DSparseBundleAdjustmentAnchor *anchor) { + std::vector cameras; + for (size_t index = 0; index < base.camera_count; ++index) + if (base.cameras[index].component_key == component_key) + cameras.push_back(&base.cameras[index]); + if (cameras.size() < 2) return false; + std::sort(cameras.begin(), cameras.end(), [](const auto *left, const auto *right) { + return left->image_id < right->image_id; + }); + const auto *pose = cameras.front(); + auto center = [](const auto &camera, double output[3]) { + const double *r = camera.pose_cw.rotation_cw; + const double *t = camera.pose_cw.translation_cw; + for (size_t column = 0; column < 3; ++column) + output[column] = -(r[column] * t[0] + r[3 + column] * t[1] + + r[6 + column] * t[2]); + }; + double pose_center[3]; + center(*pose, pose_center); + const Lardon3DSparseIncrementalCamera *scale = nullptr; + double best = -1.0; + double best_delta[3]{}; + for (size_t index = 1; index < cameras.size(); ++index) { + double candidate_center[3]; + center(*cameras[index], candidate_center); + double delta[3] = {candidate_center[0] - pose_center[0], + candidate_center[1] - pose_center[1], + candidate_center[2] - pose_center[2]}; + const double distance = std::hypot(delta[0], delta[1], delta[2]); + if (!scale || distance > best || + (distance == best && cameras[index]->image_id < scale->image_id)) { + scale = cameras[index]; + best = distance; + std::copy(delta, delta + 3, best_delta); + } + } + size_t axis = 0; + if (std::abs(best_delta[1]) > std::abs(best_delta[axis])) axis = 1; + if (std::abs(best_delta[2]) > std::abs(best_delta[axis])) axis = 2; + if (!scale || std::abs(best_delta[axis]) <= 1e-9) return false; + *anchor = {component_key, pose->image_id, scale->image_id, + static_cast(axis + 1)}; + return true; +} + +bool full_ba_affected( + const Lardon3DIncrementalReconstructionInput &input, + const std::set &affected, + std::vector *components, + std::vector *cameras, + std::vector *landmarks, + std::vector *observations) { + std::map component_indexes; + std::map camera_indexes; + std::map landmark_indexes; + std::map> cameras_by_component; + std::map> landmarks_by_component; + std::map> observations_by_component; + std::map track_components; + for (size_t index = 0; index < components->size(); ++index) + component_indexes.emplace((*components)[index].component_key, index); + for (size_t index = 0; index < cameras->size(); ++index) { + camera_indexes.emplace((*cameras)[index].image_id, index); + cameras_by_component[(*cameras)[index].component_key].push_back(index); + } + for (size_t index = 0; index < landmarks->size(); ++index) { + landmark_indexes.emplace((*landmarks)[index].track_id, index); + landmarks_by_component[(*landmarks)[index].component_key].push_back(index); + track_components.emplace((*landmarks)[index].track_id, + (*landmarks)[index].component_key); + } + for (size_t index = 0; index < observations->size(); ++index) { + auto component = track_components.find((*observations)[index].track_id); + if (component == track_components.end()) return false; + observations_by_component[component->second].push_back(index); + } + for (uint64_t key : affected) { + std::vector sub_components; + std::vector sub_cameras; + std::vector sub_landmarks; + std::vector sub_observations; + auto component = component_indexes.find(key); + if (component == component_indexes.end()) return false; + sub_components.push_back((*components)[component->second]); + for (size_t index : cameras_by_component[key]) + sub_cameras.push_back((*cameras)[index]); + for (size_t index : landmarks_by_component[key]) + sub_landmarks.push_back((*landmarks)[index]); + for (size_t index : observations_by_component[key]) + sub_observations.push_back((*observations)[index]); + Lardon3DSparseIncrementalResult sub{}; + sub.status = LARDON3D_SPARSE_INCREMENTAL_COMPLETE; + sub.components = sub_components.data(); + sub.component_count = sub_components.size(); + sub.cameras = sub_cameras.data(); + sub.camera_count = sub_cameras.size(); + sub.landmarks = sub_landmarks.data(); + sub.landmark_count = sub_landmarks.size(); + sub.observations = sub_observations.data(); + sub.observation_count = sub_observations.size(); + Lardon3DSparseBundleAdjustmentInput ba_input{ + &sub, input.extension->images, input.extension->image_count, + input.extension->observations, input.extension->observation_count}; + Lardon3DSparseBundleAdjustmentAnchor anchor{}; + if (!select_base_anchor(*input.base, key, &anchor)) return false; + Lardon3DSparseBundleAdjustmentResult adjusted{}; + auto status = lardon3d_sparse_bundle_adjustment_run_with_anchors( + &ba_input, &anchor, 1, &adjusted); + if (status != LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_EXECUTION_OK || + adjusted.status != LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_COMPLETE || + adjusted.component_count != 1 || !adjusted.diagnostics[0].accepted) { + lardon3d_sparse_bundle_adjustment_result_destroy(&adjusted); + return false; + } + for (size_t index = 0; index < adjusted.camera_count; ++index) { + auto target = camera_indexes.find(adjusted.cameras[index].image_id); + if (target == camera_indexes.end()) { + lardon3d_sparse_bundle_adjustment_result_destroy(&adjusted); + return false; + } + (*cameras)[target->second] = adjusted.cameras[index]; + } + for (size_t index = 0; index < adjusted.landmark_count; ++index) { + auto target = landmark_indexes.find(adjusted.landmarks[index].track_id); + if (target == landmark_indexes.end()) { + lardon3d_sparse_bundle_adjustment_result_destroy(&adjusted); + return false; + } + (*landmarks)[target->second] = adjusted.landmarks[index]; + } + lardon3d_sparse_bundle_adjustment_result_destroy(&adjusted); + } + return true; +} + +Lardon3DIncrementalReconstructionStatus execute( + const Lardon3DIncrementalReconstructionInput &input, + Lardon3DIncrementalReconstructionResult *result) { + if (!input.base || !input.extension || !input.parameters || + input.base_reconstruction_id == 0 || + (input.base_track_observation_count && !input.base_track_observations) || + (input.extension->image_count && !input.extension->images) || + (input.extension->observation_count && !input.extension->observations)) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_ARGUMENT; + if (!valid_base(*input.base)) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_BASE; + if (!checkpoint(input)) return LARDON3D_INCREMENTAL_RECONSTRUCTION_CANCELLED; + + std::vector lineages; + std::map extension_lineage; + std::map> + extension_tracks; + std::map extension_keys; + auto lineage_status = build_lineage(input, &lineages, &extension_lineage, + &extension_tracks, &extension_keys); + if (lineage_status != LARDON3D_INCREMENTAL_RECONSTRUCTION_OK) return lineage_status; + + std::vector components( + input.base->components, input.base->components + input.base->component_count); + std::vector cameras( + input.base->cameras, input.base->cameras + input.base->camera_count); + std::vector landmarks( + input.base->landmarks, input.base->landmarks + input.base->landmark_count); + std::set affected; + Lardon3DIncrementalReconstructionStatus registration_failure = + LARDON3D_INCREMENTAL_RECONSTRUCTION_GEOMETRY_FAILED; + if (!register_cameras(input, lineages, extension_lineage, &cameras, &affected, + ®istration_failure)) + return registration_failure; + if (!checkpoint(input)) return LARDON3D_INCREMENTAL_RECONSTRUCTION_CANCELLED; + + std::map registered_component; + for (const auto &camera : cameras) + registered_component.emplace(camera.image_id, camera.component_key); + std::vector observations; + observations.reserve(input.extension->observation_count); + std::map landmark_by_track; + std::map lineage_by_base_track; + for (size_t index = 0; index < lineages.size(); ++index) + lineage_by_base_track.emplace(lineages[index].base_track_id, index); + for (size_t index = 0; index < landmarks.size(); ++index) { + auto lineage = lineage_by_base_track.find(landmarks[index].track_id); + if (lineage == lineage_by_base_track.end()) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_BASE; + const auto &lineage_record = lineages[lineage->second]; + landmarks[index].track_id = lineage_record.extension_track_id; + landmarks[index].observation_count = 0; + landmark_by_track.emplace(lineage_record.extension_track_id, index); + } + + for (const auto &lineage : lineages) { + uint32_t position = 0; + bool added = false; + auto descendant = extension_tracks.find(lineage.extension_track_id); + if (descendant == extension_tracks.end()) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_MISSING; + for (const auto *source_pointer : descendant->second) { + const auto &source = *source_pointer; + const bool historical = lineage.historical.count( + {source.feature_set_id, source.feature_index}) != 0; + auto registered = registered_component.find(source.image_id); + if (registered == registered_component.end() || + registered->second != lineage.component_key) { + if (!historical) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_DESCENDANT_UNREGISTERED_OBSERVATION; + return LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_BASE; + } + const size_t landmark_index = landmark_by_track.at(lineage.extension_track_id); + observations.push_back({landmarks[landmark_index].landmark_id, + lineage.extension_track_id, source.image_id, + source.feature_set_id, source.feature_index, position++}); + ++landmarks[landmark_index].observation_count; + if (!historical) added = true; + } + if (added) affected.insert(lineage.component_key); + } + + uint64_t next_landmark_id = 1; + for (const auto &landmark : landmarks) { + if (landmark.landmark_id >= next_landmark_id) { + if (landmark.landmark_id >= UINT64_MAX - 1) + next_landmark_id = UINT64_MAX; + else + next_landmark_id = landmark.landmark_id + 1; + } + } + std::map images_by_id; + for (size_t index = 0; index < input.extension->image_count; ++index) + images_by_id.emplace(input.extension->images[index].image_id, + &input.extension->images[index]); + for (const auto &[track_id, track] : extension_tracks) { + if (extension_lineage.count(track_id)) continue; + if (track.size() < 2) continue; + uint64_t component_key = 0; + bool admissible = true; + std::vector normalized; + std::vector poses; + normalized.reserve(track.size()); + poses.reserve(track.size()); + for (const auto *source : track) { + auto registered = registered_component.find(source->image_id); + auto image_entry = images_by_id.find(source->image_id); + const auto *image = image_entry == images_by_id.end() ? nullptr + : image_entry->second; + auto camera = std::lower_bound(cameras.begin(), cameras.end(), source->image_id, + [](const auto &item, uint64_t id) { + return item.image_id < id; + }); + if (registered == registered_component.end() || !image || camera == cameras.end() || + camera->image_id != source->image_id || + (component_key != 0 && component_key != registered->second)) { + admissible = false; + break; + } + component_key = registered->second; + Lardon3DSparseGeometryPoint2 pixel{source->x, source->y}; + Lardon3DSparseGeometryPoint2 point{}; + if (lardon3d_sparse_geometry_normalize(&image->calibration, &pixel, 1, &point) != + LARDON3D_SPARSE_GEOMETRY_OK) { + admissible = false; + break; + } + normalized.push_back(point); + poses.push_back(camera->pose_cw); + } + if (!admissible) continue; + Lardon3DSparseGeometryPoint3 point{}; + if (lardon3d_sparse_geometry_triangulate_multi_view( + normalized.data(), poses.data(), poses.size(), &point) != + LARDON3D_SPARSE_GEOMETRY_OK) + continue; + Lardon3DSparseGeometryPoint3 refined{}; + if (lardon3d_sparse_geometry_refine_point( + normalized.data(), poses.data(), poses.size(), &point, + &input.parameters->refinement, &refined) == LARDON3D_SPARSE_GEOMETRY_OK) + point = refined; + bool accepted = finite_point(point); + for (size_t index = 0; accepted && index < track.size(); ++index) { + auto image_entry = images_by_id.find(track[index]->image_id); + const auto *image = image_entry == images_by_id.end() ? nullptr + : image_entry->second; + accepted = image && project_ok(image->calibration, poses[index], point, + {track[index]->x, track[index]->y}, + input.parameters->reprojection_threshold_px); + } + if (!accepted) continue; + if (next_landmark_id == 0 || next_landmark_id == UINT64_MAX) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_BASE; + const uint64_t landmark_id = next_landmark_id++; + landmarks.push_back({landmark_id, track_id, component_key, point, 0.0, 0.0, + static_cast(track.size())}); + uint32_t position = 0; + for (const auto *source : track) + observations.push_back({landmark_id, track_id, source->image_id, + source->feature_set_id, source->feature_index, position++}); + affected.insert(component_key); + } + + if (affected.empty()) { + result->status = LARDON3D_INCREMENTAL_RECONSTRUCTION_NO_CHANGE; + return result->status; + } + std::sort(landmarks.begin(), landmarks.end(), [](const auto &left, const auto &right) { + return left.track_id < right.track_id; + }); + std::sort(observations.begin(), observations.end(), [](const auto &left, const auto &right) { + return std::tie(left.landmark_id, left.position_in_track) < + std::tie(right.landmark_id, right.position_in_track); + }); + std::map component_camera_counts; + std::map component_landmark_counts; + for (const auto &camera : cameras) + ++component_camera_counts[camera.component_key]; + for (const auto &landmark : landmarks) + ++component_landmark_counts[landmark.component_key]; + for (auto &component : components) { + component.registered_image_count = component_camera_counts[component.component_key]; + component.image_count = component.registered_image_count; + component.landmark_count = component_landmark_counts[component.component_key]; + } + if (!checkpoint(input)) return LARDON3D_INCREMENTAL_RECONSTRUCTION_CANCELLED; + if (!full_ba_affected(input, affected, &components, &cameras, &landmarks, + &observations)) + return LARDON3D_INCREMENTAL_RECONSTRUCTION_BUNDLE_ADJUSTMENT_FAILED; + if (!checkpoint(input)) return LARDON3D_INCREMENTAL_RECONSTRUCTION_CANCELLED; + + Lardon3DSparseIncrementalResult snapshot{}; + snapshot.status = LARDON3D_SPARSE_INCREMENTAL_COMPLETE; + snapshot.track_set_id = input.extension->track_set_id; + snapshot.calibration_scope_id = input.extension->calibration_scope_id; + snapshot.component_count = components.size(); + snapshot.camera_count = cameras.size(); + snapshot.landmark_count = landmarks.size(); + snapshot.observation_count = observations.size(); + if (!copy_array(components, &snapshot.components) || + !copy_array(cameras, &snapshot.cameras) || + !copy_array(landmarks, &snapshot.landmarks) || + !copy_array(observations, &snapshot.observations)) { + destroy_snapshot(&snapshot); + return LARDON3D_INCREMENTAL_RECONSTRUCTION_OUT_OF_MEMORY; + } + result->snapshot = snapshot; + result->changed = true; + result->status = LARDON3D_INCREMENTAL_RECONSTRUCTION_OK; + return result->status; +} +} // namespace + +extern "C" Lardon3DIncrementalReconstructionStatus +lardon3d_incremental_reconstruction_run( + const Lardon3DIncrementalReconstructionInput *input, + Lardon3DIncrementalReconstructionResult *result) { + if (!result) return LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_ARGUMENT; + lardon3d_incremental_reconstruction_result_destroy(result); + if (!input) { + result->status = LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_ARGUMENT; + return result->status; + } + try { + return execute(*input, result); + } catch (const std::bad_alloc &) { + result->status = LARDON3D_INCREMENTAL_RECONSTRUCTION_OUT_OF_MEMORY; + return result->status; + } catch (...) { + result->status = LARDON3D_INCREMENTAL_RECONSTRUCTION_GEOMETRY_FAILED; + return result->status; + } +} + +extern "C" void lardon3d_incremental_reconstruction_result_destroy( + Lardon3DIncrementalReconstructionResult *result) { + if (!result) return; + destroy_snapshot(&result->snapshot); + *result = {}; +} diff --git a/src/incremental_reconstruction_identity.cpp b/src/incremental_reconstruction_identity.cpp new file mode 100644 index 0000000..7b99b75 --- /dev/null +++ b/src/incremental_reconstruction_identity.cpp @@ -0,0 +1,122 @@ +#include + +#include +#include +#include + +namespace { +constexpr size_t fingerprint_size = + LARDON3D_INCREMENTAL_RECONSTRUCTION_FINGERPRINT_RECORD_SIZE; +constexpr size_t identity_size = + LARDON3D_INCREMENTAL_RECONSTRUCTION_IDENTITY_RECORD_SIZE; + +bool put_u32(unsigned char *record, size_t capacity, size_t *offset, + uint32_t value) { + if (!record || !offset || *offset > capacity - 4) return false; + for (unsigned int byte = 0; byte < 4; ++byte) + record[(*offset)++] = static_cast(value >> (byte * 8U)); + return true; +} + +bool put_u64(unsigned char *record, size_t capacity, size_t *offset, + uint64_t value) { + if (!record || !offset || *offset > capacity - 8) return false; + for (unsigned int byte = 0; byte < 8; ++byte) + record[(*offset)++] = static_cast(value >> (byte * 8U)); + return true; +} + +bool digest_record(const unsigned char *record, size_t size, + unsigned char digest[32]) { + unsigned int length = 0; + return record && digest && + EVP_Digest(record, size, digest, &length, EVP_sha256(), nullptr) == 1 && + length == 32; +} + +bool checked_add(uint64_t left, uint64_t right, uint64_t *output) { + if (!output || right > std::numeric_limits::max() - left) return false; + *output = left + right; + return true; +} + +bool checked_term(uint64_t count, uint64_t coefficient, uint64_t *raw) { + if (count != 0 && coefficient > std::numeric_limits::max() / count) + return false; + return checked_add(*raw, count * coefficient, raw); +} +} // namespace + +extern "C" bool lardon3d_incremental_reconstruction_parameter_fingerprint_record( + unsigned char record[fingerprint_size]) { + if (!record) return false; + std::memset(record, 0, fingerprint_size); + std::memcpy(record, "L3DHPRM1", 8); + size_t offset = 8; + if (!put_u32(record, fingerprint_size, &offset, 1) || + !put_u32(record, fingerprint_size, &offset, + LARDON3D_INCREMENTAL_RECONSTRUCTION_VERSION)) + return false; + /* Field position is the policy identifier; every v1 policy is version 1. */ + for (unsigned int policy = 0; policy < 16; ++policy) + if (!put_u32(record, fingerprint_size, &offset, 1)) return false; + return offset == fingerprint_size; +} + +extern "C" bool lardon3d_incremental_reconstruction_parameter_fingerprint( + unsigned char digest[32]) { + unsigned char record[fingerprint_size]; + return digest && + lardon3d_incremental_reconstruction_parameter_fingerprint_record(record) && + digest_record(record, sizeof(record), digest); +} + +extern "C" bool lardon3d_incremental_reconstruction_identity_record( + const Lardon3DIncrementalReconstructionIdentity *identity, + unsigned char record[identity_size]) { + if (!identity || !record || identity->base_reconstruction_id == 0 || + identity->extension_track_set_id == 0 || identity->calibration_scope_id == 0 || + identity->incremental_kind != LARDON3D_INCREMENTAL_RECONSTRUCTION_KIND || + identity->incremental_version != LARDON3D_INCREMENTAL_RECONSTRUCTION_VERSION) + return false; + std::memset(record, 0, identity_size); + std::memcpy(record, "L3DHIDV1", 8); + size_t offset = 8; + if (!put_u32(record, identity_size, &offset, 1) || + !put_u64(record, identity_size, &offset, identity->base_reconstruction_id) || + !put_u64(record, identity_size, &offset, identity->extension_track_set_id) || + !put_u64(record, identity_size, &offset, identity->calibration_scope_id) || + !put_u32(record, identity_size, &offset, identity->incremental_kind) || + !put_u32(record, identity_size, &offset, identity->incremental_version)) + return false; + std::memcpy(record + offset, identity->parameter_fingerprint, 32); + offset += 32; + return offset == identity_size; +} + +extern "C" bool lardon3d_incremental_reconstruction_identity_digest( + const Lardon3DIncrementalReconstructionIdentity *identity, + unsigned char digest[32]) { + unsigned char record[identity_size]; + return digest && lardon3d_incremental_reconstruction_identity_record(identity, record) && + digest_record(record, sizeof(record), digest); +} + +extern "C" bool lardon3d_incremental_reconstruction_resource_estimate( + const Lardon3DIncrementalReconstructionShape *shape, + Lardon3DResourceEstimate *estimate) { + if (!shape || !estimate) return false; + uint64_t raw = 268435456ULL; + if (!checked_term(shape->base_camera_count, 131072ULL, &raw) || + !checked_term(shape->base_landmark_count, 4096ULL, &raw) || + !checked_term(shape->base_observation_count, 1024ULL, &raw) || + !checked_term(shape->extension_image_count, 131072ULL, &raw) || + !checked_term(shape->extension_track_count, 4096ULL, &raw) || + !checked_term(shape->extension_observation_count, 1024ULL, &raw)) + return false; + constexpr uint64_t mib = 1024ULL * 1024ULL; + const uint64_t remainder = raw % mib; + if (remainder != 0 && !checked_add(raw, mib - remainder, &raw)) return false; + *estimate = {raw, 0, 0, 0, 1, 1, 1, 0, 1, LARDON3D_RESOURCE_TASK_CPU}; + return true; +} diff --git a/src/incremental_reconstruction_internal.h b/src/incremental_reconstruction_internal.h new file mode 100644 index 0000000..556037b --- /dev/null +++ b/src/incremental_reconstruction_internal.h @@ -0,0 +1,22 @@ +#ifndef LARDON3D_INCREMENTAL_RECONSTRUCTION_INTERNAL_H +#define LARDON3D_INCREMENTAL_RECONSTRUCTION_INTERNAL_H + +#include +#include + +#include + +struct Lardon3DSparseBundleAdjustmentAnchor { + uint64_t component_key; + uint64_t pose_anchor_image_id; + uint64_t scale_anchor_image_id; + Lardon3DSparseBundleAdjustmentScaleAxis scale_axis; +}; + +Lardon3DSparseBundleAdjustmentExecutionStatus +lardon3d_sparse_bundle_adjustment_run_with_anchors( + const Lardon3DSparseBundleAdjustmentInput *input, + const Lardon3DSparseBundleAdjustmentAnchor *anchors, size_t anchor_count, + Lardon3DSparseBundleAdjustmentResult *result); + +#endif diff --git a/src/incremental_reconstruction_task.cpp b/src/incremental_reconstruction_task.cpp new file mode 100644 index 0000000..d743d9b --- /dev/null +++ b/src/incremental_reconstruction_task.cpp @@ -0,0 +1,764 @@ +extern "C" { +#include +#include +} + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sparse_sfm_gate_f_internal.h" + +extern "C" { +#include +#include +#include +#include +#include +#include +} + +namespace { +struct Context { + std::string project_path; + Lardon3DProjectDb *database = nullptr; + Lardon3DProjectDbIncrementalReconstructionTask durable{}; + Lardon3DIncrementalReconstructionShape shape{}; +}; + +struct Reader { + Lardon3DProjectDbFeatureSet feature_set{}; + Lardon3DFeatureReader *reader = nullptr; + Lardon3DFeatureFileMetadata metadata{}; +}; + +struct Resolved { + Lardon3DSparseReconstruction base_record{}; + Lardon3DSparseIncrementalResult base{}; + std::vector base_components; + std::vector base_cameras; + std::vector base_landmarks; + std::vector base_observations; + std::vector base_track_observations; + std::vector extension_images; + std::vector extension_observations; + std::map calibrations; +}; + +void destroy_context(void *userdata) { delete static_cast(userdata); } + +bool valid_durable(const Lardon3DProjectDbIncrementalReconstructionTask &durable) { + unsigned char expected[32]; + return durable.task_id != 0 && durable.base_reconstruction_id != 0 && + durable.extension_track_set_id != 0 && durable.calibration_scope_id != 0 && + durable.incremental_kind == LARDON3D_INCREMENTAL_RECONSTRUCTION_KIND && + durable.incremental_version == LARDON3D_INCREMENTAL_RECONSTRUCTION_VERSION && + lardon3d_incremental_reconstruction_parameter_fingerprint(expected) && + std::memcmp(expected, durable.parameter_fingerprint, 32) == 0; +} + +bool checked_add(uint64_t left, uint64_t right, uint64_t *output) { + if (!output || right > UINT64_MAX - left) return false; + *output = left + right; + return true; +} + +bool load_scope(Lardon3DProjectDb *database, uint64_t scope_id, + std::map *members, + std::map *calibrations) { + Lardon3DSparseCalibrationScope scope{}; + if (!database || !members || + lardon3d_sparse_calibration_scope_load(database, scope_id, &scope) != + LARDON3D_PROJECT_DB_OK) + return false; + uint64_t after = 0; + for (;;) { + Lardon3DSparseCalibrationMember page[LARDON3D_SPARSE_SFM_PAGE_MAX]{}; + size_t count = 0; + uint64_t next = 0; + if (lardon3d_sparse_calibration_scope_list_members( + database, scope_id, after, page, LARDON3D_SPARSE_SFM_PAGE_MAX, + &count, &next) != LARDON3D_PROJECT_DB_OK) + return false; + for (size_t index = 0; index < count; ++index) { + if (!members->emplace(page[index].image_id, page[index]).second) return false; + if (calibrations) { + Lardon3DSparseCalibration calibration{}; + if (lardon3d_sparse_calibration_load(database, page[index].calibration_id, + &calibration) != + LARDON3D_PROJECT_DB_OK || + std::memcmp(calibration.scientific_hash, + page[index].calibration_hash, 32) != 0) + return false; + calibrations->emplace( + page[index].image_id, + Lardon3DSparseGeometryCalibration{ + calibration.width, calibration.height, calibration.fx, + calibration.fy, calibration.cx, calibration.cy, calibration.k1, + calibration.k2, calibration.p1, calibration.p2}); + } + } + if (count == 0) break; + if (next <= after) return false; + after = next; + } + return members->size() == scope.member_count; +} + +bool derive_shape(Context *context) { + if (!context) return false; + Lardon3DSparseReconstruction base{}; + if (lardon3d_sparse_reconstruction_load( + context->database, context->durable.base_reconstruction_id, &base) != + LARDON3D_PROJECT_DB_OK) + return false; + Lardon3DProjectDbTrackSet track_set{}; + if (lardon3d_project_db_load_track_set( + context->database, context->durable.extension_track_set_id, + &track_set) != LARDON3D_PROJECT_DB_OK) + return false; + std::map members; + if (!load_scope(context->database, context->durable.calibration_scope_id, + &members, nullptr)) + return false; + uint64_t observations = 0; + uint64_t tracks_seen = 0; + uint64_t after = 0; + for (;;) { + Lardon3DProjectDbTrack tracks[LARDON3D_SPARSE_SFM_PAGE_MAX]{}; + size_t count = 0; + if (lardon3d_project_db_list_tracks( + context->database, track_set.track_set_id, after, tracks, + LARDON3D_SPARSE_SFM_PAGE_MAX, &count) != LARDON3D_PROJECT_DB_OK) + return false; + bool valid = true; + for (size_t index = 0; index < count; ++index) { + if (tracks[index].track_id <= after || + !checked_add(observations, tracks[index].observation_count, + &observations)) + valid = false; + after = tracks[index].track_id; + ++tracks_seen; + lardon3d_project_db_free_track(&tracks[index]); + } + if (!valid) return false; + if (count == 0) break; + } + if (tracks_seen != track_set.track_count || observations == 0) return false; + context->shape = {base.registered_image_count, base.landmark_count, + 0, members.size(), tracks_seen, observations}; + uint64_t landmark_after = 0; + for (;;) { + Lardon3DSparseLandmark page[LARDON3D_SPARSE_SFM_PAGE_MAX]{}; + Lardon3DSparseLandmarkPage list{landmark_after, + LARDON3D_SPARSE_SFM_PAGE_MAX, 0, 0, page}; + if (lardon3d_sparse_landmark_list( + context->database, base.reconstruction_id, landmark_after, + LARDON3D_SPARSE_SFM_PAGE_MAX, &list) != LARDON3D_PROJECT_DB_OK) + return false; + for (size_t index = 0; index < list.count; ++index) + if (!checked_add(context->shape.base_observation_count, + page[index].observation_count, + &context->shape.base_observation_count)) + return false; + if (list.count == 0) break; + if (list.next_track_id <= landmark_after) return false; + landmark_after = list.next_track_id; + } + return true; +} + +void close_readers(std::map *readers) { + for (auto &[id, reader] : *readers) { + (void)id; + if (reader.reader) lardon3d_feature_reader_close(reader.reader); + } + readers->clear(); +} + +bool resolve_observation(Context *context, + const Lardon3DProjectDbTrackObservation &source, + std::map *readers, uint64_t track_id, + Lardon3DSparseIncrementalObservation *output) { + auto found = readers->find(source.feature_set_id); + if (found == readers->end()) { + Reader reader; + if (lardon3d_project_db_load_feature_set( + context->database, source.feature_set_id, &reader.feature_set) != + LARDON3D_PROJECT_DB_OK || + lardon3d_feature_reader_open(context->project_path.c_str(), + &reader.feature_set, &reader.reader, + &reader.metadata) != LARDON3D_FEATURE_STORE_OK || + reader.metadata.feature_count != reader.feature_set.feature_count) { + if (reader.reader) lardon3d_feature_reader_close(reader.reader); + return false; + } + found = readers->emplace(source.feature_set_id, reader).first; + } + Lardon3DFeatureKeypoint keypoint{}; + if (source.feature_index >= found->second.metadata.feature_count || + lardon3d_feature_reader_keypoints(found->second.reader, source.feature_index, + &keypoint, 1) != LARDON3D_FEATURE_STORE_OK || + !std::isfinite(keypoint.x) || !std::isfinite(keypoint.y)) + return false; + *output = {track_id, found->second.feature_set.image_id, source.feature_set_id, + source.feature_index, found->second.metadata.feature_count, + static_cast(keypoint.x), static_cast(keypoint.y)}; + return true; +} + +bool load_base_snapshot(Context *context, Resolved *resolved, + std::map *readers) { + if (lardon3d_sparse_reconstruction_load( + context->database, context->durable.base_reconstruction_id, + &resolved->base_record) != LARDON3D_PROJECT_DB_OK) + return false; + uint64_t after = 0; + for (;;) { + Lardon3DSparseComponent page[LARDON3D_SPARSE_SFM_PAGE_MAX]{}; + Lardon3DSparseComponentPage list{after, LARDON3D_SPARSE_SFM_PAGE_MAX, + 0, 0, page}; + if (lardon3d_sparse_component_list( + context->database, resolved->base_record.reconstruction_id, after, + LARDON3D_SPARSE_SFM_PAGE_MAX, &list) != LARDON3D_PROJECT_DB_OK) + return false; + for (size_t index = 0; index < list.count; ++index) + resolved->base_components.push_back( + {page[index].component_key, page[index].registered_image_count, + page[index].registered_image_count, page[index].landmark_count}); + if (list.count == 0) break; + if (list.next_component_key <= after) return false; + after = list.next_component_key; + } + after = 0; + for (;;) { + Lardon3DSparseRegisteredImage page[LARDON3D_SPARSE_SFM_PAGE_MAX]{}; + Lardon3DSparseRegisteredImagePage list{after, LARDON3D_SPARSE_SFM_PAGE_MAX, + 0, 0, page}; + if (lardon3d_sparse_registered_image_list( + context->database, resolved->base_record.reconstruction_id, after, + LARDON3D_SPARSE_SFM_PAGE_MAX, &list) != LARDON3D_PROJECT_DB_OK) + return false; + for (size_t index = 0; index < list.count; ++index) { + Lardon3DSparseGeometryPose pose{}; + std::memcpy(pose.rotation_cw, page[index].rotation_cw, + sizeof(pose.rotation_cw)); + std::memcpy(pose.translation_cw, page[index].translation_cw, + sizeof(pose.translation_cw)); + resolved->base_cameras.push_back( + {page[index].image_id, page[index].component_key, pose}); + } + if (list.count == 0) break; + if (list.next_image_id <= after) return false; + after = list.next_image_id; + } + after = 0; + for (;;) { + Lardon3DSparseLandmark page[LARDON3D_SPARSE_SFM_PAGE_MAX]{}; + Lardon3DSparseLandmarkPage list{after, LARDON3D_SPARSE_SFM_PAGE_MAX, + 0, 0, page}; + if (lardon3d_sparse_landmark_list( + context->database, resolved->base_record.reconstruction_id, after, + LARDON3D_SPARSE_SFM_PAGE_MAX, &list) != LARDON3D_PROJECT_DB_OK) + return false; + for (size_t index = 0; index < list.count; ++index) { + resolved->base_landmarks.push_back( + {page[index].landmark_id, page[index].track_id, + page[index].component_key, {page[index].x, page[index].y, page[index].z}, + page[index].reprojection_rmse_px, + page[index].reprojection_median_px, page[index].observation_count}); + Lardon3DProjectDbTrack track{}; + if (lardon3d_project_db_load_track(context->database, page[index].track_id, + &track) != LARDON3D_PROJECT_DB_OK) + return false; + bool valid = true; + for (uint32_t position = 0; position < track.observation_count; ++position) { + Lardon3DSparseIncrementalObservation observation{}; + if (!resolve_observation(context, track.observations[position], readers, + track.track_id, &observation)) { + valid = false; + break; + } + resolved->base_track_observations.push_back(observation); + } + lardon3d_project_db_free_track(&track); + if (!valid) return false; + } + if (list.count == 0) break; + if (list.next_track_id <= after) return false; + after = list.next_track_id; + } + uint64_t after_landmark = 0; + uint32_t after_position = 0; + for (;;) { + Lardon3DSparseLandmarkObservation page[LARDON3D_SPARSE_SFM_PAGE_MAX]{}; + Lardon3DSparseObservationPage list{after_landmark, after_position, + LARDON3D_SPARSE_SFM_PAGE_MAX, 0, 0, 0, + page}; + if (lardon3d_sparse_observation_list( + context->database, resolved->base_record.reconstruction_id, + after_landmark, after_position, LARDON3D_SPARSE_SFM_PAGE_MAX, + &list) != LARDON3D_PROJECT_DB_OK) + return false; + for (size_t index = 0; index < list.count; ++index) { + auto reader = readers->find(page[index].feature_set_id); + if (reader == readers->end()) { + Reader loaded; + if (lardon3d_project_db_load_feature_set( + context->database, page[index].feature_set_id, + &loaded.feature_set) != LARDON3D_PROJECT_DB_OK) + return false; + reader = readers->emplace(page[index].feature_set_id, loaded).first; + } + resolved->base_observations.push_back( + {page[index].landmark_id, page[index].track_id, + reader->second.feature_set.image_id, + page[index].feature_set_id, page[index].feature_index, + page[index].position_in_track}); + } + if (list.count == 0) break; + if (list.next_landmark_id < after_landmark || + (list.next_landmark_id == after_landmark && + list.next_position_in_track <= after_position)) + return false; + after_landmark = list.next_landmark_id; + after_position = list.next_position_in_track; + } + resolved->base.status = LARDON3D_SPARSE_INCREMENTAL_COMPLETE; + resolved->base.track_set_id = resolved->base_record.track_set_id; + resolved->base.calibration_scope_id = resolved->base_record.calibration_scope_id; + resolved->base.components = resolved->base_components.data(); + resolved->base.component_count = resolved->base_components.size(); + resolved->base.cameras = resolved->base_cameras.data(); + resolved->base.camera_count = resolved->base_cameras.size(); + resolved->base.landmarks = resolved->base_landmarks.data(); + resolved->base.landmark_count = resolved->base_landmarks.size(); + resolved->base.observations = resolved->base_observations.data(); + resolved->base.observation_count = resolved->base_observations.size(); + return resolved->base.component_count == resolved->base_record.component_count && + resolved->base.camera_count == resolved->base_record.registered_image_count && + resolved->base.landmark_count == resolved->base_record.landmark_count; +} + +bool resolve_all(Context *context, Resolved *resolved) { + std::map readers; + bool valid = load_base_snapshot(context, resolved, &readers); + std::map base_scope; + std::map extension_scope; + if (valid) + valid = load_scope(context->database, resolved->base_record.calibration_scope_id, + &base_scope, nullptr) && + load_scope(context->database, context->durable.calibration_scope_id, + &extension_scope, &resolved->calibrations); + for (const auto &camera : resolved->base_cameras) { + auto historical = base_scope.find(camera.image_id); + auto extension = extension_scope.find(camera.image_id); + if (!valid || historical == base_scope.end() || extension == extension_scope.end() || + std::memcmp(historical->second.calibration_hash, + extension->second.calibration_hash, 32) != 0) { + valid = false; + break; + } + } + Lardon3DProjectDbTrackSet track_set{}; + if (valid) + valid = lardon3d_project_db_load_track_set( + context->database, context->durable.extension_track_set_id, + &track_set) == LARDON3D_PROJECT_DB_OK; + uint64_t after = 0; + uint64_t tracks_seen = 0; + std::set participating_images; + while (valid) { + Lardon3DProjectDbTrack tracks[LARDON3D_SPARSE_SFM_PAGE_MAX]{}; + size_t count = 0; + if (lardon3d_project_db_list_tracks( + context->database, track_set.track_set_id, after, tracks, + LARDON3D_SPARSE_SFM_PAGE_MAX, &count) != LARDON3D_PROJECT_DB_OK) { + valid = false; + break; + } + for (size_t index = 0; valid && index < count; ++index) { + if (tracks[index].track_id <= after) { + valid = false; + break; + } + for (uint32_t position = 0; + valid && position < tracks[index].observation_count; ++position) { + Lardon3DSparseIncrementalObservation observation{}; + valid = resolve_observation(context, tracks[index].observations[position], + &readers, tracks[index].track_id, &observation) && + resolved->calibrations.count(observation.image_id) != 0; + if (valid) { + participating_images.insert(observation.image_id); + resolved->extension_observations.push_back(observation); + } + } + after = tracks[index].track_id; + ++tracks_seen; + } + for (size_t index = 0; index < count; ++index) + lardon3d_project_db_free_track(&tracks[index]); + if (!valid || count == 0) break; + } + for (const auto &camera : resolved->base_cameras) + participating_images.insert(camera.image_id); + for (uint64_t image_id : participating_images) { + auto calibration = resolved->calibrations.find(image_id); + if (calibration == resolved->calibrations.end()) { + valid = false; + break; + } + resolved->extension_images.push_back({image_id, calibration->second}); + } + close_readers(&readers); + return valid && tracks_seen == track_set.track_count && + !resolved->extension_observations.empty(); +} + +bool persist(Context *context, const Lardon3DTask *task) { + Lardon3DTaskDurableSnapshot snapshot{}; + if (!lardon3d_task_durable_snapshot(task, &snapshot)) return false; + Lardon3DProjectDbCheckpoint checkpoint_record{}; + std::snprintf(checkpoint_record.path, sizeof(checkpoint_record.path), + ".lardon3d/checkpoints/%llu.chk", + static_cast(snapshot.id)); + checkpoint_record.format_version = LARDON3D_TASK_CHECKPOINT_VERSION; + std::string path = context->project_path + "/" + checkpoint_record.path; + auto saved = lardon3d_task_checkpoint_save(path.c_str(), &snapshot); + if (saved != LARDON3D_TASK_CHECKPOINT_OK && + saved != LARDON3D_TASK_CHECKPOINT_PUBLISHED_NOT_DURABLE) + return false; + checkpoint_record.durability = + saved == LARDON3D_TASK_CHECKPOINT_OK + ? LARDON3D_DB_CHECKPOINT_DURABLE + : LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE; + return lardon3d_project_db_record_incremental_reconstruction_task( + context->database, &snapshot, + LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_KIND, + LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_KIND_VERSION, + &checkpoint_record, &context->durable, 0) == LARDON3D_PROJECT_DB_OK; +} + +bool task_checkpoint(void *userdata) { + return lardon3d_task_checkpoint(static_cast(userdata)); +} + +bool build_publication(const Resolved &resolved, + const Lardon3DSparseIncrementalResult &snapshot, + const unsigned char parameter_fingerprint[32], + std::vector *components, + std::vector *cameras, + std::vector *landmarks, + std::vector *observations, + Lardon3DSparsePublication *publication) { + std::map cameras_by_image; + std::map landmarks_by_track; + std::map, + const Lardon3DSparseIncrementalObservation *> + source_observations; + std::vector squared_errors; + for (size_t index = 0; index < snapshot.camera_count; ++index) + cameras_by_image.emplace(snapshot.cameras[index].image_id, + &snapshot.cameras[index]); + for (size_t index = 0; index < snapshot.landmark_count; ++index) + landmarks_by_track.emplace(snapshot.landmarks[index].track_id, + &snapshot.landmarks[index]); + for (const auto &source : resolved.extension_observations) + if (!source_observations + .emplace(std::make_pair(source.feature_set_id, + source.feature_index), + &source) + .second) + return false; + for (size_t index = 0; index < snapshot.component_count; ++index) + components->push_back({0, snapshot.components[index].component_key, + snapshot.components[index].registered_image_count, + snapshot.components[index].landmark_count}); + for (size_t index = 0; index < snapshot.camera_count; ++index) { + Lardon3DSparseRegisteredImage camera{}; + camera.image_id = snapshot.cameras[index].image_id; + camera.component_key = snapshot.cameras[index].component_key; + std::memcpy(camera.rotation_cw, snapshot.cameras[index].pose_cw.rotation_cw, + sizeof(camera.rotation_cw)); + std::memcpy(camera.translation_cw, + snapshot.cameras[index].pose_cw.translation_cw, + sizeof(camera.translation_cw)); + cameras->push_back(camera); + } + for (size_t index = 0; index < snapshot.landmark_count; ++index) { + const auto &source = snapshot.landmarks[index]; + landmarks->push_back({0, source.track_id, source.component_key, source.point.x, + source.point.y, source.point.z, + source.reprojection_rmse_px, + source.reprojection_median_px, + source.observation_count}); + } + for (size_t index = 0; index < snapshot.observation_count; ++index) { + const auto &source = snapshot.observations[index]; + auto camera = cameras_by_image.find(source.image_id); + auto landmark = landmarks_by_track.find(source.track_id); + auto calibration = resolved.calibrations.find(source.image_id); + auto observed = source_observations.find( + std::make_pair(source.feature_set_id, source.feature_index)); + if (camera == cameras_by_image.end() || + landmark == landmarks_by_track.end() || + calibration == resolved.calibrations.end() || + observed == source_observations.end() || + observed->second->track_id != source.track_id || + observed->second->image_id != source.image_id) + return false; + Lardon3DSparseGeometryPoint2 pixel{observed->second->x, + observed->second->y}; + double squared_error = 0.0; + if (!lardon3d_sparse_sfm_squared_reprojection_error( + &calibration->second, &camera->second->pose_cw, + &landmark->second->point, &pixel, &squared_error)) + return false; + squared_errors.push_back(squared_error); + observations->push_back({0, source.track_id, source.feature_set_id, + source.feature_index, source.position_in_track}); + } + *publication = {resolved.base.track_set_id, 0, 0, 0, {}, nullptr, 0, + nullptr, 0, nullptr, 0, nullptr, 0, 0.0, 0.0, 0}; + publication->track_set_id = snapshot.track_set_id; + publication->calibration_scope_id = snapshot.calibration_scope_id; + publication->sfm_kind = LARDON3D_SPARSE_SFM_KIND_INCREMENTAL; + publication->sfm_version = LARDON3D_SPARSE_SFM_VERSION; + std::memcpy(publication->parameter_fingerprint, parameter_fingerprint, 32); + publication->components = components->data(); + publication->component_count = components->size(); + publication->registered_images = cameras->data(); + publication->registered_image_count = cameras->size(); + publication->landmarks = landmarks->data(); + publication->landmark_count = landmarks->size(); + publication->observations = observations->data(); + publication->observation_count = observations->size(); + if (!lardon3d_sparse_sfm_publication_metrics( + squared_errors.data(), squared_errors.size(), + &publication->reprojection_rmse_px, + &publication->reprojection_median_px)) + return false; + publication->created_at = static_cast(std::time(nullptr)); + return true; +} + +bool execute(Context *context, Lardon3DTask *task, bool *reused, bool *no_change) { + Lardon3DIncrementalReconstructionIdentity identity{ + context->durable.base_reconstruction_id, + context->durable.extension_track_set_id, + context->durable.calibration_scope_id, + context->durable.incremental_kind, + context->durable.incremental_version, + {}}; + std::memcpy(identity.parameter_fingerprint, + context->durable.parameter_fingerprint, 32); + unsigned char digest[32]; + if (!lardon3d_incremental_reconstruction_identity_digest(&identity, digest)) + return false; + Lardon3DIncrementalReconstructionMetadata existing_metadata{}; + Lardon3DSparseReconstruction existing{}; + auto lookup = lardon3d_incremental_reconstruction_find_exact( + context->database, digest, &existing_metadata, &existing); + if (lookup == LARDON3D_PROJECT_DB_OK) { + *reused = true; + return true; + } + if (lookup != LARDON3D_PROJECT_DB_NOT_FOUND || !lardon3d_task_checkpoint(task)) + return false; + Resolved resolved; + if (!resolve_all(context, &resolved)) return false; + Lardon3DSparseIncrementalParameters parameters{}; + if (!lardon3d_sparse_incremental_parameters_default(¶meters)) return false; + Lardon3DSparseIncrementalInput extension{ + context->durable.extension_track_set_id, + context->durable.calibration_scope_id, + resolved.extension_images.data(), resolved.extension_images.size(), + resolved.extension_observations.data(), resolved.extension_observations.size()}; + Lardon3DIncrementalReconstructionInput input{ + context->durable.base_reconstruction_id, &resolved.base, + resolved.base_track_observations.data(), + resolved.base_track_observations.size(), &extension, ¶meters, + task_checkpoint, task}; + Lardon3DIncrementalReconstructionResult result{}; + auto status = lardon3d_incremental_reconstruction_run(&input, &result); + if (status == LARDON3D_INCREMENTAL_RECONSTRUCTION_NO_CHANGE) { + *no_change = true; + return true; + } + if (status != LARDON3D_INCREMENTAL_RECONSTRUCTION_OK) { + lardon3d_incremental_reconstruction_result_destroy(&result); + return false; + } + std::vector components; + std::vector cameras; + std::vector landmarks; + std::vector observations; + Lardon3DSparsePublication publication{}; + if (!build_publication(resolved, result.snapshot, + context->durable.parameter_fingerprint, + &components, &cameras, + &landmarks, &observations, &publication)) { + lardon3d_incremental_reconstruction_result_destroy(&result); + return false; + } + Lardon3DIncrementalReconstructionMetadata metadata{ + 0, context->durable.base_reconstruction_id, + context->durable.extension_track_set_id, + context->durable.calibration_scope_id, + context->durable.incremental_kind, + context->durable.incremental_version, + {}, {}}; + std::memcpy(metadata.parameter_fingerprint, + context->durable.parameter_fingerprint, 32); + std::memcpy(metadata.scientific_identity, digest, 32); + Lardon3DSparseReconstruction published{}; + auto published_status = lardon3d_incremental_reconstruction_publish( + context->database, &publication, &metadata, &published); + lardon3d_incremental_reconstruction_result_destroy(&result); + if (published_status == LARDON3D_PROJECT_DB_OK) return true; + if (published_status != LARDON3D_PROJECT_DB_CONSTRAINT) return false; + lookup = lardon3d_incremental_reconstruction_find_exact( + context->database, digest, &existing_metadata, &existing); + *reused = lookup == LARDON3D_PROJECT_DB_OK; + return *reused; +} + +bool run(Lardon3DTask *task, void *userdata) { + try { + bool reused = false; + bool no_change = false; + if (!execute(static_cast(userdata), task, &reused, &no_change)) + return lardon3d_task_fail(task, + "Reconstruction incrémentale non publiable."); + const char *message = reused ? "Reconstruction incrémentale réutilisée." + : no_change ? "Extension sans changement scientifique." + : "Reconstruction incrémentale publiée."; + return lardon3d_task_set_progress(task, 100, message); + } catch (const std::bad_alloc &) { + return lardon3d_task_fail(task, + "Mémoire insuffisante pour la phase H."); + } catch (...) { + return lardon3d_task_fail(task, "Erreur interne de la phase H."); + } +} + +void finished(const Lardon3DTask *task, void *userdata) { + try { + (void)persist(static_cast(userdata), task); + } catch (...) { + } +} +} // namespace + +extern "C" bool lardon3d_incremental_reconstruction_task_reconstruct( + const Lardon3DTaskDurableSnapshot *snapshot, void *userdata, + Lardon3DTaskKindBinding *binding) { + try { + if (!snapshot || !userdata || !binding) return false; + auto *runtime = static_cast(userdata); + Lardon3DProjectDbIncrementalReconstructionTask durable{}; + if (lardon3d_project_db_load_incremental_reconstruction_task( + runtime->project_db, snapshot->id, &durable) != LARDON3D_PROJECT_DB_OK) + return false; + if (!valid_durable(durable)) return false; + auto *context = new (std::nothrow) Context; + if (!context) return false; + context->project_path = runtime->project_path; + context->database = runtime->project_db; + context->durable = durable; + if (!derive_shape(context)) { + delete context; + return false; + } + *binding = {run, context, destroy_context, finished, context}; + return true; + } catch (...) { + return false; + } +} + +extern "C" Lardon3DTask *lardon3d_project_create_incremental_reconstruction_task( + Lardon3DAppState *state, + const Lardon3DIncrementalReconstructionTaskConfiguration *configuration, + uint64_t *task_id) { + try { + if (task_id) *task_id = 0; + if (!state || !state->project_loaded || !state->project_db || + !state->resource_governor || !configuration || + configuration->base_reconstruction_id == 0 || + configuration->extension_track_set_id == 0 || + configuration->calibration_scope_id == 0) + return nullptr; + auto *context = new (std::nothrow) Context; + if (!context) return nullptr; + context->project_path = state->project_path; + context->database = state->project_db; + context->durable.base_reconstruction_id = + configuration->base_reconstruction_id; + context->durable.extension_track_set_id = + configuration->extension_track_set_id; + context->durable.calibration_scope_id = configuration->calibration_scope_id; + context->durable.incremental_kind = + LARDON3D_INCREMENTAL_RECONSTRUCTION_KIND; + context->durable.incremental_version = + LARDON3D_INCREMENTAL_RECONSTRUCTION_VERSION; + if (!lardon3d_incremental_reconstruction_parameter_fingerprint( + context->durable.parameter_fingerprint) || !derive_shape(context)) { + delete context; + return nullptr; + } + Lardon3DResourceEstimate estimate{}; + if (!lardon3d_incremental_reconstruction_resource_estimate(&context->shape, + &estimate)) { + delete context; + return nullptr; + } + uint64_t id = 0; + if (lardon3d_project_db_allocate_task_id(state->project_db, &id) != + LARDON3D_PROJECT_DB_OK) { + delete context; + return nullptr; + } + context->durable.task_id = id; + Lardon3DTask *task = lardon3d_task_create_typed( + "Reconstruction incrémentale", &estimate, + LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_KIND, + LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_KIND_VERSION, run, context, + destroy_context); + if (!task || !lardon3d_task_assign_id(task, id) || + !lardon3d_task_set_finished_callback(task, finished, context) || + !persist(context, task)) { + if (task) + lardon3d_task_destroy(task); + else + delete context; + return nullptr; + } + if (task_id) *task_id = id; + return task; + } catch (...) { + if (task_id) *task_id = 0; + return nullptr; + } +} + +extern "C" bool lardon3d_project_enqueue_incremental_reconstruction_task( + Lardon3DAppState *state, + const Lardon3DIncrementalReconstructionTaskConfiguration *configuration, + uint64_t *task_id) { + Lardon3DTask *task = lardon3d_project_create_incremental_reconstruction_task( + state, configuration, task_id); + if (!task || !lardon3d_task_queue_add(state->task_queue, task, nullptr)) { + if (task) lardon3d_task_destroy(task); + return false; + } + return true; +} diff --git a/src/project_db.c b/src/project_db.c index 5f7b5e4..b9c762d 100644 --- a/src/project_db.c +++ b/src/project_db.c @@ -447,6 +447,136 @@ static const char schema_sparse_sfm_task_v17[] = "CHECK(length(pnp_deterministic_seed)=8),refinement_max_iterations INTEGER NOT NULL," "refinement_convergence_tolerance REAL NOT NULL);"; +static const char schema_sparse_derivation_identity_v18[] = + "PRAGMA legacy_alter_table=ON;" + "DROP INDEX sparse_landmark_observations_page_idx;" + "DROP INDEX sparse_landmark_track_lookup_idx;" + "DROP INDEX sparse_landmarks_page_idx;" + "DROP INDEX sparse_registered_image_lookup_idx;" + "DROP INDEX sparse_registered_page_idx;" + "DROP INDEX sparse_components_page_idx;" + "DROP INDEX sparse_reconstructions_scope_idx;" + "DROP INDEX IF EXISTS sparse_reconstructions_gate_f_candidate_idx;" + "DROP INDEX IF EXISTS sparse_reconstructions_derivation_idx;" + "ALTER TABLE sparse_landmark_observations RENAME TO sparse_landmark_observations_v17;" + "ALTER TABLE sparse_landmarks RENAME TO sparse_landmarks_v17;" + "ALTER TABLE sparse_registered_images RENAME TO sparse_registered_images_v17;" + "ALTER TABLE sparse_reconstruction_components RENAME TO sparse_reconstruction_components_v17;" + "ALTER TABLE sparse_reconstructions RENAME TO sparse_reconstructions_v17;" + "CREATE TABLE sparse_reconstructions(" + "reconstruction_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(reconstruction_id>0)," + "track_set_id INTEGER NOT NULL REFERENCES track_sets(track_set_id)," + "calibration_scope_id INTEGER NOT NULL REFERENCES sparse_calibration_scopes(scope_id)," + "sfm_kind INTEGER NOT NULL CHECK(sfm_kind=1),sfm_version INTEGER NOT NULL " + "CHECK(sfm_version=1),parameter_fingerprint BLOB NOT NULL " + "CHECK(length(parameter_fingerprint)=32),derivation_identity BLOB " + "CHECK(derivation_identity IS NULL OR length(derivation_identity)=32)," + "component_count INTEGER NOT NULL CHECK(component_count>0)," + "registered_image_count INTEGER NOT NULL CHECK(registered_image_count>=2)," + "landmark_count INTEGER NOT NULL CHECK(landmark_count>0)," + "reprojection_rmse_px REAL NOT NULL,reprojection_median_px REAL NOT NULL," + "created_at INTEGER NOT NULL);" + "INSERT INTO sparse_reconstructions(" + "reconstruction_id,track_set_id,calibration_scope_id,sfm_kind,sfm_version," + "parameter_fingerprint,derivation_identity,component_count," + "registered_image_count,landmark_count,reprojection_rmse_px," + "reprojection_median_px,created_at) SELECT reconstruction_id,track_set_id," + "calibration_scope_id,sfm_kind,sfm_version,parameter_fingerprint,NULL," + "component_count,registered_image_count,landmark_count,reprojection_rmse_px," + "reprojection_median_px,created_at FROM sparse_reconstructions_v17;"; + +static const char schema_sparse_derivation_children_v18[] = + "CREATE TABLE sparse_reconstruction_components(" + "component_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(component_id>0)," + "reconstruction_id INTEGER NOT NULL REFERENCES sparse_reconstructions(" + "reconstruction_id) ON DELETE CASCADE,component_key INTEGER NOT NULL " + "CHECK(component_key>0),registered_image_count INTEGER NOT NULL " + "CHECK(registered_image_count>0),landmark_count INTEGER NOT NULL " + "CHECK(landmark_count>0),UNIQUE(reconstruction_id,component_id)," + "UNIQUE(reconstruction_id,component_key));" + "INSERT INTO sparse_reconstruction_components SELECT * FROM " + "sparse_reconstruction_components_v17;" + "CREATE TABLE sparse_registered_images(" + "reconstruction_id INTEGER NOT NULL REFERENCES sparse_reconstructions(" + "reconstruction_id) ON DELETE CASCADE,component_id INTEGER NOT NULL," + "image_id INTEGER NOT NULL REFERENCES images(image_id)," + "rotation_00 REAL NOT NULL,rotation_01 REAL NOT NULL,rotation_02 REAL NOT NULL," + "rotation_10 REAL NOT NULL,rotation_11 REAL NOT NULL,rotation_12 REAL NOT NULL," + "rotation_20 REAL NOT NULL,rotation_21 REAL NOT NULL,rotation_22 REAL NOT NULL," + "translation_0 REAL NOT NULL,translation_1 REAL NOT NULL,translation_2 REAL NOT NULL," + "PRIMARY KEY(reconstruction_id,image_id),FOREIGN KEY(reconstruction_id,component_id) " + "REFERENCES sparse_reconstruction_components(reconstruction_id,component_id));" + "INSERT INTO sparse_registered_images SELECT * FROM sparse_registered_images_v17;" + "CREATE TABLE sparse_landmarks(" + "landmark_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(landmark_id>0)," + "reconstruction_id INTEGER NOT NULL REFERENCES sparse_reconstructions(" + "reconstruction_id) ON DELETE CASCADE,component_id INTEGER NOT NULL," + "track_id INTEGER NOT NULL REFERENCES tracks(track_id),x REAL NOT NULL,y REAL NOT NULL," + "z REAL NOT NULL,reprojection_rmse_px REAL NOT NULL,reprojection_median_px REAL NOT NULL," + "observation_count INTEGER NOT NULL CHECK(observation_count>0)," + "UNIQUE(reconstruction_id,track_id),FOREIGN KEY(reconstruction_id,component_id) " + "REFERENCES sparse_reconstruction_components(reconstruction_id,component_id));" + "INSERT INTO sparse_landmarks SELECT * FROM sparse_landmarks_v17;" + "CREATE TABLE sparse_landmark_observations(" + "landmark_id INTEGER NOT NULL REFERENCES sparse_landmarks(landmark_id) ON DELETE CASCADE," + "feature_set_id INTEGER NOT NULL REFERENCES feature_sets(feature_set_id)," + "feature_index INTEGER NOT NULL CHECK(feature_index>=0)," + "position_in_track INTEGER NOT NULL CHECK(position_in_track>=0)," + "PRIMARY KEY(landmark_id,feature_set_id,feature_index));" + "INSERT INTO sparse_landmark_observations SELECT * FROM " + "sparse_landmark_observations_v17;" + "DROP TABLE sparse_landmark_observations_v17;" + "DROP TABLE sparse_landmarks_v17;" + "DROP TABLE sparse_registered_images_v17;" + "DROP TABLE sparse_reconstruction_components_v17;" + "DROP TABLE sparse_reconstructions_v17;" + "CREATE UNIQUE INDEX sparse_reconstructions_gate_f_candidate_idx ON " + "sparse_reconstructions(track_set_id,calibration_scope_id,sfm_kind,sfm_version," + "parameter_fingerprint) WHERE derivation_identity IS NULL;" + "CREATE UNIQUE INDEX sparse_reconstructions_derivation_idx ON " + "sparse_reconstructions(derivation_identity) WHERE derivation_identity IS NOT NULL;" + "CREATE INDEX sparse_reconstructions_scope_idx ON " + "sparse_reconstructions(calibration_scope_id);" + "CREATE INDEX sparse_components_page_idx ON sparse_reconstruction_components(" + "reconstruction_id,component_key);" + "CREATE INDEX sparse_registered_page_idx ON sparse_registered_images(" + "reconstruction_id,component_id,image_id);" + "CREATE INDEX sparse_registered_image_lookup_idx ON sparse_registered_images(" + "reconstruction_id,image_id);" + "CREATE INDEX sparse_landmarks_page_idx ON sparse_landmarks(" + "reconstruction_id,component_id,track_id);" + "CREATE INDEX sparse_landmark_track_lookup_idx ON sparse_landmarks(" + "reconstruction_id,track_id);" + "CREATE INDEX sparse_landmark_observations_page_idx ON " + "sparse_landmark_observations(landmark_id,position_in_track);" + "PRAGMA legacy_alter_table=OFF;"; + +static const char schema_incremental_reconstruction_v18[] = + "CREATE TABLE incremental_reconstructions(" + "reconstruction_id INTEGER PRIMARY KEY REFERENCES sparse_reconstructions(" + "reconstruction_id) ON DELETE CASCADE," + "base_reconstruction_id INTEGER NOT NULL REFERENCES sparse_reconstructions(" + "reconstruction_id)," + "extension_track_set_id INTEGER NOT NULL REFERENCES track_sets(track_set_id)," + "calibration_scope_id INTEGER NOT NULL REFERENCES sparse_calibration_scopes(scope_id)," + "incremental_kind INTEGER NOT NULL CHECK(incremental_kind=1)," + "incremental_version INTEGER NOT NULL CHECK(incremental_version=1)," + "parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32)," + "scientific_identity BLOB NOT NULL CHECK(length(scientific_identity)=32) UNIQUE," + "UNIQUE(base_reconstruction_id,extension_track_set_id,calibration_scope_id," + "incremental_kind,incremental_version,parameter_fingerprint));" + "CREATE INDEX incremental_reconstructions_base_idx ON incremental_reconstructions(" + "base_reconstruction_id,reconstruction_id);" + "CREATE TABLE incremental_reconstruction_tasks(" + "task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE," + "base_reconstruction_id INTEGER NOT NULL REFERENCES sparse_reconstructions(" + "reconstruction_id)," + "extension_track_set_id INTEGER NOT NULL REFERENCES track_sets(track_set_id)," + "calibration_scope_id INTEGER NOT NULL REFERENCES sparse_calibration_scopes(scope_id)," + "incremental_kind INTEGER NOT NULL CHECK(incremental_kind=1)," + "incremental_version INTEGER NOT NULL CHECK(incremental_version=1)," + "parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32));"; + static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) { if (destination) { (void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : ""); @@ -537,8 +667,8 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int if (from_version != 0 && from_version != 1 && from_version != 2 && from_version != 3 && from_version != 4 && from_version != 5 && from_version != 6 && from_version != 7 && from_version != 8 && from_version != 9 && from_version != 10 && from_version != 11 && - from_version != 12 && from_version != 13 && from_version != 14 && - from_version != 15) { + from_version != 12 && from_version != 13 && from_version != 14 && + from_version != 15 && from_version != 16 && from_version != 17) { return LARDON3D_PROJECT_DB_CORRUPT; } Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration"); @@ -880,6 +1010,29 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int "finish schema v17 migration"); } } + if (result == LARDON3D_PROJECT_DB_OK && from_version < 18) { + result = execute(database, schema_sparse_derivation_identity_v18, + "migrate sparse derivation identity v17 to v18"); + if (result == LARDON3D_PROJECT_DB_OK) + result = execute(database, schema_sparse_derivation_children_v18, + "migrate sparse derivation children v17 to v18"); + if (result == LARDON3D_PROJECT_DB_OK) + result = execute(database, schema_incremental_reconstruction_v18, + "migrate incremental reconstruction v17 to v18"); +#ifdef LARDON3D_PROJECT_DB_TESTING + if (result == LARDON3D_PROJECT_DB_OK && + getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V18")) { + result = execute(database, "INSERT INTO missing_v18_test_table VALUES(1)", + "forced migration v18 failure"); + } +#endif + if (result == LARDON3D_PROJECT_DB_OK) { + result = execute( + database, + "UPDATE metadata SET value=18 WHERE key='schema_version' AND value=17", + "finish schema v18 migration"); + } + } if (result == LARDON3D_PROJECT_DB_OK) { result = execute(database, "COMMIT", "commit migration"); } @@ -1008,7 +1161,9 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje "sparse_reconstruction_components", "sparse_registered_images", "sparse_landmarks", - "sparse_landmark_observations"}; + "sparse_landmark_observations", + "incremental_reconstructions", + "incremental_reconstruction_tasks"}; for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) && result == LARDON3D_PROJECT_DB_OK; ++index) { @@ -4167,6 +4322,115 @@ Lardon3DProjectDbResult lardon3d_project_db_load_sparse_sfm_task( return result; } +Lardon3DProjectDbResult lardon3d_project_db_record_incremental_reconstruction_task( + Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, + const char *kind, uint32_t version, + const Lardon3DProjectDbCheckpoint *checkpoint, + const Lardon3DProjectDbIncrementalReconstructionTask *parameters, + int64_t updated_at) { + if (!database || !snapshot || !parameters || + !lardon3d_task_kind_is_valid(kind) || + strcmp(kind, "incremental_reconstruction.run") != 0 || version != 1 || + parameters->task_id != snapshot->id || + !valid_task_id(parameters->base_reconstruction_id) || + !valid_task_id(parameters->extension_track_set_id) || + !valid_task_id(parameters->calibration_scope_id) || + parameters->incremental_kind != 1 || parameters->incremental_version != 1) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + Lardon3DProjectDbResult result = lardon3d_project_db_record_task( + database, snapshot, kind, version, checkpoint, updated_at); + if (result != LARDON3D_PROJECT_DB_OK) return result; + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + result = execute(database, "BEGIN IMMEDIATE", "begin incremental task payload"); + if (result == LARDON3D_PROJECT_DB_OK) + result = prepare( + database, + "INSERT INTO incremental_reconstruction_tasks(task_id,base_reconstruction_id," + "extension_track_set_id,calibration_scope_id,incremental_kind,incremental_version," + "parameter_fingerprint) VALUES(?1,?2,?3,?4,?5,?6,?7) ON CONFLICT(task_id) DO " + "UPDATE SET task_id=excluded.task_id WHERE incremental_reconstruction_tasks." + "base_reconstruction_id=excluded.base_reconstruction_id AND " + "incremental_reconstruction_tasks.extension_track_set_id=excluded." + "extension_track_set_id AND incremental_reconstruction_tasks.calibration_scope_id=" + "excluded.calibration_scope_id AND incremental_reconstruction_tasks.incremental_kind=" + "excluded.incremental_kind AND incremental_reconstruction_tasks.incremental_version=" + "excluded.incremental_version AND incremental_reconstruction_tasks." + "parameter_fingerprint=excluded.parameter_fingerprint", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)parameters->task_id); + sqlite3_bind_int64(statement, 2, + (sqlite3_int64)parameters->base_reconstruction_id); + sqlite3_bind_int64(statement, 3, + (sqlite3_int64)parameters->extension_track_set_id); + sqlite3_bind_int64(statement, 4, + (sqlite3_int64)parameters->calibration_scope_id); + sqlite3_bind_int(statement, 5, (int)parameters->incremental_kind); + sqlite3_bind_int(statement, 6, (int)parameters->incremental_version); + sqlite3_bind_blob(statement, 7, parameters->parameter_fingerprint, 32, + SQLITE_TRANSIENT); + result = step_done(database, statement, "upsert incremental task payload"); + statement = NULL; + if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) { + copy_error(database->error, "Payload de reconstruction incrémentale immuable."); + result = LARDON3D_PROJECT_DB_CONSTRAINT; + } + } + if (statement) sqlite3_finalize(statement); + if (result == LARDON3D_PROJECT_DB_OK) + result = execute(database, "COMMIT", "commit incremental task payload"); + if (result != LARDON3D_PROJECT_DB_OK) + (void)execute(database, "ROLLBACK", "rollback incremental task payload"); + (void)pthread_mutex_unlock(&database->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_project_db_load_incremental_reconstruction_task( + Lardon3DProjectDb *database, uint64_t task_id, + Lardon3DProjectDbIncrementalReconstructionTask *parameters) { + if (!database || !valid_task_id(task_id) || !parameters) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + memset(parameters, 0, sizeof(*parameters)); + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + database, + "SELECT base_reconstruction_id,extension_track_set_id,calibration_scope_id," + "incremental_kind,incremental_version,parameter_fingerprint FROM " + "incremental_reconstruction_tasks WHERE task_id=?1", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) + result = LARDON3D_PROJECT_DB_NOT_FOUND; + else if (code != SQLITE_ROW || sqlite3_column_bytes(statement, 5) != 32) + result = LARDON3D_PROJECT_DB_CORRUPT; + else { + parameters->task_id = task_id; + parameters->base_reconstruction_id = + (uint64_t)sqlite3_column_int64(statement, 0); + parameters->extension_track_set_id = + (uint64_t)sqlite3_column_int64(statement, 1); + parameters->calibration_scope_id = + (uint64_t)sqlite3_column_int64(statement, 2); + parameters->incremental_kind = (uint32_t)sqlite3_column_int(statement, 3); + parameters->incremental_version = (uint32_t)sqlite3_column_int(statement, 4); + memcpy(parameters->parameter_fingerprint, sqlite3_column_blob(statement, 5), 32); + if (!valid_task_id(parameters->base_reconstruction_id) || + !valid_task_id(parameters->extension_track_set_id) || + !valid_task_id(parameters->calibration_scope_id) || + parameters->incremental_kind != 1 || parameters->incremental_version != 1) + result = LARDON3D_PROJECT_DB_CORRUPT; + } + sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&database->mutex); + if (result != LARDON3D_PROJECT_DB_OK) memset(parameters, 0, sizeof(*parameters)); + return result; +} + static bool read_match_result(sqlite3_stmt *statement, Lardon3DProjectDbMatchResult *result) { sqlite3_int64 id = sqlite3_column_int64(statement, 0); diff --git a/src/project_db_sparse_sfm.c b/src/project_db_sparse_sfm.c index 1e2cf85..ff4b72e 100644 --- a/src/project_db_sparse_sfm.c +++ b/src/project_db_sparse_sfm.c @@ -34,6 +34,37 @@ static void sparse_put_u32(unsigned char *data, size_t *offset, data[(*offset)++] = (unsigned char)(value >> (byte * 8)); } +static void sparse_put_u64(unsigned char *data, size_t *offset, + uint64_t value); + +static bool sparse_incremental_identity_valid( + const Lardon3DIncrementalReconstructionMetadata *metadata) { + unsigned char record[76] = {0}; + unsigned char digest[32] = {0}; + unsigned int digest_size = 0; + size_t offset = 0; + if (!metadata || metadata->base_reconstruction_id == 0 || + metadata->extension_track_set_id == 0 || + metadata->calibration_scope_id == 0 || metadata->incremental_kind != 1 || + metadata->incremental_version != 1) + return false; + memcpy(record, "L3DHIDV1", 8); + offset = 8; + sparse_put_u32(record, &offset, 1); + sparse_put_u64(record, &offset, metadata->base_reconstruction_id); + sparse_put_u64(record, &offset, metadata->extension_track_set_id); + sparse_put_u64(record, &offset, metadata->calibration_scope_id); + sparse_put_u32(record, &offset, metadata->incremental_kind); + sparse_put_u32(record, &offset, metadata->incremental_version); + memcpy(record + offset, metadata->parameter_fingerprint, 32); + offset += 32; + return offset == sizeof(record) && + EVP_Digest(record, sizeof(record), digest, &digest_size, EVP_sha256(), + NULL) == 1 && + digest_size == sizeof(digest) && + memcmp(digest, metadata->scientific_identity, sizeof(digest)) == 0; +} + static void sparse_put_u64(unsigned char *data, size_t *offset, uint64_t value) { for (unsigned byte = 0; byte < 8; ++byte) @@ -939,7 +970,7 @@ sparse_load_reconstruction_locked(Lardon3DProjectDb *db, uint64_t id, prepare(db, "SELECT " "track_set_id,calibration_scope_id,sfm_kind,sfm_version," - "parameter_fingerprint,component_count,registered_image_count," + "parameter_fingerprint,derivation_identity,component_count,registered_image_count," "landmark_count,reprojection_rmse_px,reprojection_median_px FROM " "sparse_reconstructions WHERE reconstruction_id=?1", &statement); @@ -949,7 +980,9 @@ sparse_load_reconstruction_locked(Lardon3DProjectDb *db, uint64_t id, int code = sqlite3_step(statement); if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND; - else if (code != SQLITE_ROW || sqlite3_column_bytes(statement, 4) != 32) + else if (code != SQLITE_ROW || sqlite3_column_bytes(statement, 4) != 32 || + (sqlite3_column_type(statement, 5) != SQLITE_NULL && + sqlite3_column_bytes(statement, 5) != 32)) result = LARDON3D_PROJECT_DB_CORRUPT; else { memset(output, 0, sizeof(*output)); @@ -960,12 +993,17 @@ sparse_load_reconstruction_locked(Lardon3DProjectDb *db, uint64_t id, output->sfm_version = (uint32_t)sqlite3_column_int64(statement, 3); memcpy(output->parameter_fingerprint, sqlite3_column_blob(statement, 4), 32); - output->component_count = (uint64_t)sqlite3_column_int64(statement, 5); + output->has_derivation_identity = + sqlite3_column_type(statement, 5) != SQLITE_NULL; + if (output->has_derivation_identity) + memcpy(output->derivation_identity, sqlite3_column_blob(statement, 5), + 32); + output->component_count = (uint64_t)sqlite3_column_int64(statement, 6); output->registered_image_count = - (uint64_t)sqlite3_column_int64(statement, 6); - output->landmark_count = (uint64_t)sqlite3_column_int64(statement, 7); - output->reprojection_rmse_px = sqlite3_column_double(statement, 8); - output->reprojection_median_px = sqlite3_column_double(statement, 9); + (uint64_t)sqlite3_column_int64(statement, 7); + output->landmark_count = (uint64_t)sqlite3_column_int64(statement, 8); + output->reprojection_rmse_px = sqlite3_column_double(statement, 9); + output->reprojection_median_px = sqlite3_column_double(statement, 10); if (!output->track_set_id || !output->calibration_scope_id || output->sfm_kind != LARDON3D_SPARSE_SFM_KIND_INCREMENTAL || output->sfm_version != LARDON3D_SPARSE_SFM_VERSION || @@ -981,10 +1019,21 @@ sparse_load_reconstruction_locked(Lardon3DProjectDb *db, uint64_t id, return result; } -Lardon3DProjectDbResult lardon3d_sparse_reconstruction_publish( +static Lardon3DProjectDbResult sparse_reconstruction_publish_internal( Lardon3DProjectDb *db, const Lardon3DSparsePublication *publication, + const Lardon3DIncrementalReconstructionMetadata *metadata, Lardon3DSparseReconstruction *output) { - if (!db || !output || !sparse_publication_valid(publication)) + if (!db || !output || !sparse_publication_valid(publication) || + (metadata && + memcmp(publication->parameter_fingerprint, + metadata->parameter_fingerprint, 32) != 0) || + (metadata && + (!sparse_fits_sqlite(metadata->base_reconstruction_id) || + metadata->base_reconstruction_id == 0 || + metadata->extension_track_set_id != publication->track_set_id || + metadata->calibration_scope_id != publication->calibration_scope_id || + metadata->incremental_kind != 1 || metadata->incremental_version != 1 || + !sparse_incremental_identity_valid(metadata)))) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; (void)pthread_mutex_lock(&db->mutex); Lardon3DProjectDbResult result = @@ -1022,9 +1071,9 @@ Lardon3DProjectDbResult lardon3d_sparse_reconstruction_publish( db, "INSERT INTO " "sparse_reconstructions(track_set_id,calibration_scope_id,sfm_kind,sfm_" - "version,parameter_fingerprint,component_count,registered_image_count," + "version,parameter_fingerprint,derivation_identity,component_count,registered_image_count," "landmark_count,reprojection_rmse_px,reprojection_median_px,created_at)" - " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)", + " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)", &statement); if (result == LARDON3D_PROJECT_DB_OK) { sqlite3_bind_int64(statement, 1, @@ -1036,20 +1085,26 @@ Lardon3DProjectDbResult lardon3d_sparse_reconstruction_publish( sqlite3_bind_int64(statement, 4, publication->sfm_version); sqlite3_bind_blob(statement, 5, publication->parameter_fingerprint, 32, SQLITE_TRANSIENT); - sqlite3_bind_int64(statement, 6, - (sqlite3_int64)publication->component_count); + if (metadata) + sqlite3_bind_blob(statement, 6, metadata->scientific_identity, 32, + SQLITE_TRANSIENT); + else + sqlite3_bind_null(statement, 6); sqlite3_bind_int64(statement, 7, - (sqlite3_int64)publication->registered_image_count); + (sqlite3_int64)publication->component_count); sqlite3_bind_int64(statement, 8, + (sqlite3_int64)publication->registered_image_count); + sqlite3_bind_int64(statement, 9, (sqlite3_int64)publication->landmark_count); - sqlite3_bind_double(statement, 9, publication->reprojection_rmse_px); - sqlite3_bind_double(statement, 10, publication->reprojection_median_px); - sqlite3_bind_int64(statement, 11, publication->created_at); + sqlite3_bind_double(statement, 10, publication->reprojection_rmse_px); + sqlite3_bind_double(statement, 11, publication->reprojection_median_px); + sqlite3_bind_int64(statement, 12, publication->created_at); result = step_done(db, statement, "insert sparse reconstruction"); statement = NULL; } if (statement) sqlite3_finalize(statement); + statement = NULL; if (result == LARDON3D_PROJECT_DB_OK) reconstruction_id = (uint64_t)sqlite3_last_insert_rowid(db->connection); if (result == LARDON3D_PROJECT_DB_OK) @@ -1156,6 +1211,33 @@ Lardon3DProjectDbResult lardon3d_sparse_reconstruction_publish( } if (statement) sqlite3_finalize(statement); + statement = NULL; + if (result == LARDON3D_PROJECT_DB_OK && metadata) + result = prepare( + db, + "INSERT INTO incremental_reconstructions(reconstruction_id," + "base_reconstruction_id,extension_track_set_id,calibration_scope_id," + "incremental_kind,incremental_version,parameter_fingerprint," + "scientific_identity) VALUES(?1,?2,?3,?4,?5,?6,?7,?8)", + &statement); + if (result == LARDON3D_PROJECT_DB_OK && metadata) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)reconstruction_id); + sqlite3_bind_int64(statement, 2, + (sqlite3_int64)metadata->base_reconstruction_id); + sqlite3_bind_int64(statement, 3, + (sqlite3_int64)metadata->extension_track_set_id); + sqlite3_bind_int64(statement, 4, + (sqlite3_int64)metadata->calibration_scope_id); + sqlite3_bind_int(statement, 5, (int)metadata->incremental_kind); + sqlite3_bind_int(statement, 6, (int)metadata->incremental_version); + sqlite3_bind_blob(statement, 7, metadata->parameter_fingerprint, 32, + SQLITE_TRANSIENT); + sqlite3_bind_blob(statement, 8, metadata->scientific_identity, 32, + SQLITE_TRANSIENT); + result = step_done(db, statement, "insert incremental reconstruction metadata"); + statement = NULL; + } + if (statement) sqlite3_finalize(statement); if (result == LARDON3D_PROJECT_DB_OK) result = execute(db, "COMMIT", "commit sparse reconstruction"); if (result != LARDON3D_PROJECT_DB_OK) @@ -1166,6 +1248,131 @@ Lardon3DProjectDbResult lardon3d_sparse_reconstruction_publish( return result; } +Lardon3DProjectDbResult lardon3d_sparse_reconstruction_publish( + Lardon3DProjectDb *db, const Lardon3DSparsePublication *publication, + Lardon3DSparseReconstruction *output) { + return sparse_reconstruction_publish_internal(db, publication, NULL, output); +} + +Lardon3DProjectDbResult lardon3d_incremental_reconstruction_publish( + Lardon3DProjectDb *db, const Lardon3DSparsePublication *publication, + const Lardon3DIncrementalReconstructionMetadata *metadata, + Lardon3DSparseReconstruction *output) { + if (!metadata) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + return sparse_reconstruction_publish_internal(db, publication, metadata, output); +} + +static Lardon3DProjectDbResult incremental_metadata_locked( + Lardon3DProjectDb *db, uint64_t reconstruction_id, + Lardon3DIncrementalReconstructionMetadata *metadata) { + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + db, + "SELECT base_reconstruction_id,extension_track_set_id,calibration_scope_id," + "incremental_kind,incremental_version,parameter_fingerprint,scientific_identity " + "FROM incremental_reconstructions WHERE reconstruction_id=?1", + &statement); + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_int64(statement, 1, (sqlite3_int64)reconstruction_id); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) + result = LARDON3D_PROJECT_DB_NOT_FOUND; + else if (code != SQLITE_ROW || sqlite3_column_bytes(statement, 5) != 32 || + sqlite3_column_bytes(statement, 6) != 32) + result = LARDON3D_PROJECT_DB_CORRUPT; + else { + memset(metadata, 0, sizeof(*metadata)); + metadata->reconstruction_id = reconstruction_id; + metadata->base_reconstruction_id = + (uint64_t)sqlite3_column_int64(statement, 0); + metadata->extension_track_set_id = + (uint64_t)sqlite3_column_int64(statement, 1); + metadata->calibration_scope_id = + (uint64_t)sqlite3_column_int64(statement, 2); + metadata->incremental_kind = (uint32_t)sqlite3_column_int(statement, 3); + metadata->incremental_version = (uint32_t)sqlite3_column_int(statement, 4); + memcpy(metadata->parameter_fingerprint, sqlite3_column_blob(statement, 5), 32); + memcpy(metadata->scientific_identity, sqlite3_column_blob(statement, 6), 32); + if (!metadata->base_reconstruction_id || !metadata->extension_track_set_id || + !metadata->calibration_scope_id || metadata->incremental_kind != 1 || + metadata->incremental_version != 1 || + !sparse_incremental_identity_valid(metadata)) + result = LARDON3D_PROJECT_DB_CORRUPT; + } + sqlite3_finalize(statement); + } + if (result != LARDON3D_PROJECT_DB_OK) memset(metadata, 0, sizeof(*metadata)); + return result; +} + +Lardon3DProjectDbResult lardon3d_incremental_reconstruction_load_metadata( + Lardon3DProjectDb *db, uint64_t reconstruction_id, + Lardon3DIncrementalReconstructionMetadata *metadata) { + if (!db || !reconstruction_id || !metadata) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + (void)pthread_mutex_lock(&db->mutex); + Lardon3DProjectDbResult result = + incremental_metadata_locked(db, reconstruction_id, metadata); + Lardon3DSparseReconstruction reconstruction = {0}; + if (result == LARDON3D_PROJECT_DB_OK) + result = sparse_load_reconstruction_locked(db, reconstruction_id, + &reconstruction); + if (result == LARDON3D_PROJECT_DB_OK && + (!reconstruction.has_derivation_identity || + memcmp(reconstruction.derivation_identity, + metadata->scientific_identity, 32) != 0 || + memcmp(reconstruction.parameter_fingerprint, + metadata->parameter_fingerprint, 32) != 0)) + result = LARDON3D_PROJECT_DB_CORRUPT; + (void)pthread_mutex_unlock(&db->mutex); + return result; +} + +Lardon3DProjectDbResult lardon3d_incremental_reconstruction_find_exact( + Lardon3DProjectDb *db, const unsigned char scientific_identity[32], + Lardon3DIncrementalReconstructionMetadata *metadata, + Lardon3DSparseReconstruction *reconstruction) { + if (!db || !scientific_identity || !metadata || !reconstruction) + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + memset(metadata, 0, sizeof(*metadata)); + memset(reconstruction, 0, sizeof(*reconstruction)); + (void)pthread_mutex_lock(&db->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult result = prepare( + db, + "SELECT reconstruction_id FROM incremental_reconstructions WHERE " + "scientific_identity=?1", + &statement); + uint64_t reconstruction_id = 0; + if (result == LARDON3D_PROJECT_DB_OK) { + sqlite3_bind_blob(statement, 1, scientific_identity, 32, SQLITE_TRANSIENT); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) + result = LARDON3D_PROJECT_DB_NOT_FOUND; + else if (code != SQLITE_ROW) + result = LARDON3D_PROJECT_DB_CORRUPT; + else + reconstruction_id = (uint64_t)sqlite3_column_int64(statement, 0); + sqlite3_finalize(statement); + } + if (result == LARDON3D_PROJECT_DB_OK) + result = incremental_metadata_locked(db, reconstruction_id, metadata); + if (result == LARDON3D_PROJECT_DB_OK) + result = sparse_load_reconstruction_locked(db, reconstruction_id, reconstruction); + if (result == LARDON3D_PROJECT_DB_OK && + (!reconstruction->has_derivation_identity || + memcmp(reconstruction->derivation_identity, scientific_identity, 32) != 0 || + memcmp(reconstruction->parameter_fingerprint, + metadata->parameter_fingerprint, 32) != 0)) + result = LARDON3D_PROJECT_DB_CORRUPT; + (void)pthread_mutex_unlock(&db->mutex); + if (result != LARDON3D_PROJECT_DB_OK) { + memset(metadata, 0, sizeof(*metadata)); + memset(reconstruction, 0, sizeof(*reconstruction)); + } + return result; +} + Lardon3DProjectDbResult lardon3d_sparse_reconstruction_find_exact( Lardon3DProjectDb *db, uint64_t track_set_id, uint64_t scope_id, uint32_t kind, uint32_t version, const unsigned char fingerprint[32], @@ -1178,7 +1385,8 @@ Lardon3DProjectDbResult lardon3d_sparse_reconstruction_find_exact( prepare(db, "SELECT reconstruction_id FROM sparse_reconstructions WHERE " "track_set_id=?1 AND calibration_scope_id=?2 AND sfm_kind=?3 AND " - "sfm_version=?4 AND parameter_fingerprint=?5", + "sfm_version=?4 AND parameter_fingerprint=?5 AND " + "derivation_identity IS NULL", &statement); if (result == LARDON3D_PROJECT_DB_OK) { sqlite3_bind_int64(statement, 1, (sqlite3_int64)track_set_id); diff --git a/src/sparse_sfm_bundle_adjustment.cpp b/src/sparse_sfm_bundle_adjustment.cpp index a24f011..0433995 100644 --- a/src/sparse_sfm_bundle_adjustment.cpp +++ b/src/sparse_sfm_bundle_adjustment.cpp @@ -1,5 +1,7 @@ #include +#include "incremental_reconstruction_internal.h" + #include #include @@ -547,8 +549,10 @@ void copy_view(std::vector *destination, const T *source, size_t count) { if (count != 0) destination->assign(source, source + count); } -PreparationStatus prepare(const Lardon3DSparseBundleAdjustmentInput &input, - Preparation *preparation) { +PreparationStatus prepare( + const Lardon3DSparseBundleAdjustmentInput &input, Preparation *preparation, + const Lardon3DSparseBundleAdjustmentAnchor *anchors = nullptr, + size_t anchor_count = 0) { if (!preparation || !input.incremental_result) return PreparationStatus::invalid_argument; const auto &result = *input.incremental_result; @@ -730,10 +734,45 @@ PreparationStatus prepare(const Lardon3DSparseBundleAdjustmentInput &input, diagnostic.termination = LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_TERMINATION_NOT_RUN; diagnostic.rejection_reason = LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_REJECTION_INELIGIBLE; + const Lardon3DSparseBundleAdjustmentAnchor *override = nullptr; + for (size_t anchor_index = 0; anchor_index < anchor_count; ++anchor_index) { + if (anchors[anchor_index].component_key == component.component_key) { + if (override) return PreparationStatus::invalid_argument; + override = &anchors[anchor_index]; + } + } + bool anchors_valid = false; + if (override) { + const auto pose = std::find_if(candidate.cameras.begin(), candidate.cameras.end(), + [&](const auto &camera) { + return camera.component_key == component.component_key && + camera.image_id == + override->pose_anchor_image_id; + }); + const auto scale = std::find_if(candidate.cameras.begin(), candidate.cameras.end(), + [&](const auto &camera) { + return camera.component_key == component.component_key && + camera.image_id == + override->scale_anchor_image_id; + }); + anchors_valid = pose != candidate.cameras.end() && + scale != candidate.cameras.end() && pose != scale && + override->scale_axis >= + LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_AXIS_X && + override->scale_axis <= + LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_AXIS_Z; + if (anchors_valid) { + diagnostic.pose_anchor_image_id = override->pose_anchor_image_id; + diagnostic.scale_anchor_image_id = override->scale_anchor_image_id; + diagnostic.scale_axis = override->scale_axis; + diagnostic.has_anchors = true; + } + } else { + anchors_valid = select_anchors(candidate.cameras, component.component_key, + &diagnostic); + } diagnostic.eligible = component.registered_image_count >= 2 && - component.landmark_count >= 1 && - select_anchors(candidate.cameras, component.component_key, - &diagnostic); + component.landmark_count >= 1 && anchors_valid; if (diagnostic.eligible) diagnostic.rejection_reason = LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_REJECTION_NONE; else if (!diagnostic.has_anchors && component.registered_image_count >= 2) @@ -1065,17 +1104,19 @@ void destroy_result(Lardon3DSparseBundleAdjustmentResult *result) { } // namespace lardon3d::sparse_bundle_adjustment -extern "C" Lardon3DSparseBundleAdjustmentExecutionStatus -lardon3d_sparse_bundle_adjustment_run( +static Lardon3DSparseBundleAdjustmentExecutionStatus run_bundle_adjustment( const Lardon3DSparseBundleAdjustmentInput *input, + const Lardon3DSparseBundleAdjustmentAnchor *anchors, size_t anchor_count, Lardon3DSparseBundleAdjustmentResult *result) { using namespace lardon3d::sparse_bundle_adjustment; if (!result) return LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_EXECUTION_INVALID_ARGUMENT; destroy_result(result); - if (!input) return LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_EXECUTION_INVALID_ARGUMENT; + if (!input || (anchor_count != 0 && !anchors)) + return LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_EXECUTION_INVALID_ARGUMENT; try { Preparation preparation; - const PreparationStatus preparation_status = prepare(*input, &preparation); + const PreparationStatus preparation_status = + prepare(*input, &preparation, anchors, anchor_count); if (preparation_status == PreparationStatus::invalid_argument) return LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_EXECUTION_INVALID_ARGUMENT; if (preparation_status == PreparationStatus::out_of_memory) @@ -1145,6 +1186,21 @@ lardon3d_sparse_bundle_adjustment_run( } } +extern "C" Lardon3DSparseBundleAdjustmentExecutionStatus +lardon3d_sparse_bundle_adjustment_run( + const Lardon3DSparseBundleAdjustmentInput *input, + Lardon3DSparseBundleAdjustmentResult *result) { + return run_bundle_adjustment(input, nullptr, 0, result); +} + +Lardon3DSparseBundleAdjustmentExecutionStatus +lardon3d_sparse_bundle_adjustment_run_with_anchors( + const Lardon3DSparseBundleAdjustmentInput *input, + const Lardon3DSparseBundleAdjustmentAnchor *anchors, size_t anchor_count, + Lardon3DSparseBundleAdjustmentResult *result) { + return run_bundle_adjustment(input, anchors, anchor_count, result); +} + extern "C" void lardon3d_sparse_bundle_adjustment_result_destroy( Lardon3DSparseBundleAdjustmentResult *result) { lardon3d::sparse_bundle_adjustment::destroy_result(result); diff --git a/src/sparse_sfm_gate_f.cpp b/src/sparse_sfm_gate_f.cpp index ad0a36b..18cec4d 100644 --- a/src/sparse_sfm_gate_f.cpp +++ b/src/sparse_sfm_gate_f.cpp @@ -150,3 +150,30 @@ bool lardon3d_sparse_sfm_publication_metrics(const double *squared_errors, size_ return false; } } + +bool lardon3d_sparse_sfm_squared_reprojection_error( + const Lardon3DSparseGeometryCalibration *calibration, + const Lardon3DSparseGeometryPose *pose, + const Lardon3DSparseGeometryPoint3 *point, + const Lardon3DSparseGeometryPoint2 *observed, double *squared_error) { + if (!calibration || !pose || !point || !observed || !squared_error) return false; + const double *r = pose->rotation_cw; + const double *t = pose->translation_cw; + const double xc = r[0] * point->x + r[1] * point->y + r[2] * point->z + t[0]; + const double yc = r[3] * point->x + r[4] * point->y + r[5] * point->z + t[1]; + const double zc = r[6] * point->x + r[7] * point->y + r[8] * point->z + t[2]; + if (!std::isfinite(xc) || !std::isfinite(yc) || !std::isfinite(zc) || zc <= 1e-9) + return false; + const double xn = xc / zc; + const double yn = yc / zc; + const double r2 = xn * xn + yn * yn; + const double radial = 1.0 + calibration->k1 * r2 + calibration->k2 * r2 * r2; + const double xd = xn * radial + 2.0 * calibration->p1 * xn * yn + + calibration->p2 * (r2 + 2.0 * xn * xn); + const double yd = yn * radial + calibration->p1 * (r2 + 2.0 * yn * yn) + + 2.0 * calibration->p2 * xn * yn; + const double dx = calibration->fx * xd + calibration->cx - observed->x; + const double dy = calibration->fy * yd + calibration->cy - observed->y; + *squared_error = dx * dx + dy * dy; + return std::isfinite(*squared_error); +} diff --git a/src/sparse_sfm_gate_f_internal.h b/src/sparse_sfm_gate_f_internal.h index 5d10835..085e418 100644 --- a/src/sparse_sfm_gate_f_internal.h +++ b/src/sparse_sfm_gate_f_internal.h @@ -19,5 +19,10 @@ bool lardon3d_sparse_sfm_component_persistable(uint64_t registered_image_count, uint64_t landmark_count); bool lardon3d_sparse_sfm_publication_metrics(const double *squared_errors, size_t count, double *rmse, double *median); +bool lardon3d_sparse_sfm_squared_reprojection_error( + const Lardon3DSparseGeometryCalibration *calibration, + const Lardon3DSparseGeometryPose *pose, + const Lardon3DSparseGeometryPoint3 *point, + const Lardon3DSparseGeometryPoint2 *observed, double *squared_error); #endif diff --git a/src/sparse_sfm_task.cpp b/src/sparse_sfm_task.cpp index e5225b2..42a8329 100644 --- a/src/sparse_sfm_task.cpp +++ b/src/sparse_sfm_task.cpp @@ -389,25 +389,19 @@ bool build_publication(const Resolved &resolved, observation.feature_index); if (camera == cameras.end() || calibration == resolved.calibrations.end() || !source) return false; - const auto &pose = *camera->second; - const auto &point = *landmark->second; - double xc = pose.rotation_cw[0] * point.x + pose.rotation_cw[1] * point.y + - pose.rotation_cw[2] * point.z + pose.translation_cw[0]; - double yc = pose.rotation_cw[3] * point.x + pose.rotation_cw[4] * point.y + - pose.rotation_cw[5] * point.z + pose.translation_cw[1]; - double zc = pose.rotation_cw[6] * point.x + pose.rotation_cw[7] * point.y + - pose.rotation_cw[8] * point.z + pose.translation_cw[2]; - if (!std::isfinite(xc) || !std::isfinite(yc) || !std::isfinite(zc) || zc <= 1e-9) + Lardon3DSparseGeometryPose pose{}; + std::memcpy(pose.rotation_cw, camera->second->rotation_cw, + sizeof(pose.rotation_cw)); + std::memcpy(pose.translation_cw, camera->second->translation_cw, + sizeof(pose.translation_cw)); + const Lardon3DSparseGeometryPoint3 point{landmark->second->x, + landmark->second->y, + landmark->second->z}; + const Lardon3DSparseGeometryPoint2 observed{source->x, source->y}; + double squared = 0.0; + if (!lardon3d_sparse_sfm_squared_reprojection_error( + &calibration->second, &pose, &point, &observed, &squared)) return false; - double xn = xc / zc, yn = yc / zc, r2 = xn * xn + yn * yn; - const auto &cal = calibration->second; - double radial = 1.0 + cal.k1 * r2 + cal.k2 * r2 * r2; - double xd = xn * radial + 2.0 * cal.p1 * xn * yn + cal.p2 * (r2 + 2.0 * xn * xn); - double yd = yn * radial + cal.p1 * (r2 + 2.0 * yn * yn) + 2.0 * cal.p2 * xn * yn; - double dx = cal.fx * xd + cal.cx - source->x; - double dy = cal.fy * yd + cal.cy - source->y; - double squared = dx * dx + dy * dy; - if (!std::isfinite(squared)) return false; squared_errors.push_back(squared); storage->observations.push_back( {0, observation.track_id, observation.feature_set_id, observation.feature_index, diff --git a/src/task_kinds.c b/src/task_kinds.c index e01abdd..6365b46 100644 --- a/src/task_kinds.c +++ b/src/task_kinds.c @@ -11,6 +11,9 @@ #ifdef LARDON3D_SPARSE_SFM_TASK_AVAILABLE #include #endif +#ifdef LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_AVAILABLE +#include +#endif #include const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) { @@ -68,6 +71,13 @@ const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) { .kind_version = LARDON3D_SPARSE_SFM_TASK_KIND_VERSION, .reconstruct = lardon3d_sparse_sfm_task_reconstruct, }, +#endif +#ifdef LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_AVAILABLE + { + .kind = LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_KIND, + .kind_version = LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_KIND_VERSION, + .reconstruct = lardon3d_incremental_reconstruction_task_reconstruct, + }, #endif }; static const Lardon3DTaskKindRegistry registry = { diff --git a/tests/test_geometric_verification.c b/tests/test_geometric_verification.c index 9a93fb8..d312fea 100644 --- a/tests/test_geometric_verification.c +++ b/tests/test_geometric_verification.c @@ -172,6 +172,8 @@ static bool create_v11_database(const char *path) { lardon3d_project_db_close(database); return execute_sql(path, "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE IF EXISTS incremental_reconstruction_tasks;" + "DROP TABLE IF EXISTS incremental_reconstructions;" "DROP TABLE IF EXISTS sparse_sfm_tasks;" "DROP TABLE IF EXISTS sparse_landmark_observations;" "DROP TABLE IF EXISTS sparse_landmarks;" @@ -195,7 +197,7 @@ static bool test_model_api(const char *path) { Lardon3DProjectDb *database = NULL; CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); Parents parents; CHECK(create_parents(database, &parents)); @@ -479,7 +481,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) { 0)); CHECK(lardon3d_project_db_open(v11_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); Parents parents; CHECK(create_parents(database, &parents)); unsigned char fingerprint[32] = {0x91}; @@ -494,7 +496,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) { migrated_result.geometric_verification_result_id; lardon3d_project_db_close(database); CHECK(query_integer( - v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 17)); + v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 18)); CHECK( query_integer(v11_path, "SELECT count(*) FROM sqlite_master WHERE type='index' AND " @@ -523,7 +525,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) { 0)); CHECK(lardon3d_project_db_open(failed_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); return true; } diff --git a/tests/test_geometric_verifier_task.c b/tests/test_geometric_verifier_task.c index 30d9669..4ed3bc2 100644 --- a/tests/test_geometric_verifier_task.c +++ b/tests/test_geometric_verifier_task.c @@ -332,6 +332,8 @@ static bool run_task_test(void) { CHECK(exec_sql( database_path, "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE IF EXISTS incremental_reconstruction_tasks;" + "DROP TABLE IF EXISTS incremental_reconstructions;" "DROP TABLE IF EXISTS sparse_sfm_tasks;" "DROP TABLE IF EXISTS sparse_landmark_observations;" "DROP TABLE IF EXISTS sparse_landmarks;" @@ -362,7 +364,7 @@ static bool run_task_test(void) { 0)); CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); CHECK(remove_tree(root)); return true; diff --git a/tests/test_incremental_reconstruction.cpp b/tests/test_incremental_reconstruction.cpp new file mode 100644 index 0000000..b3e951a --- /dev/null +++ b/tests/test_incremental_reconstruction.cpp @@ -0,0 +1,473 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#define CHECK(value) \ + do { \ + if (!(value)) { \ + std::fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, \ + #value); \ + return false; \ + } \ + } while (0) + +namespace { +Lardon3DSparseGeometryCalibration calibration() { + return {640, 480, 800.0, 800.0, 320.0, 240.0, 0.0, 0.0, 0.0, 0.0}; +} + +Lardon3DSparseGeometryPose pose(double center_x) { + Lardon3DSparseGeometryPose result{}; + result.rotation_cw[0] = result.rotation_cw[4] = result.rotation_cw[8] = 1.0; + result.translation_cw[0] = -center_x; + return result; +} + +Lardon3DSparseGeometryPoint2 project(double center_x, + const Lardon3DSparseGeometryPoint3 &point) { + return {800.0 * (point.x - center_x) / point.z + 320.0, + 800.0 * point.y / point.z + 240.0}; +} + +struct Fixture { + Lardon3DSparseIncrementalParameters parameters{}; + std::vector components; + std::vector cameras; + std::vector landmarks; + std::vector base_observations; + std::vector base_tracks; + std::vector images; + std::vector extension_observations; + Lardon3DSparseIncrementalResult base{}; + Lardon3DSparseIncrementalInput extension{}; + + void refresh() { + base.components = components.data(); + base.component_count = components.size(); + base.cameras = cameras.data(); + base.camera_count = cameras.size(); + base.landmarks = landmarks.data(); + base.landmark_count = landmarks.size(); + base.observations = base_observations.data(); + base.observation_count = base_observations.size(); + extension.images = images.data(); + extension.image_count = images.size(); + extension.observations = extension_observations.data(); + extension.observation_count = extension_observations.size(); + } + + Fixture(uint64_t new_image_id, bool descendant_new_observations, + bool new_only_observations) { + (void)lardon3d_sparse_incremental_parameters_default(¶meters); + parameters.minimum_pnp_correspondences = 6; + parameters.pnp.minimum_inliers = 6; + components.push_back({100, 2, 2, 8}); + cameras.push_back({100, 100, pose(0.0)}); + cameras.push_back({101, 100, pose(1.0)}); + images.push_back({100, calibration()}); + images.push_back({101, calibration()}); + images.push_back({new_image_id, calibration()}); + for (uint32_t index = 0; index < 8; ++index) { + Lardon3DSparseGeometryPoint3 point{ + -0.8 + 0.25 * index, -0.35 + 0.1 * (index % 4), + 4.0 + 0.3 * (index % 3)}; + const uint64_t base_track = 10 + index; + const uint64_t extension_track = 1000 + index; + landmarks.push_back({500 + index, base_track, 100, point, 0.0, 0.0, 2}); + for (uint32_t camera_index = 0; camera_index < 2; ++camera_index) { + const uint64_t image_id = 100 + camera_index; + const uint64_t feature_set = 10000 + image_id; + const auto pixel = project(static_cast(camera_index), point); + base_tracks.push_back({base_track, image_id, feature_set, index, 64, + pixel.x, pixel.y}); + extension_observations.push_back( + {extension_track, image_id, feature_set, index, 64, pixel.x, pixel.y}); + base_observations.push_back({500 + index, base_track, image_id, + feature_set, index, camera_index}); + } + if (descendant_new_observations) { + const auto pixel = project(0.5, point); + extension_observations.push_back( + {extension_track, new_image_id, 10000 + new_image_id, index, 64, + pixel.x, pixel.y}); + } + } + if (new_only_observations) + extension_observations.push_back( + {9000, new_image_id, 20000 + new_image_id, 0, 1, 320.0, 240.0}); + base.status = LARDON3D_SPARSE_INCREMENTAL_COMPLETE; + base.track_set_id = 1; + base.calibration_scope_id = 1; + extension = {2, 2, images.data(), images.size(), extension_observations.data(), + extension_observations.size()}; + refresh(); + } + + Lardon3DIncrementalReconstructionStatus run( + Lardon3DIncrementalReconstructionResult *result) { + Lardon3DIncrementalReconstructionInput input{ + 7, &base, base_tracks.data(), base_tracks.size(), &extension, + ¶meters, nullptr, nullptr}; + return lardon3d_incremental_reconstruction_run(&input, result); + } +}; + +bool test_identity_and_estimate() { + unsigned char fingerprint[32]{}; + CHECK(lardon3d_incremental_reconstruction_parameter_fingerprint(fingerprint)); + const unsigned char golden[32] = { + 0xf4, 0x4a, 0x89, 0xb2, 0x3b, 0x52, 0x04, 0x81, + 0x70, 0x18, 0x48, 0xec, 0xf1, 0x75, 0x63, 0x8e, + 0x82, 0xf2, 0xc9, 0xe2, 0x5b, 0x70, 0xa0, 0x1f, + 0x3e, 0xb7, 0x67, 0xbf, 0x28, 0x44, 0x6c, 0xd8}; + CHECK(std::memcmp(fingerprint, golden, 32) == 0); + const unsigned char zero[32]{}; + CHECK(std::memcmp(fingerprint, zero, 32) != 0); + Lardon3DIncrementalReconstructionIdentity identity{7, 8, 9, 1, 1, {}}; + std::memcpy(identity.parameter_fingerprint, fingerprint, 32); + unsigned char first[32]{}; + unsigned char second[32]{}; + CHECK(lardon3d_incremental_reconstruction_identity_digest(&identity, first)); + CHECK(lardon3d_incremental_reconstruction_identity_digest(&identity, second)); + CHECK(std::memcmp(first, second, 32) == 0); + ++identity.base_reconstruction_id; + CHECK(lardon3d_incremental_reconstruction_identity_digest(&identity, second)); + CHECK(std::memcmp(first, second, 32) != 0); + --identity.base_reconstruction_id; + ++identity.extension_track_set_id; + CHECK(lardon3d_incremental_reconstruction_identity_digest(&identity, second)); + CHECK(std::memcmp(first, second, 32) != 0); + --identity.extension_track_set_id; + ++identity.calibration_scope_id; + CHECK(lardon3d_incremental_reconstruction_identity_digest(&identity, second)); + CHECK(std::memcmp(first, second, 32) != 0); + + Lardon3DIncrementalReconstructionShape shape{2, 8, 16, 3, 8, 24}; + Lardon3DResourceEstimate estimate{}; + CHECK(lardon3d_incremental_reconstruction_resource_estimate(&shape, &estimate)); + CHECK(estimate.memory_fixed_bytes == 269484032); + CHECK(estimate.minimum_batch_size == 1 && estimate.maximum_batch_size == 1); + CHECK(estimate.desired_cpu_threads == 1 && estimate.desired_gpu_slots == 0 && + estimate.desired_io_slots == 1 && + estimate.task_class == LARDON3D_RESOURCE_TASK_CPU); + shape.base_camera_count = std::numeric_limits::max(); + CHECK(!lardon3d_incremental_reconstruction_resource_estimate(&shape, &estimate)); + return true; +} + +bool test_lineage_failures() { + Fixture missing(150, false, false); + missing.extension_observations.erase(missing.extension_observations.begin()); + missing.extension.observations = missing.extension_observations.data(); + missing.extension.observation_count = missing.extension_observations.size(); + Lardon3DIncrementalReconstructionResult result{}; + CHECK(missing.run(&result) == LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_MISSING); + + Fixture split(150, false, false); + split.extension_observations[1].track_id = 7000; + CHECK(split.run(&result) == LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_SPLIT); + + Fixture merge(150, false, false); + for (auto &observation : merge.extension_observations) + if (observation.track_id == 1001) observation.track_id = 1000; + CHECK(merge.run(&result) == LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_MERGE); + + Fixture duplicate(150, false, false); + duplicate.extension_observations.push_back(duplicate.extension_observations.front()); + duplicate.extension.observations = duplicate.extension_observations.data(); + duplicate.extension.observation_count = duplicate.extension_observations.size(); + CHECK(duplicate.run(&result) == + LARDON3D_INCREMENTAL_RECONSTRUCTION_LINEAGE_DUPLICATE); + return true; +} + +bool test_h_b01() { + Fixture fixture(150, true, false); + fixture.parameters.maximum_registration_rounds = 0; + Lardon3DIncrementalReconstructionResult result{}; + CHECK(fixture.run(&result) == + LARDON3D_INCREMENTAL_RECONSTRUCTION_DESCENDANT_UNREGISTERED_OBSERVATION); + + Fixture new_only(150, false, true); + new_only.parameters.maximum_registration_rounds = 0; + CHECK(new_only.run(&result) == LARDON3D_INCREMENTAL_RECONSTRUCTION_NO_CHANGE); + CHECK(!result.changed && result.snapshot.component_count == 0); + return true; +} + +bool test_h_b02() { + Fixture fixture(99, true, false); + Lardon3DIncrementalReconstructionResult result{}; + CHECK(fixture.run(&result) == + LARDON3D_INCREMENTAL_RECONSTRUCTION_COMPONENT_KEY_VIOLATION); + CHECK(!result.changed && result.snapshot.component_count == 0); + return true; +} + +bool reject_checkpoint(void *) { return false; } + +bool test_cancellation() { + Fixture fixture(150, true, false); + Lardon3DIncrementalReconstructionInput input{ + 7, &fixture.base, fixture.base_tracks.data(), fixture.base_tracks.size(), + &fixture.extension, &fixture.parameters, reject_checkpoint, nullptr}; + Lardon3DIncrementalReconstructionResult result{}; + CHECK(lardon3d_incremental_reconstruction_run(&input, &result) == + LARDON3D_INCREMENTAL_RECONSTRUCTION_CANCELLED); + CHECK(!result.changed && result.snapshot.component_count == 0); + return true; +} + +bool test_successful_enrichment() { + Fixture fixture(150, true, false); + Lardon3DIncrementalReconstructionResult result{}; + CHECK(fixture.run(&result) == LARDON3D_INCREMENTAL_RECONSTRUCTION_OK); + CHECK(result.changed && result.snapshot.component_count == 1 && + result.snapshot.camera_count == 3 && result.snapshot.landmark_count == 8 && + result.snapshot.observation_count == 24); + CHECK(result.snapshot.components[0].component_key == 100); + CHECK(result.snapshot.cameras[0].image_id == 100 && + result.snapshot.cameras[2].image_id == 150); + lardon3d_incremental_reconstruction_result_destroy(&result); + return true; +} + +void add_second_component(Fixture *fixture) { + fixture->components.push_back({200, 2, 2, 8}); + fixture->cameras.push_back({200, 200, pose(10.0)}); + fixture->cameras.push_back({201, 200, pose(11.0)}); + fixture->images.push_back({200, calibration()}); + fixture->images.push_back({201, calibration()}); + for (uint32_t index = 0; index < 8; ++index) { + Lardon3DSparseGeometryPoint3 point{ + 9.2 + 0.25 * index, -0.35 + 0.1 * (index % 4), + 4.0 + 0.3 * (index % 3)}; + const uint64_t base_track = 30 + index; + const uint64_t extension_track = 3000 + index; + fixture->landmarks.push_back( + {700 + index, base_track, 200, point, 0.0, 0.0, 2}); + for (uint32_t camera_index = 0; camera_index < 2; ++camera_index) { + const uint64_t image_id = 200 + camera_index; + const uint64_t feature_set = 10000 + image_id; + const auto pixel = project(10.0 + camera_index, point); + fixture->base_tracks.push_back({base_track, image_id, feature_set, index, + 64, pixel.x, pixel.y}); + fixture->extension_observations.push_back( + {extension_track, image_id, feature_set, index, 64, pixel.x, pixel.y}); + fixture->base_observations.push_back( + {700 + index, base_track, image_id, feature_set, index, camera_index}); + } + } + fixture->refresh(); +} + +bool test_component_isolation_and_bridge() { + Fixture fixture(150, true, false); + add_second_component(&fixture); + const auto camera_b0 = fixture.cameras[2]; + const auto landmark_b0 = fixture.landmarks[8]; + Lardon3DIncrementalReconstructionResult result{}; + CHECK(fixture.run(&result) == LARDON3D_INCREMENTAL_RECONSTRUCTION_OK); + CHECK(result.snapshot.component_count == 2 && + result.snapshot.components[0].component_key == 100 && + result.snapshot.components[1].component_key == 200); + auto camera = std::find_if(result.snapshot.cameras, + result.snapshot.cameras + result.snapshot.camera_count, + [](const auto &item) { return item.image_id == 200; }); + auto landmark = std::find_if( + result.snapshot.landmarks, + result.snapshot.landmarks + result.snapshot.landmark_count, + [](const auto &item) { return item.track_id == 3000; }); + CHECK(camera != result.snapshot.cameras + result.snapshot.camera_count && + landmark != result.snapshot.landmarks + result.snapshot.landmark_count); + CHECK(std::memcmp(&camera->pose_cw, &camera_b0.pose_cw, + sizeof(camera->pose_cw)) == 0); + CHECK(std::memcmp(&landmark->point, &landmark_b0.point, + sizeof(landmark->point)) == 0); + lardon3d_incremental_reconstruction_result_destroy(&result); + + Fixture bridge(150, true, false); + add_second_component(&bridge); + for (auto &observation : bridge.extension_observations) + if (observation.track_id == 3000) observation.track_id = 1000; + bridge.refresh(); + CHECK(bridge.run(&result) == + LARDON3D_INCREMENTAL_RECONSTRUCTION_CROSS_COMPONENT_BRIDGE); + return true; +} + +bool expect_invalid_base(Fixture *fixture) { + fixture->refresh(); + Lardon3DIncrementalReconstructionResult result{}; + CHECK(fixture->run(&result) == + LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_BASE); + CHECK(!result.changed && result.snapshot.components == nullptr && + result.snapshot.component_count == 0); + lardon3d_incremental_reconstruction_result_destroy(&result); + return true; +} + +bool test_base_observation_validation() { + Fixture wrong_landmark(150, false, false); + ++wrong_landmark.base_observations[0].landmark_id; + CHECK(expect_invalid_base(&wrong_landmark)); + + Fixture wrong_track(150, false, false); + ++wrong_track.base_observations[0].track_id; + CHECK(expect_invalid_base(&wrong_track)); + + Fixture duplicate_position(150, false, false); + duplicate_position.base_observations[1].position_in_track = 0; + CHECK(expect_invalid_base(&duplicate_position)); + + Fixture invalid_position(150, false, false); + invalid_position.base_observations[0].position_in_track = 2; + CHECK(expect_invalid_base(&invalid_position)); + + Fixture wrong_count(150, false, false); + ++wrong_count.landmarks[0].observation_count; + CHECK(expect_invalid_base(&wrong_count)); + + Fixture cross_component(150, false, false); + add_second_component(&cross_component); + cross_component.base_observations[0].image_id = 200; + CHECK(expect_invalid_base(&cross_component)); + + Fixture missing_source(150, false, false); + missing_source.base_tracks.pop_back(); + CHECK(expect_invalid_base(&missing_source)); + + Fixture extra_source(150, false, false); + extra_source.base_tracks.push_back(extra_source.base_tracks.front()); + extra_source.base_tracks.back().feature_index = 63; + CHECK(expect_invalid_base(&extra_source)); + + Fixture wrong_source_image(150, false, false); + wrong_source_image.base_tracks[0].image_id = 101; + CHECK(expect_invalid_base(&wrong_source_image)); + + Fixture wrong_source_feature(150, false, false); + ++wrong_source_feature.base_tracks[0].feature_index; + CHECK(expect_invalid_base(&wrong_source_feature)); + + Fixture duplicate_source(150, false, false); + duplicate_source.base_tracks.push_back(duplicate_source.base_tracks.front()); + CHECK(expect_invalid_base(&duplicate_source)); + return true; +} + +bool test_landmark_id_exhaustion() { + Fixture fixture(150, true, false); + fixture.images.push_back({151, calibration()}); + for (uint32_t index = 0; index < 8; ++index) { + const auto pixel = project(0.75, fixture.landmarks[index].point); + fixture.extension_observations.push_back( + {1000 + index, 151, 10151, index, 64, pixel.x, pixel.y}); + } + const Lardon3DSparseGeometryPoint3 new_point{0.2, -0.1, 5.5}; + const auto first = project(0.5, new_point); + const auto second = project(0.75, new_point); + fixture.extension_observations.push_back( + {9000, 150, 20150, 0, 1, first.x, first.y}); + fixture.extension_observations.push_back( + {9000, 151, 20151, 0, 1, second.x, second.y}); + fixture.landmarks.back().landmark_id = UINT64_MAX - 1; + for (auto &observation : fixture.base_observations) + if (observation.track_id == fixture.landmarks.back().track_id) + observation.landmark_id = UINT64_MAX - 1; + fixture.refresh(); + Lardon3DIncrementalReconstructionResult result{}; + CHECK(fixture.run(&result) == LARDON3D_INCREMENTAL_RECONSTRUCTION_INVALID_BASE); + CHECK(!result.changed && result.snapshot.components == nullptr); + lardon3d_incremental_reconstruction_result_destroy(&result); + return true; +} + +bool snapshots_equal(const Lardon3DSparseIncrementalResult &left, + const Lardon3DSparseIncrementalResult &right) { + return left.component_count == right.component_count && + left.camera_count == right.camera_count && + left.landmark_count == right.landmark_count && + left.observation_count == right.observation_count && + std::memcmp(left.components, right.components, + left.component_count * sizeof(*left.components)) == 0 && + std::memcmp(left.cameras, right.cameras, + left.camera_count * sizeof(*left.cameras)) == 0 && + std::memcmp(left.landmarks, right.landmarks, + left.landmark_count * sizeof(*left.landmarks)) == 0 && + std::memcmp(left.observations, right.observations, + left.observation_count * sizeof(*left.observations)) == 0; +} + +bool test_new_only_triangulation_and_determinism() { + Fixture fixture(150, true, false); + fixture.images.push_back({151, calibration()}); + for (uint32_t index = 0; index < 8; ++index) { + const auto pixel = project(0.75, fixture.landmarks[index].point); + fixture.extension_observations.push_back( + {1000 + index, 151, 10151, index, 64, pixel.x, pixel.y}); + } + const Lardon3DSparseGeometryPoint3 new_point{0.2, -0.1, 5.5}; + auto first = project(0.5, new_point); + auto second = project(0.75, new_point); + fixture.extension_observations.push_back( + {9000, 150, 20150, 0, 1, first.x, first.y}); + fixture.extension_observations.push_back( + {9000, 151, 20151, 0, 1, second.x, second.y}); + fixture.refresh(); + + Lardon3DIncrementalReconstructionResult first_result{}; + Lardon3DIncrementalReconstructionResult second_result{}; + CHECK(fixture.run(&first_result) == LARDON3D_INCREMENTAL_RECONSTRUCTION_OK); + CHECK(fixture.run(&second_result) == LARDON3D_INCREMENTAL_RECONSTRUCTION_OK); + CHECK(first_result.snapshot.camera_count == 4 && + first_result.snapshot.landmark_count == 9 && + first_result.snapshot.components[0].component_key == 100); + auto landmark = std::lower_bound( + first_result.snapshot.landmarks, + first_result.snapshot.landmarks + first_result.snapshot.landmark_count, + UINT64_C(9000), [](const auto &item, uint64_t track_id) { + return item.track_id < track_id; + }); + CHECK(landmark != first_result.snapshot.landmarks + + first_result.snapshot.landmark_count && + landmark->track_id == 9000 && landmark->component_key == 100 && + landmark->observation_count == 2); + CHECK(snapshots_equal(first_result.snapshot, second_result.snapshot)); + lardon3d_incremental_reconstruction_result_destroy(&first_result); + lardon3d_incremental_reconstruction_result_destroy(&second_result); + return true; +} + +bool test_bundle_adjustment_failure() { + Fixture fixture(150, true, false); + fixture.cameras[1].pose_cw = fixture.cameras[0].pose_cw; + fixture.refresh(); + Lardon3DIncrementalReconstructionResult result{}; + CHECK(fixture.run(&result) == + LARDON3D_INCREMENTAL_RECONSTRUCTION_BUNDLE_ADJUSTMENT_FAILED); + CHECK(!result.changed && result.snapshot.components == nullptr && + result.snapshot.component_count == 0); + lardon3d_incremental_reconstruction_result_destroy(&result); + return true; +} +} // namespace + +int main() { + if (!test_identity_and_estimate() || !test_lineage_failures() || + !test_h_b01() || !test_h_b02() || !test_cancellation() || + !test_successful_enrichment() || + !test_component_isolation_and_bridge() || + !test_base_observation_validation() || + !test_landmark_id_exhaustion() || + !test_new_only_triangulation_and_determinism() || + !test_bundle_adjustment_failure()) + return 1; + return 0; +} diff --git a/tests/test_match_result.c b/tests/test_match_result.c index 21377cf..c80cc7b 100644 --- a/tests/test_match_result.c +++ b/tests/test_match_result.c @@ -53,6 +53,8 @@ static bool create_v9_database(const char *path) { return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE IF EXISTS incremental_reconstruction_tasks;" + "DROP TABLE IF EXISTS incremental_reconstructions;" "DROP TABLE IF EXISTS sparse_sfm_tasks;" "DROP TABLE IF EXISTS sparse_landmark_observations;" "DROP TABLE IF EXISTS sparse_landmarks;" @@ -117,7 +119,7 @@ static bool run_test(void) { Lardon3DProjectDb *database = NULL; CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(database && lardon3d_project_db_schema_version(database) == 17); + CHECK(database && lardon3d_project_db_schema_version(database) == 18); Lardon3DProjectDbScanSet scanset; CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) == @@ -494,7 +496,7 @@ static bool run_test(void) { CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); /* Verify persistence: load previously created results */ CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) == @@ -537,11 +539,11 @@ static bool run_test(void) { v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 9)); CHECK(lardon3d_project_db_open(v9_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; CHECK(query_integer( - v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 17)); + v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 18)); CHECK( query_integer(v9_path, "SELECT count(*) FROM sqlite_master WHERE type='table' AND " @@ -569,7 +571,7 @@ static bool run_test(void) { 0)); CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 17); + lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; diff --git a/tests/test_matcher_task.c b/tests/test_matcher_task.c index 030e15e..deb70ca 100644 --- a/tests/test_matcher_task.c +++ b/tests/test_matcher_task.c @@ -89,6 +89,8 @@ static bool downgrade_project_to_historical_v10(const char *database_path) { } static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE IF EXISTS incremental_reconstruction_tasks;" + "DROP TABLE IF EXISTS incremental_reconstructions;" "DROP TABLE IF EXISTS sparse_sfm_tasks;" "DROP TABLE IF EXISTS sparse_landmark_observations;" "DROP TABLE IF EXISTS sparse_landmarks;" @@ -366,7 +368,7 @@ static bool run_test(void) { "name='matcher_tasks'", 0)); CHECK(reopen_runtime(&fixture)); - CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 17); + CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 18); CHECK( query_integer(database_path, "SELECT count(*) FROM sqlite_master WHERE type='table' AND " diff --git a/tests/test_project_db.c b/tests/test_project_db.c index 54dd1cf..e17156e 100644 --- a/tests/test_project_db.c +++ b/tests/test_project_db.c @@ -116,7 +116,7 @@ static bool create_future_database(const char *path) { sqlite3_exec( connection, "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" - "INSERT INTO metadata VALUES('schema_version',18);", + "INSERT INTO metadata VALUES('schema_version',19);", NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; } @@ -133,6 +133,8 @@ static bool create_v7_database(const char *path) { return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE incremental_reconstruction_tasks;" + "DROP TABLE incremental_reconstructions;" "DROP TABLE sparse_sfm_tasks;" "DROP TABLE sparse_landmark_observations;" "DROP TABLE sparse_landmarks;" @@ -170,6 +172,8 @@ static bool create_v6_database(const char *path) { return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE incremental_reconstruction_tasks;" + "DROP TABLE incremental_reconstructions;" "DROP TABLE sparse_sfm_tasks;" "DROP TABLE sparse_landmark_observations;" "DROP TABLE sparse_landmarks;" @@ -215,6 +219,8 @@ static bool create_v10_database(const char *path) { } static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE incremental_reconstruction_tasks;" + "DROP TABLE incremental_reconstructions;" "DROP TABLE sparse_sfm_tasks;" "DROP TABLE sparse_landmark_observations;" "DROP TABLE sparse_landmarks;" @@ -249,6 +255,8 @@ static bool create_v13_database(const char *path) { return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE incremental_reconstruction_tasks;" + "DROP TABLE incremental_reconstructions;" "DROP TABLE sparse_sfm_tasks;" "DROP TABLE sparse_landmark_observations;" "DROP TABLE sparse_landmarks;" @@ -870,7 +878,8 @@ static bool run_test(void) { char direct_v5_path[512], v8_path[512], failed_v8_path[512]; char v10_path[512], failed_v11_path[512]; char v13_path[512], true_v14_path[512], failed_v14_path[512], - failed_v15_path[512], true_v15_path[512]; + failed_v15_path[512], true_v15_path[512], v17_path[512], + failed_v18_path[512]; CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0); CHECK(snprintf(artifact_path, sizeof(artifact_path), "%s/artifact.bin", @@ -913,12 +922,15 @@ static bool run_test(void) { directory) > 0); CHECK(snprintf(true_v15_path, sizeof(true_v15_path), "%s/true-v15.db", directory) > 0); + CHECK(snprintf(v17_path, sizeof(v17_path), "%s/v17.db", directory) > 0); + CHECK(snprintf(failed_v18_path, sizeof(failed_v18_path), + "%s/failed-v18.db", directory) > 0); char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; Lardon3DProjectDb *database = NULL; CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(database && lardon3d_project_db_schema_version(database) == 17); + CHECK(database && lardon3d_project_db_schema_version(database) == 18); bool legacy_pending = true; CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) == LARDON3D_PROJECT_DB_OK && @@ -1236,9 +1248,69 @@ static bool run_test(void) { database = NULL; CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", - 17)); + 18)); CHECK(query_integer(database_path, "SELECT count(*) FROM tasks WHERE task_id=1", 1)); + + CHECK(lardon3d_project_db_open(v17_path, &database, error) == + LARDON3D_PROJECT_DB_OK); + lardon3d_project_db_close(database); + database = NULL; + CHECK(execute_test_sql( + v17_path, + "INSERT INTO track_sets(track_set_id,builder_kind,builder_version," + "parameter_fingerprint,verifier_kind,verifier_version,verifier_fingerprint," + "input_scope_hash,gvr_count,track_count,created_at) VALUES(1,'builder',1," + "zeroblob(32),1,1,zeroblob(32),zeroblob(32),1,0,1);" + "INSERT INTO sparse_calibration_scopes(scope_id,scientific_hash,member_count) " + "VALUES(1,randomblob(32),1);" + "INSERT INTO sparse_reconstructions(reconstruction_id,track_set_id," + "calibration_scope_id,sfm_kind,sfm_version,parameter_fingerprint,component_count," + "registered_image_count,landmark_count,reprojection_rmse_px,reprojection_median_px," + "created_at) VALUES(1,1,1,1,1,zeroblob(32),1,2,1,0,0,1);" + "DROP TABLE incremental_reconstruction_tasks;" + "DROP TABLE incremental_reconstructions;" + "UPDATE metadata SET value=17 WHERE key='schema_version';")); + CHECK(lardon3d_project_db_open(v17_path, &database, error) == + LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_schema_version(database) == 18); + lardon3d_project_db_close(database); + database = NULL; + CHECK(query_integer(v17_path, + "SELECT count(*) FROM sparse_reconstructions WHERE " + "reconstruction_id=1 AND derivation_identity IS NULL " + "AND parameter_fingerprint=zeroblob(32)", 1)); + CHECK(query_integer(v17_path, "SELECT count(*) FROM pragma_foreign_key_check", 0)); + CHECK(query_integer(v17_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name IN('incremental_reconstructions'," + "'incremental_reconstruction_tasks')", 2)); + + CHECK(lardon3d_project_db_open(failed_v18_path, &database, error) == + LARDON3D_PROJECT_DB_OK); + lardon3d_project_db_close(database); + database = NULL; + CHECK(execute_test_sql( + failed_v18_path, + "DROP TABLE incremental_reconstruction_tasks;" + "DROP TABLE incremental_reconstructions;" + "UPDATE metadata SET value=17 WHERE key='schema_version';")); + CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V18", "1", 1) == 0); + CHECK(lardon3d_project_db_open(failed_v18_path, &database, error) == + LARDON3D_PROJECT_DB_IO_ERROR); + CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V18") == 0); + CHECK(query_integer(failed_v18_path, + "SELECT value FROM metadata WHERE key='schema_version'", 17)); + CHECK(query_integer(failed_v18_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='incremental_reconstructions'", 0)); + CHECK(query_integer(failed_v18_path, + "SELECT count(*) FROM pragma_foreign_key_check", 0)); + CHECK(lardon3d_project_db_open(failed_v18_path, &database, error) == + LARDON3D_PROJECT_DB_OK); + lardon3d_project_db_close(database); + database = NULL; + CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_load_task(database, 1, &task) == @@ -1264,7 +1336,7 @@ static bool run_test(void) { CHECK(create_v1_database(legacy_path)); CHECK(lardon3d_project_db_open(legacy_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK && strcmp(loaded_project.stable_id, "legacy-project") == 0); @@ -1279,7 +1351,7 @@ static bool run_test(void) { database = NULL; CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", - 17)); + 18)); CHECK(create_v1_database(failed_migration_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0); @@ -1310,7 +1382,7 @@ static bool run_test(void) { lardon3d_project_db_close(database); database = NULL; CHECK(query_integer( - v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 17)); + v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 18)); CHECK(create_v2_database(failed_v3_migration_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0); @@ -1344,7 +1416,7 @@ static bool run_test(void) { lardon3d_project_db_close(database); database = NULL; CHECK(query_integer( - v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 17)); + v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 18)); CHECK(create_v3_database(failed_v4_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0); @@ -1366,7 +1438,7 @@ static bool run_test(void) { fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error); } CHECK(v4_result == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", @@ -1421,7 +1493,7 @@ static bool run_test(void) { error); } CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 17); + lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; @@ -1431,7 +1503,7 @@ static bool run_test(void) { 5)); CHECK(lardon3d_project_db_open(direct_v5_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 17); + lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; @@ -1440,11 +1512,11 @@ static bool run_test(void) { v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7)); CHECK(lardon3d_project_db_open(v8_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; CHECK(query_integer( - v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 17)); + v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 18)); CHECK(create_v7_database(failed_v8_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0); @@ -1461,7 +1533,7 @@ static bool run_test(void) { 0)); CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 17); + lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; @@ -1480,11 +1552,11 @@ static bool run_test(void) { 0)); CHECK(lardon3d_project_db_open(v10_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 17); + lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; CHECK(query_integer( - v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 17)); + v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 18)); CHECK( query_integer(v10_path, "SELECT count(*) FROM sqlite_master WHERE type='table' AND " @@ -1511,7 +1583,7 @@ static bool run_test(void) { 1)); CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; @@ -1525,11 +1597,11 @@ static bool run_test(void) { 0)); CHECK(lardon3d_project_db_open(v13_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 17); + lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; CHECK(query_integer( - v13_path, "SELECT value FROM metadata WHERE key='schema_version'", 17)); + v13_path, "SELECT value FROM metadata WHERE key='schema_version'", 18)); CHECK( query_integer(v13_path, "SELECT count(*) FROM sqlite_master WHERE type='table' AND " @@ -1552,7 +1624,7 @@ static bool run_test(void) { true_v14_path); } CHECK(true_v14_result == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 17); + lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; CHECK( @@ -1667,7 +1739,7 @@ static bool run_test(void) { if (true_v15_result != LARDON3D_PROJECT_DB_OK) fprintf(stderr, "true v15 upgrade: %d %s\n", true_v15_result, error); CHECK(true_v15_result == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); Lardon3DProjectDbProject migrated_project; CHECK(lardon3d_project_db_get_project(database, &migrated_project) == LARDON3D_PROJECT_DB_OK && @@ -1699,7 +1771,7 @@ static bool run_test(void) { database = NULL; CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; CHECK(schema_compare(database_path, true_v15_path, true)); @@ -1734,7 +1806,7 @@ static bool run_test(void) { 0)); CHECK(lardon3d_project_db_open(true_v15_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; @@ -1753,7 +1825,7 @@ static bool run_test(void) { 0)); CHECK(lardon3d_project_db_open(failed_v14_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 17); + lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; @@ -1772,7 +1844,7 @@ static bool run_test(void) { 0)); CHECK(lardon3d_project_db_open(failed_v15_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 17); + lardon3d_project_db_schema_version(database) == 18); lardon3d_project_db_close(database); database = NULL; @@ -2241,6 +2313,8 @@ static bool run_test(void) { CHECK(unlink(true_v14_path) == 0); CHECK(unlink(true_v15_path) == 0); CHECK(unlink(failed_v15_path) == 0); + CHECK(unlink(v17_path) == 0); + CHECK(unlink(failed_v18_path) == 0); CHECK(rmdir(directory) == 0); return true; } diff --git a/tests/test_sparse_sfm_model.c b/tests/test_sparse_sfm_model.c index aa43175..f6bf832 100644 --- a/tests/test_sparse_sfm_model.c +++ b/tests/test_sparse_sfm_model.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -17,6 +18,37 @@ } \ } while (0) +static void test_put_u32(unsigned char *record, size_t *offset, + uint32_t value) { + for (unsigned byte = 0; byte < 4; ++byte) + record[(*offset)++] = (unsigned char)(value >> (byte * 8)); +} + +static void test_put_u64(unsigned char *record, size_t *offset, + uint64_t value) { + for (unsigned byte = 0; byte < 8; ++byte) + record[(*offset)++] = (unsigned char)(value >> (byte * 8)); +} + +static bool test_h_identity(Lardon3DIncrementalReconstructionMetadata *metadata) { + unsigned char record[76] = {0}; + unsigned int size = 0; + size_t offset = 8; + memcpy(record, "L3DHIDV1", 8); + test_put_u32(record, &offset, 1); + test_put_u64(record, &offset, metadata->base_reconstruction_id); + test_put_u64(record, &offset, metadata->extension_track_set_id); + test_put_u64(record, &offset, metadata->calibration_scope_id); + test_put_u32(record, &offset, metadata->incremental_kind); + test_put_u32(record, &offset, metadata->incremental_version); + memcpy(record + offset, metadata->parameter_fingerprint, 32); + offset += 32; + return offset == sizeof(record) && + EVP_Digest(record, sizeof(record), metadata->scientific_identity, + &size, EVP_sha256(), NULL) == 1 && + size == sizeof(metadata->scientific_identity); +} + static int raw_exec(const char *path, const char *sql) { sqlite3 *connection = NULL; char *message = NULL; @@ -91,7 +123,7 @@ static bool run_test(void) { Lardon3DProjectDb *db = NULL; char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(db) == 17); + CHECK(lardon3d_project_db_schema_version(db) == 18); Lardon3DProjectDbScanSet scanset; CHECK(lardon3d_project_db_create_scanset(db, "Sparse model", &scanset) == @@ -305,6 +337,92 @@ static bool run_test(void) { Lardon3DSparseReconstruction reconstruction; CHECK(lardon3d_sparse_reconstruction_publish( db, &publication, &reconstruction) == LARDON3D_PROJECT_DB_OK); + CHECK(!reconstruction.has_derivation_identity && + reconstruction.parameter_fingerprint[0] == 9); + Lardon3DIncrementalReconstructionMetadata h_metadata = { + .base_reconstruction_id = reconstruction.reconstruction_id, + .extension_track_set_id = published_tracks.track_set_id, + .calibration_scope_id = scope.scope_id, + .incremental_kind = 1, + .incremental_version = 1, + }; + h_metadata.parameter_fingerprint[0] = 44; + CHECK(test_h_identity(&h_metadata)); + memcpy(publication.parameter_fingerprint, h_metadata.parameter_fingerprint, 32); + Lardon3DSparseReconstruction h_reconstruction; + CHECK(lardon3d_incremental_reconstruction_publish( + db, &publication, &h_metadata, &h_reconstruction) == + LARDON3D_PROJECT_DB_OK); + Lardon3DIncrementalReconstructionMetadata loaded_h_metadata; + Lardon3DSparseReconstruction loaded_h_reconstruction; + CHECK(lardon3d_incremental_reconstruction_find_exact( + db, h_metadata.scientific_identity, &loaded_h_metadata, + &loaded_h_reconstruction) == LARDON3D_PROJECT_DB_OK); + CHECK(loaded_h_metadata.reconstruction_id == h_reconstruction.reconstruction_id); + CHECK(loaded_h_metadata.base_reconstruction_id == reconstruction.reconstruction_id); + CHECK(loaded_h_reconstruction.reconstruction_id == h_reconstruction.reconstruction_id); + CHECK(loaded_h_reconstruction.has_derivation_identity && + memcmp(loaded_h_reconstruction.derivation_identity, + h_metadata.scientific_identity, 32) == 0 && + memcmp(loaded_h_reconstruction.parameter_fingerprint, + h_metadata.parameter_fingerprint, 32) == 0); + CHECK(memcmp(loaded_h_metadata.parameter_fingerprint, + h_metadata.parameter_fingerprint, 32) == 0); + CHECK(memcmp(loaded_h_metadata.scientific_identity, + h_metadata.scientific_identity, 32) == 0); + Lardon3DSparseReconstruction duplicate_h; + Lardon3DSparsePublication mismatched_h_publication = publication; + mismatched_h_publication.parameter_fingerprint[0] ^= 0x80; + CHECK(lardon3d_incremental_reconstruction_publish( + db, &mismatched_h_publication, &h_metadata, &duplicate_h) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + CHECK(raw_scalar_equals(path, "SELECT COUNT(*) FROM sparse_reconstructions", 2)); + CHECK(raw_scalar_equals(path, "SELECT COUNT(*) FROM incremental_reconstructions", 1)); + CHECK(lardon3d_incremental_reconstruction_publish( + db, &publication, &h_metadata, &duplicate_h) == + LARDON3D_PROJECT_DB_CONSTRAINT); + + publication.parameter_fingerprint[0] = 10; + Lardon3DSparseReconstruction second_base; + CHECK(lardon3d_sparse_reconstruction_publish(db, &publication, &second_base) == + LARDON3D_PROJECT_DB_OK); + CHECK(!second_base.has_derivation_identity && + second_base.parameter_fingerprint[0] == 10); + Lardon3DIncrementalReconstructionMetadata second_h_metadata = h_metadata; + second_h_metadata.base_reconstruction_id = second_base.reconstruction_id; + CHECK(test_h_identity(&second_h_metadata)); + memcpy(publication.parameter_fingerprint, + second_h_metadata.parameter_fingerprint, 32); + Lardon3DSparseReconstruction second_h; + CHECK(lardon3d_incremental_reconstruction_publish( + db, &publication, &second_h_metadata, &second_h) == + LARDON3D_PROJECT_DB_OK); + CHECK(second_h.reconstruction_id != h_reconstruction.reconstruction_id && + second_h.has_derivation_identity && + memcmp(second_h.parameter_fingerprint, + h_reconstruction.parameter_fingerprint, 32) == 0 && + memcmp(second_h.derivation_identity, + h_reconstruction.derivation_identity, 32) != 0); + unsigned char valid_identity[32]; + memcpy(valid_identity, h_metadata.scientific_identity, 32); + h_metadata.scientific_identity[0] ^= 0x80; + CHECK(lardon3d_incremental_reconstruction_publish( + db, &publication, &h_metadata, &loaded_h_reconstruction) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + memcpy(h_metadata.scientific_identity, valid_identity, 32); + h_metadata.parameter_fingerprint[0] ^= 0x40; + CHECK(lardon3d_incremental_reconstruction_publish( + db, &publication, &h_metadata, &loaded_h_reconstruction) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + h_metadata.parameter_fingerprint[0] ^= 0x40; + publication.parameter_fingerprint[0] = h_metadata.parameter_fingerprint[0]; + h_metadata.base_reconstruction_id = UINT64_C(999999); + CHECK(test_h_identity(&h_metadata)); + CHECK(lardon3d_incremental_reconstruction_publish( + db, &publication, &h_metadata, &loaded_h_reconstruction) == + LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(raw_scalar_equals(path, "SELECT COUNT(*) FROM sparse_reconstructions", 4)); + publication.parameter_fingerprint[0] = 9; Lardon3DSparseLandmarkPage landmark_page = {0}; Lardon3DSparseLandmark landmark_items[1]; landmark_page.items = landmark_items; diff --git a/tests/test_sparse_sfm_payload.cpp b/tests/test_sparse_sfm_payload.cpp index 203bb7c..5db635c 100644 --- a/tests/test_sparse_sfm_payload.cpp +++ b/tests/test_sparse_sfm_payload.cpp @@ -7,6 +7,7 @@ extern "C" { #include +#include #include } @@ -65,7 +66,7 @@ int main() { Lardon3DProjectDb *database = nullptr; char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{}; CHECK(lardon3d_project_db_open(path.c_str(), &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(lardon3d_project_db_schema_version(database) == 17); + CHECK(lardon3d_project_db_schema_version(database) == 18); sqlite3 *raw = nullptr; CHECK(sqlite3_open(path.c_str(), &raw) == SQLITE_OK); @@ -74,7 +75,11 @@ int main() { "verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,track_count," "created_at) VALUES(41,'track_builder',1,zeroblob(32),1,1,zeroblob(32),zeroblob(32),1,0,0);" "INSERT INTO sparse_calibration_scopes(scope_id,scientific_hash,member_count) " - "VALUES(43,randomblob(32),1);"; + "VALUES(43,randomblob(32),1);" + "INSERT INTO sparse_reconstructions(reconstruction_id,track_set_id," + "calibration_scope_id,sfm_kind,sfm_version,parameter_fingerprint,component_count," + "registered_image_count,landmark_count,reprojection_rmse_px,reprojection_median_px," + "created_at) VALUES(42,41,43,1,1,zeroblob(32),1,2,1,0,0,0);"; CHECK(sqlite3_exec(raw, fixture_sql, nullptr, nullptr, nullptr) == SQLITE_OK); sqlite3_close(raw); @@ -135,6 +140,30 @@ int main() { sqlite3_close(raw); CHECK(lardon3d_project_db_load_sparse_sfm_task(database, 101, &corrupt) == LARDON3D_PROJECT_DB_NOT_FOUND); + + Lardon3DTaskDurableSnapshot h_snapshot{}; + h_snapshot.id = 200; + std::snprintf(h_snapshot.name, sizeof(h_snapshot.name), "Phase H payload"); + h_snapshot.saved_state = TASK_PENDING; + h_snapshot.recovery_state = TASK_PENDING; + h_snapshot.estimate = {269484032, 0, 0, 0, 1, 1, 1, 0, 1, + LARDON3D_RESOURCE_TASK_CPU}; + Lardon3DProjectDbIncrementalReconstructionTask h_payload{ + 200, 42, 41, 43, 1, 1, {}}; + h_payload.parameter_fingerprint[0] = 77; + CHECK(lardon3d_project_db_record_incremental_reconstruction_task( + database, &h_snapshot, LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_KIND, + LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_KIND_VERSION, nullptr, + &h_payload, 0) == LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbIncrementalReconstructionTask loaded_h{}; + CHECK(lardon3d_project_db_load_incremental_reconstruction_task( + database, 200, &loaded_h) == LARDON3D_PROJECT_DB_OK); + CHECK(loaded_h.task_id == 200 && loaded_h.base_reconstruction_id == 42 && + loaded_h.extension_track_set_id == 41 && + loaded_h.calibration_scope_id == 43 && loaded_h.incremental_kind == 1 && + loaded_h.incremental_version == 1 && + std::memcmp(loaded_h.parameter_fingerprint, + h_payload.parameter_fingerprint, 32) == 0); lardon3d_project_db_close(database); CHECK(unlink(path.c_str()) == 0); CHECK(rmdir(directory) == 0); diff --git a/tests/test_sparse_sfm_task.cpp b/tests/test_sparse_sfm_task.cpp index 2a17862..e7f9616 100644 --- a/tests/test_sparse_sfm_task.cpp +++ b/tests/test_sparse_sfm_task.cpp @@ -1,6 +1,8 @@ +#include #include #include #include +#include #include #include #include @@ -10,6 +12,8 @@ extern "C" { #include #include +#include +#include #include #include #include @@ -38,7 +42,8 @@ namespace { } \ } while (0) -constexpr size_t kImageCount = 3; +constexpr size_t kBaseImageCount = 3; +constexpr size_t kImageCount = 4; constexpr size_t kTrackCount = 12; struct Fixture { @@ -48,6 +53,7 @@ struct Fixture { Lardon3DProjectDbScanSet scanset{}; Lardon3DProjectDbImage images[kImageCount]{}; Lardon3DProjectDbFeatureSet feature_sets[kImageCount]{}; + Lardon3DProjectDbTrackSet base_track_set{}; Lardon3DProjectDbTrackSet track_set{}; Lardon3DSparseCalibrationScope scope{}; Lardon3DSparseIncrementalParameters parameters{}; @@ -58,12 +64,34 @@ Lardon3DResourcePolicy policy() { } bool wait_state(Lardon3DTaskQueue *queue, uint64_t id, Lardon3DTaskState wanted) { - for (size_t attempt = 0; attempt < 4000000; ++attempt) { + timespec start{}; + if (clock_gettime(CLOCK_MONOTONIC, &start) != 0) return false; + const int64_t deadline = start.tv_sec * INT64_C(1000000000) + start.tv_nsec + + INT64_C(10000000000); + for (;;) { Lardon3DTaskSnapshot snapshot{}; if (lardon3d_task_queue_get(queue, id, &snapshot) && snapshot.state == wanted) return true; + timespec now{}; + if (clock_gettime(CLOCK_MONOTONIC, &now) != 0 || + now.tv_sec * INT64_C(1000000000) + now.tv_nsec >= deadline) + return false; sched_yield(); } - return false; +} + +bool wait_reservations_released(Lardon3DResourceGovernor *governor) { + timespec start{}; + if (clock_gettime(CLOCK_MONOTONIC, &start) != 0) return false; + const int64_t deadline = start.tv_sec * INT64_C(1000000000) + start.tv_nsec + + INT64_C(10000000000); + while (lardon3d_resource_governor_reservation_count(governor) != 0) { + timespec now{}; + if (clock_gettime(CLOCK_MONOTONIC, &now) != 0 || + now.tv_sec * INT64_C(1000000000) + now.tv_nsec >= deadline) + return false; + sched_yield(); + } + return true; } Lardon3DSparseGeometryPoint2 project(const Lardon3DSparseGeometryCalibration &calibration, @@ -144,6 +172,8 @@ bool create_fixture(Fixture *fixture) { auto pixel = project(geometry, camera, points[track]); keypoints[track].x = static_cast(pixel.x); keypoints[track].y = static_cast(pixel.y); + if (camera == kImageCount - 1 && track == 0) + keypoints[track].x += 0.25F; keypoints[track].size = 1.0F; std::memset(descriptors.data() + track * 32, static_cast(track + camera), 32); } @@ -202,6 +232,23 @@ bool create_fixture(Fixture *fixture) { CHECK(lardon3d_project_db_create_track_set(fixture->state.project_db, &configuration, tracks.data(), tracks.size(), &fixture->track_set) == LARDON3D_PROJECT_DB_OK); + std::vector base_observations( + kTrackCount * kBaseImageCount); + std::vector base_tracks(kTrackCount); + for (size_t track = 0; track < kTrackCount; ++track) { + base_tracks[track].observation_count = kBaseImageCount; + base_tracks[track].observations = + base_observations.data() + track * kBaseImageCount; + for (size_t camera = 0; camera < kBaseImageCount; ++camera) + base_tracks[track].observations[camera] = { + fixture->feature_sets[camera].feature_set_id, + static_cast(track), static_cast(camera)}; + } + configuration.parameter_fingerprint[0] = 1; + CHECK(lardon3d_project_db_create_track_set( + fixture->state.project_db, &configuration, base_tracks.data(), + base_tracks.size(), &fixture->base_track_set) == + LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_sparse_incremental_parameters_default(&fixture->parameters)); fixture->parameters.minimum_seed_tracks = 6; fixture->parameters.minimum_seed_landmarks = 6; @@ -219,7 +266,88 @@ void destroy_runtime(Fixture *fixture) { } Lardon3DSparseSfmTaskConfiguration configuration(Fixture *fixture) { - return {fixture->track_set.track_set_id, fixture->scope.scope_id, fixture->parameters}; + return {fixture->base_track_set.track_set_id, fixture->scope.scope_id, + fixture->parameters}; +} + +bool recompute_metrics(Fixture *fixture, + const Lardon3DSparseReconstruction &reconstruction, + double *rmse, double *median) { + std::map cameras; + std::map landmarks; + Lardon3DSparseRegisteredImage camera_items[64]{}; + Lardon3DSparseRegisteredImagePage camera_page{0, 64, 0, 0, camera_items}; + CHECK(lardon3d_sparse_registered_image_list( + fixture->state.project_db, reconstruction.reconstruction_id, 0, + 64, &camera_page) == LARDON3D_PROJECT_DB_OK); + for (size_t index = 0; index < camera_page.count; ++index) + cameras.emplace(camera_items[index].image_id, camera_items[index]); + Lardon3DSparseLandmark landmark_items[64]{}; + Lardon3DSparseLandmarkPage landmark_page{0, 64, 0, 0, landmark_items}; + CHECK(lardon3d_sparse_landmark_list( + fixture->state.project_db, reconstruction.reconstruction_id, 0, + 64, &landmark_page) == LARDON3D_PROJECT_DB_OK); + for (size_t index = 0; index < landmark_page.count; ++index) + landmarks.emplace(landmark_items[index].track_id, landmark_items[index]); + Lardon3DSparseLandmarkObservation observation_items[64]{}; + Lardon3DSparseObservationPage observation_page{0, 0, 64, 0, 0, 0, + observation_items}; + CHECK(lardon3d_sparse_observation_list( + fixture->state.project_db, reconstruction.reconstruction_id, 0, 0, + 64, &observation_page) == LARDON3D_PROJECT_DB_OK); + std::vector errors; + double sum = 0.0; + const auto geometry = Lardon3DSparseGeometryCalibration{ + 4000, 3000, 2000.0, 2000.0, 2000.0, 1500.0, 0.0, 0.0, 0.0, 0.0}; + for (size_t index = 0; index < observation_page.count; ++index) { + const auto &observation = observation_items[index]; + auto landmark = landmarks.find(observation.track_id); + const Lardon3DProjectDbFeatureSet *feature_set = nullptr; + size_t camera_index = 0; + for (; camera_index < kImageCount; ++camera_index) + if (fixture->feature_sets[camera_index].feature_set_id == + observation.feature_set_id) { + feature_set = &fixture->feature_sets[camera_index]; + break; + } + CHECK(landmark != landmarks.end() && feature_set != nullptr); + auto camera = cameras.find(fixture->images[camera_index].image_id); + CHECK(camera != cameras.end()); + Lardon3DFeatureReader *reader = nullptr; + Lardon3DFeatureFileMetadata metadata{}; + CHECK(lardon3d_feature_reader_open(fixture->root.c_str(), feature_set, + &reader, &metadata) == + LARDON3D_FEATURE_STORE_OK); + Lardon3DFeatureKeypoint keypoint{}; + const auto read_status = lardon3d_feature_reader_keypoints( + reader, observation.feature_index, &keypoint, 1); + lardon3d_feature_reader_close(reader); + CHECK(read_status == LARDON3D_FEATURE_STORE_OK); + const double *r = camera->second.rotation_cw; + const double *t = camera->second.translation_cw; + const double x = r[0] * landmark->second.x + r[1] * landmark->second.y + + r[2] * landmark->second.z + t[0]; + const double y = r[3] * landmark->second.x + r[4] * landmark->second.y + + r[5] * landmark->second.z + t[1]; + const double z = r[6] * landmark->second.x + r[7] * landmark->second.y + + r[8] * landmark->second.z + t[2]; + CHECK(std::isfinite(z) && z > 1e-9); + const double dx = geometry.fx * x / z + geometry.cx - keypoint.x; + const double dy = geometry.fy * y / z + geometry.cy - keypoint.y; + const double squared = dx * dx + dy * dy; + CHECK(std::isfinite(squared) && std::isfinite(sum + squared)); + sum += squared; + errors.push_back(std::sqrt(squared)); + } + CHECK(!errors.empty()); + std::sort(errors.begin(), errors.end()); + *rmse = std::sqrt(sum / static_cast(errors.size())); + const size_t middle = errors.size() / 2; + *median = errors.size() % 2 + ? errors[middle] + : errors[middle - 1] + + (errors[middle] - errors[middle - 1]) / 2.0; + return std::isfinite(*rmse) && std::isfinite(*median); } bool test_lookup() { @@ -296,7 +424,8 @@ bool test_task() { CHECK(lardon3d_sparse_sfm_parameter_fingerprint(&fixture.parameters, fingerprint)); Lardon3DSparseReconstruction reconstruction{}; CHECK(lardon3d_sparse_reconstruction_find_exact( - fixture.state.project_db, fixture.track_set.track_set_id, fixture.scope.scope_id, 1, 1, + fixture.state.project_db, fixture.base_track_set.track_set_id, + fixture.scope.scope_id, 1, 1, fingerprint, &reconstruction) == LARDON3D_PROJECT_DB_OK); CHECK(reconstruction.component_count == 1 && reconstruction.registered_image_count == 3 && reconstruction.landmark_count >= 6 && @@ -310,12 +439,150 @@ bool test_task() { &observation_page) == LARDON3D_PROJECT_DB_OK); CHECK(observation_page.count >= 18); + Lardon3DIncrementalReconstructionTaskConfiguration h_configuration{ + reconstruction.reconstruction_id, fixture.track_set.track_set_id, + fixture.scope.scope_id}; + uint64_t h_task_id = 0; + Lardon3DTask *h_task = lardon3d_project_create_incremental_reconstruction_task( + &fixture.state, &h_configuration, &h_task_id); + CHECK(h_task && h_task_id != 0); + CHECK(lardon3d_task_kind(h_task, kind, &kind_version)); + CHECK(std::strcmp(kind, LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_KIND) == 0 && + kind_version == 1); + Lardon3DResourceEstimate h_estimate{}; + CHECK(lardon3d_task_resource_estimate(h_task, &h_estimate)); + CHECK(h_estimate.minimum_batch_size == 1 && h_estimate.maximum_batch_size == 1 && + h_estimate.desired_cpu_threads == 1 && h_estimate.desired_gpu_slots == 0 && + h_estimate.desired_io_slots == 1 && + h_estimate.task_class == LARDON3D_RESOURCE_TASK_CPU); + Lardon3DProjectDbIncrementalReconstructionTask h_payload{}; + CHECK(lardon3d_project_db_load_incremental_reconstruction_task( + fixture.state.project_db, h_task_id, &h_payload) == LARDON3D_PROJECT_DB_OK); + unsigned char h_fingerprint[32]{}; + CHECK(lardon3d_incremental_reconstruction_parameter_fingerprint(h_fingerprint)); + CHECK(std::memcmp(h_payload.parameter_fingerprint, h_fingerprint, 32) == 0); + CHECK(lardon3d_task_queue_add(fixture.state.task_queue, h_task, nullptr)); + CHECK(wait_state(fixture.state.task_queue, h_task_id, TASK_COMPLETED)); + CHECK(wait_reservations_released(fixture.state.resource_governor)); + Lardon3DIncrementalReconstructionIdentity h_identity{ + reconstruction.reconstruction_id, fixture.track_set.track_set_id, + fixture.scope.scope_id, 1, 1, {}}; + std::memcpy(h_identity.parameter_fingerprint, h_fingerprint, 32); + unsigned char h_digest[32]{}; + CHECK(lardon3d_incremental_reconstruction_identity_digest(&h_identity, + h_digest)); + Lardon3DIncrementalReconstructionMetadata h_metadata{}; + Lardon3DSparseReconstruction h_reconstruction{}; + CHECK(lardon3d_incremental_reconstruction_find_exact( + fixture.state.project_db, h_digest, &h_metadata, + &h_reconstruction) == LARDON3D_PROJECT_DB_OK); + CHECK(h_metadata.base_reconstruction_id == reconstruction.reconstruction_id); + CHECK(h_reconstruction.reconstruction_id != reconstruction.reconstruction_id && + h_reconstruction.registered_image_count == kImageCount && + h_reconstruction.landmark_count == reconstruction.landmark_count && + h_reconstruction.reprojection_rmse_px > 0.0 && + h_reconstruction.reprojection_median_px >= 0.0); + double expected_h_rmse = 0.0; + double expected_h_median = 0.0; + CHECK(recompute_metrics(&fixture, h_reconstruction, &expected_h_rmse, + &expected_h_median)); + CHECK(std::abs(h_reconstruction.reprojection_rmse_px - expected_h_rmse) < + 1e-12 && + std::abs(h_reconstruction.reprojection_median_px - expected_h_median) < + 1e-12); + CHECK(std::abs(h_reconstruction.reprojection_rmse_px - + reconstruction.reprojection_rmse_px) > 1e-12); + + Lardon3DSparseCalibration incompatible_calibration{}; + incompatible_calibration.model_kind = + LARDON3D_SPARSE_SFM_CALIBRATION_KIND_PINHOLE; + incompatible_calibration.model_version = + LARDON3D_SPARSE_SFM_CALIBRATION_VERSION; + incompatible_calibration.width = 4000; + incompatible_calibration.height = 3000; + incompatible_calibration.fx = 2100.0; + incompatible_calibration.fy = 2100.0; + incompatible_calibration.cx = 2000.0; + incompatible_calibration.cy = 1500.0; + incompatible_calibration.provenance_kind = + LARDON3D_SPARSE_SFM_PROVENANCE_USER_EXPLICIT; + Lardon3DSparseCalibration incompatible_stored{}; + CHECK(lardon3d_sparse_calibration_create( + fixture.state.project_db, &incompatible_calibration, + &incompatible_stored) == LARDON3D_PROJECT_DB_OK); + Lardon3DSparseCalibrationMember incompatible_members[kImageCount]{}; + for (size_t index = 0; index < kImageCount; ++index) { + incompatible_members[index].image_id = fixture.images[index].image_id; + incompatible_members[index].calibration_id = incompatible_stored.calibration_id; + std::memcpy(incompatible_members[index].calibration_hash, + incompatible_stored.scientific_hash, 32); + } + Lardon3DSparseCalibrationScope incompatible_scope{}; + CHECK(lardon3d_sparse_calibration_scope_create( + fixture.state.project_db, incompatible_members, kImageCount, + &incompatible_scope) == LARDON3D_PROJECT_DB_OK); + Lardon3DIncrementalReconstructionTaskConfiguration incompatible_h{ + reconstruction.reconstruction_id, fixture.track_set.track_set_id, + incompatible_scope.scope_id}; + uint64_t incompatible_task_id = 0; + CHECK(lardon3d_project_enqueue_incremental_reconstruction_task( + &fixture.state, &incompatible_h, &incompatible_task_id)); + CHECK(wait_state(fixture.state.task_queue, incompatible_task_id, TASK_FAILED)); + CHECK(wait_reservations_released(fixture.state.resource_governor)); + Lardon3DIncrementalReconstructionIdentity incompatible_identity{ + reconstruction.reconstruction_id, fixture.track_set.track_set_id, + incompatible_scope.scope_id, 1, 1, {}}; + std::memcpy(incompatible_identity.parameter_fingerprint, h_fingerprint, 32); + unsigned char incompatible_digest[32]{}; + CHECK(lardon3d_incremental_reconstruction_identity_digest( + &incompatible_identity, incompatible_digest)); + Lardon3DIncrementalReconstructionMetadata missing_metadata{}; + Lardon3DSparseReconstruction missing_reconstruction{}; + CHECK(lardon3d_incremental_reconstruction_find_exact( + fixture.state.project_db, incompatible_digest, &missing_metadata, + &missing_reconstruction) == LARDON3D_PROJECT_DB_NOT_FOUND); + + uint64_t h_restart_id = 0; + Lardon3DTask *h_pending = + lardon3d_project_create_incremental_reconstruction_task( + &fixture.state, &h_configuration, &h_restart_id); + CHECK(h_pending != nullptr); + Lardon3DTaskDurableSnapshot h_before{}; + CHECK(lardon3d_task_durable_snapshot(h_pending, &h_before)); + lardon3d_task_destroy(h_pending); + Lardon3DTaskKindDescriptor h_descriptor{ + LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_KIND, 1, + lardon3d_incremental_reconstruction_task_reconstruct}; + Lardon3DTaskKindRegistry h_registry{}; + CHECK(lardon3d_task_kind_registry_init(&h_registry, &h_descriptor, 1)); + Lardon3DTaskReconstructionContext h_runtime{ + fixture.root.c_str(), fixture.state.project_db, + fixture.state.resource_governor, nullptr}; + Lardon3DTask *h_restored = nullptr; + CHECK(lardon3d_task_kind_registry_restore( + &h_registry, LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_KIND, 1, + &h_before, &h_runtime, &h_restored) == LARDON3D_TASK_KIND_OK); + Lardon3DResourceEstimate h_restored_estimate{}; + CHECK(lardon3d_task_resource_estimate(h_restored, &h_restored_estimate)); + CHECK(std::memcmp(&h_estimate, &h_restored_estimate, + sizeof(h_estimate)) == 0); + CHECK(lardon3d_task_queue_add(fixture.state.task_queue, h_restored, nullptr)); + CHECK(wait_state(fixture.state.task_queue, h_restart_id, TASK_COMPLETED)); + CHECK(wait_reservations_released(fixture.state.resource_governor)); + Lardon3DIncrementalReconstructionMetadata reused_h_metadata{}; + Lardon3DSparseReconstruction reused_h{}; + CHECK(lardon3d_incremental_reconstruction_find_exact( + fixture.state.project_db, h_digest, &reused_h_metadata, &reused_h) == + LARDON3D_PROJECT_DB_OK); + CHECK(reused_h.reconstruction_id == h_reconstruction.reconstruction_id); + uint64_t reuse_id = 0; CHECK(lardon3d_project_enqueue_sparse_sfm_task(&fixture.state, &config, &reuse_id)); CHECK(wait_state(fixture.state.task_queue, reuse_id, TASK_COMPLETED)); Lardon3DSparseReconstruction reused{}; CHECK(lardon3d_sparse_reconstruction_find_exact( - fixture.state.project_db, fixture.track_set.track_set_id, fixture.scope.scope_id, 1, 1, + fixture.state.project_db, fixture.base_track_set.track_set_id, + fixture.scope.scope_id, 1, 1, fingerprint, &reused) == LARDON3D_PROJECT_DB_OK); CHECK(reused.reconstruction_id == reconstruction.reconstruction_id); @@ -331,6 +598,17 @@ bool test_task() { char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{}; CHECK(lardon3d_project_db_open(fixture.database_path.c_str(), &fixture.state.project_db, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DIncrementalReconstructionMetadata reopened_h_metadata{}; + Lardon3DSparseReconstruction reopened_h{}; + CHECK(lardon3d_incremental_reconstruction_find_exact( + fixture.state.project_db, h_digest, &reopened_h_metadata, + &reopened_h) == LARDON3D_PROJECT_DB_OK); + CHECK(reopened_h.reconstruction_id == h_reconstruction.reconstruction_id && + reopened_h_metadata.base_reconstruction_id == + reconstruction.reconstruction_id && + reopened_h.registered_image_count == kImageCount && + reopened_h.reprojection_rmse_px == + h_reconstruction.reprojection_rmse_px); Lardon3DTaskKindDescriptor descriptor{LARDON3D_SPARSE_SFM_TASK_KIND, 1, lardon3d_sparse_sfm_task_reconstruct}; Lardon3DTaskKindRegistry registry{};