feat: complete incremental reconstruction phase H

This commit is contained in:
fy59 2026-08-25 23:27:14 +02:00
parent 070280b9cc
commit 138b13babc
31 changed files with 3708 additions and 118 deletions

View file

@ -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é,

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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;

View file

@ -0,0 +1,102 @@
#ifndef LARDON3D_INCREMENTAL_RECONSTRUCTION_H
#define LARDON3D_INCREMENTAL_RECONSTRUCTION_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/resource_governor.h>
#include <lardon3d/sparse_sfm_incremental.h>
#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

View file

@ -0,0 +1,42 @@
#ifndef LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_H
#define LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_H
#include <stdbool.h>
#include <stdint.h>
#include <lardon3d/task_kind_registry.h>
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

View file

@ -10,7 +10,7 @@
#include <lardon3d/sparse_sfm_incremental.h>
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,

View file

@ -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,

View file

@ -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: [

View file

@ -0,0 +1,786 @@
#include "incremental_reconstruction_internal.h"
#include <lardon3d/incremental_reconstruction.h>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <map>
#include <new>
#include <set>
#include <tuple>
#include <vector>
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<ObservationKey> 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 <typename T>
bool copy_array(const std::vector<T> &source, T **output) {
*output = nullptr;
if (source.empty()) return true;
if (source.size() > SIZE_MAX / sizeof(T)) return false;
*output = static_cast<T *>(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<uint64_t, uint64_t> component_min_image;
std::map<uint64_t, uint64_t> component_camera_count;
std::map<uint64_t, uint64_t> component_landmark_count;
std::map<uint64_t, uint64_t> camera_components;
std::map<uint64_t, const Lardon3DSparseIncrementalLandmark *> landmarks_by_track;
std::set<uint64_t> 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<uint64_t, uint64_t> observation_counts;
std::map<uint64_t, std::set<uint32_t>> positions;
std::set<ObservationKey> 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<Lineage> *lineages,
std::map<uint64_t, size_t> *extension_lineage,
std::map<uint64_t,
std::vector<const Lardon3DSparseIncrementalObservation *>>
*extension_tracks,
std::map<ObservationKey, const Lardon3DSparseIncrementalObservation *> *extension_keys) {
const auto &base = *input.base;
std::map<uint64_t, const Lardon3DSparseIncrementalLandmark *> 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<uint64_t, std::vector<const Lardon3DSparseIncrementalObservation *>> base_tracks;
std::map<HistoricalObservationKey,
const Lardon3DSparseIncrementalLandmarkObservation *>
snapshot_historical;
std::map<HistoricalObservationKey,
const Lardon3DSparseIncrementalObservation *>
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<ObservationKey> 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<ObservationKey> 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<Lardon3DSparseGeometryPoint3> points;
std::vector<Lardon3DSparseGeometryPoint2> pixels;
};
bool register_cameras(
const Lardon3DIncrementalReconstructionInput &input,
const std::vector<Lineage> &lineages,
const std::map<uint64_t, size_t> &extension_lineage,
std::vector<Lardon3DSparseIncrementalCamera> *cameras,
std::set<uint64_t> *affected,
Lardon3DIncrementalReconstructionStatus *failure) {
std::map<uint64_t, const Lardon3DSparseIncrementalLandmark *> 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<uint64_t> registered;
std::map<uint64_t, uint64_t> component_keys;
for (const auto &camera : *cameras) {
registered.insert(camera.image_id);
component_keys.emplace(camera.component_key, camera.component_key);
}
std::set<uint64_t> remaining;
std::map<uint64_t, const Lardon3DSparseIncrementalImage *> images_by_id;
std::map<uint64_t, std::vector<const Lardon3DSparseIncrementalObservation *>>
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<Candidate> candidates;
for (uint64_t image_id : remaining) {
std::map<uint64_t, Candidate> 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<uint8_t> 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<const Lardon3DSparseIncrementalCamera *> 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<Lardon3DSparseBundleAdjustmentScaleAxis>(axis + 1)};
return true;
}
bool full_ba_affected(
const Lardon3DIncrementalReconstructionInput &input,
const std::set<uint64_t> &affected,
std::vector<Lardon3DSparseIncrementalComponent> *components,
std::vector<Lardon3DSparseIncrementalCamera> *cameras,
std::vector<Lardon3DSparseIncrementalLandmark> *landmarks,
std::vector<Lardon3DSparseIncrementalLandmarkObservation> *observations) {
std::map<uint64_t, size_t> component_indexes;
std::map<uint64_t, size_t> camera_indexes;
std::map<uint64_t, size_t> landmark_indexes;
std::map<uint64_t, std::vector<size_t>> cameras_by_component;
std::map<uint64_t, std::vector<size_t>> landmarks_by_component;
std::map<uint64_t, std::vector<size_t>> observations_by_component;
std::map<uint64_t, uint64_t> 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<Lardon3DSparseIncrementalComponent> sub_components;
std::vector<Lardon3DSparseIncrementalCamera> sub_cameras;
std::vector<Lardon3DSparseIncrementalLandmark> sub_landmarks;
std::vector<Lardon3DSparseIncrementalLandmarkObservation> 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<Lineage> lineages;
std::map<uint64_t, size_t> extension_lineage;
std::map<uint64_t,
std::vector<const Lardon3DSparseIncrementalObservation *>>
extension_tracks;
std::map<ObservationKey, const Lardon3DSparseIncrementalObservation *> 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<Lardon3DSparseIncrementalComponent> components(
input.base->components, input.base->components + input.base->component_count);
std::vector<Lardon3DSparseIncrementalCamera> cameras(
input.base->cameras, input.base->cameras + input.base->camera_count);
std::vector<Lardon3DSparseIncrementalLandmark> landmarks(
input.base->landmarks, input.base->landmarks + input.base->landmark_count);
std::set<uint64_t> affected;
Lardon3DIncrementalReconstructionStatus registration_failure =
LARDON3D_INCREMENTAL_RECONSTRUCTION_GEOMETRY_FAILED;
if (!register_cameras(input, lineages, extension_lineage, &cameras, &affected,
&registration_failure))
return registration_failure;
if (!checkpoint(input)) return LARDON3D_INCREMENTAL_RECONSTRUCTION_CANCELLED;
std::map<uint64_t, uint64_t> registered_component;
for (const auto &camera : cameras)
registered_component.emplace(camera.image_id, camera.component_key);
std::vector<Lardon3DSparseIncrementalLandmarkObservation> observations;
observations.reserve(input.extension->observation_count);
std::map<uint64_t, size_t> landmark_by_track;
std::map<uint64_t, size_t> 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<uint64_t, const Lardon3DSparseIncrementalImage *> 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<Lardon3DSparseGeometryPoint2> normalized;
std::vector<Lardon3DSparseGeometryPose> 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<uint64_t>(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<uint64_t, uint64_t> component_camera_counts;
std::map<uint64_t, uint64_t> 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 = {};
}

View file

@ -0,0 +1,122 @@
#include <lardon3d/incremental_reconstruction.h>
#include <cstring>
#include <limits>
#include <openssl/evp.h>
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<unsigned char>(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<unsigned char>(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<uint64_t>::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<uint64_t>::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;
}

View file

@ -0,0 +1,22 @@
#ifndef LARDON3D_INCREMENTAL_RECONSTRUCTION_INTERNAL_H
#define LARDON3D_INCREMENTAL_RECONSTRUCTION_INTERNAL_H
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/sparse_sfm_bundle_adjustment.h>
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

View file

@ -0,0 +1,764 @@
extern "C" {
#include <lardon3d/app_state.h>
#include <lardon3d/incremental_reconstruction_task.h>
}
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <map>
#include <new>
#include <set>
#include <string>
#include <tuple>
#include <vector>
#include "sparse_sfm_gate_f_internal.h"
extern "C" {
#include <lardon3d/feature_store.h>
#include <lardon3d/incremental_reconstruction.h>
#include <lardon3d/project.h>
#include <lardon3d/sparse_sfm_model.h>
#include <lardon3d/task_checkpoint.h>
#include <lardon3d/task_queue.h>
}
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<Lardon3DSparseIncrementalComponent> base_components;
std::vector<Lardon3DSparseIncrementalCamera> base_cameras;
std::vector<Lardon3DSparseIncrementalLandmark> base_landmarks;
std::vector<Lardon3DSparseIncrementalLandmarkObservation> base_observations;
std::vector<Lardon3DSparseIncrementalObservation> base_track_observations;
std::vector<Lardon3DSparseIncrementalImage> extension_images;
std::vector<Lardon3DSparseIncrementalObservation> extension_observations;
std::map<uint64_t, Lardon3DSparseGeometryCalibration> calibrations;
};
void destroy_context(void *userdata) { delete static_cast<Context *>(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<uint64_t, Lardon3DSparseCalibrationMember> *members,
std::map<uint64_t, Lardon3DSparseGeometryCalibration> *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<uint64_t, Lardon3DSparseCalibrationMember> 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<uint64_t, Reader> *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<uint64_t, Reader> *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<double>(keypoint.x), static_cast<double>(keypoint.y)};
return true;
}
bool load_base_snapshot(Context *context, Resolved *resolved,
std::map<uint64_t, Reader> *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<uint64_t, Reader> readers;
bool valid = load_base_snapshot(context, resolved, &readers);
std::map<uint64_t, Lardon3DSparseCalibrationMember> base_scope;
std::map<uint64_t, Lardon3DSparseCalibrationMember> 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<uint64_t> 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<unsigned long long>(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<Lardon3DTask *>(userdata));
}
bool build_publication(const Resolved &resolved,
const Lardon3DSparseIncrementalResult &snapshot,
const unsigned char parameter_fingerprint[32],
std::vector<Lardon3DSparseComponent> *components,
std::vector<Lardon3DSparseRegisteredImage> *cameras,
std::vector<Lardon3DSparseLandmark> *landmarks,
std::vector<Lardon3DSparseLandmarkObservation> *observations,
Lardon3DSparsePublication *publication) {
std::map<uint64_t, const Lardon3DSparseIncrementalCamera *> cameras_by_image;
std::map<uint64_t, const Lardon3DSparseIncrementalLandmark *> landmarks_by_track;
std::map<std::pair<uint64_t, uint32_t>,
const Lardon3DSparseIncrementalObservation *>
source_observations;
std::vector<double> 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<int64_t>(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(&parameters)) 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, &parameters,
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<Lardon3DSparseComponent> components;
std::vector<Lardon3DSparseRegisteredImage> cameras;
std::vector<Lardon3DSparseLandmark> landmarks;
std::vector<Lardon3DSparseLandmarkObservation> 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<Context *>(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<Context *>(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<Lardon3DTaskReconstructionContext *>(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;
}

View file

@ -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);

View file

@ -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);

View file

@ -1,5 +1,7 @@
#include <lardon3d/sparse_sfm_bundle_adjustment.h>
#include "incremental_reconstruction_internal.h"
#include <ceres/ceres.h>
#include <ceres/rotation.h>
@ -547,8 +549,10 @@ void copy_view(std::vector<T> *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);

View file

@ -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);
}

View file

@ -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

View file

@ -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,

View file

@ -11,6 +11,9 @@
#ifdef LARDON3D_SPARSE_SFM_TASK_AVAILABLE
#include <lardon3d/sparse_sfm_task.h>
#endif
#ifdef LARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_AVAILABLE
#include <lardon3d/incremental_reconstruction_task.h>
#endif
#include <lardon3d/task_kind_registry.h>
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 = {

View file

@ -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;
}

View file

@ -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;

View file

@ -0,0 +1,473 @@
#include <lardon3d/incremental_reconstruction.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <limits>
#include <vector>
#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<Lardon3DSparseIncrementalComponent> components;
std::vector<Lardon3DSparseIncrementalCamera> cameras;
std::vector<Lardon3DSparseIncrementalLandmark> landmarks;
std::vector<Lardon3DSparseIncrementalLandmarkObservation> base_observations;
std::vector<Lardon3DSparseIncrementalObservation> base_tracks;
std::vector<Lardon3DSparseIncrementalImage> images;
std::vector<Lardon3DSparseIncrementalObservation> 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(&parameters);
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<double>(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,
&parameters, 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<uint64_t>::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;
}

View file

@ -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;

View file

@ -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 "

View file

@ -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;
}

View file

@ -1,4 +1,5 @@
#include <math.h>
#include <openssl/evp.h>
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
@ -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;

View file

@ -7,6 +7,7 @@
extern "C" {
#include <lardon3d/project_db.h>
#include <lardon3d/incremental_reconstruction_task.h>
#include <lardon3d/sparse_sfm_task.h>
}
@ -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);

View file

@ -1,6 +1,8 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <map>
#include <sched.h>
#include <string>
#include <sys/stat.h>
@ -10,6 +12,8 @@
extern "C" {
#include <lardon3d/app_state.h>
#include <lardon3d/feature_store.h>
#include <lardon3d/incremental_reconstruction.h>
#include <lardon3d/incremental_reconstruction_task.h>
#include <lardon3d/project_db.h>
#include <lardon3d/resource_governor.h>
#include <lardon3d/sparse_sfm_model.h>
@ -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<float>(pixel.x);
keypoints[track].y = static_cast<float>(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<int>(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<Lardon3DProjectDbTrackObservation> base_observations(
kTrackCount * kBaseImageCount);
std::vector<Lardon3DProjectDbTrack> 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<uint32_t>(track), static_cast<uint32_t>(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<uint64_t, Lardon3DSparseRegisteredImage> cameras;
std::map<uint64_t, Lardon3DSparseLandmark> 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<double> 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<double>(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{};