From 558f00cd6f544dabdf716c7fc1702ea3a785a6eb Mon Sep 17 00:00:00 2001 From: fy59 Date: Sun, 9 Aug 2026 14:01:04 +0200 Subject: [PATCH] feat(reconstruction): add geometric verification model --- README.md | 9 +- docs/architecture/geometric_verification.md | 175 +++++++ docs/architecture/match_result.md | 5 +- docs/architecture/matcher.md | 6 +- docs/architecture/project_database.md | 59 ++- docs/architecture/reconstruction_pipeline.md | 15 +- docs/architecture/resource_aware_pipeline.md | 8 + docs/architecture/task_system.md | 5 + include/lardon3d/project_db.h | 49 +- meson.build | 16 + src/project_db.c | 406 +++++++++++++++- tests/test_geometric_verification.c | 463 +++++++++++++++++++ tests/test_match_result.c | 11 +- tests/test_matcher_task.c | 3 +- tests/test_project_db.c | 35 +- 15 files changed, 1227 insertions(+), 38 deletions(-) create mode 100644 docs/architecture/geometric_verification.md create mode 100644 tests/test_geometric_verification.c diff --git a/README.md b/README.md index 93fb211..c0a2480 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ Lardon3D ne vise pas simplement "dossier de photos → objet 3D", mais "ensemble - **Image View** : vues triées et filtrées pour la TUI - **Task** : moteur de tâches avec pause/reprise, annulation et séquences - **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre -- **Project Database v11** : tâches, catalogue, Feature Store, matching et tâches Matcher durables +- **Project Database v12** : tâches, catalogue, matching et résultats géométriques durables +- **Geometric Verification Model v1** : identité, masque d'inliers et modèle 3×3 persistants - **Task Kind Registry** : identité métier durable et reconstruction runtime explicite - **Recovery projet** : reprise automatique sélective et bornée des imports récupérables - **Task Queue** : file FIFO avec sélection adaptative et backpressure @@ -111,6 +112,7 @@ Acquisitions - [Candidate Pair](docs/architecture/candidate_pair.md) - [Match Result](docs/architecture/match_result.md) - [Matcher](docs/architecture/matcher.md) +- [Geometric Verification](docs/architecture/geometric_verification.md) - [Backend Vulkan ORB](docs/architecture/vulkan_matcher.md) - [Viewer](docs/architecture/viewer.md) - [Revue des fondations](docs/architecture/foundation_review.md) @@ -152,11 +154,12 @@ Pour les changements sensibles à la mémoire ou à la concurrence, ajouter ASan Lardon3D est en développement actif. La persistance des tâches, le catalogue, le Feature Store multipasse, le Visual Index ORB, Candidate Pair Generator -et Matcher v1 sont implémentés. Le runtime Feature + Matcher emploie des tâches +et Matcher v1 sont implémentés. Geometric Verification Model v1 est implémenté ; +le Geometric Verifier reste planifié. Le runtime Feature + Matcher emploie des tâches durables, de petits lots, le Resource Governor interactif et un hot path Vulkan ORB exact avec fallback CPU. La feasibility Vulkan SIFT/RootSIFT a été rejetée ; ces deux matchers restent sur OpenCV L2. DAG générique, -vérification géométrique, SfM et viewer restent des tickets séparés planifiés. +calcul de vérification géométrique, SfM et viewer restent des tickets séparés planifiés. ## Licence diff --git a/docs/architecture/geometric_verification.md b/docs/architecture/geometric_verification.md new file mode 100644 index 0000000..698d76d --- /dev/null +++ b/docs/architecture/geometric_verification.md @@ -0,0 +1,175 @@ +# Geometric Verification + +## Scope + +Geometric Verification Model v1 est le contrat scientifique persistant placé après le Matcher. +Il stocke un résultat terminé, compact et immutable. Il n'est ni un moteur de calcul ni une tâche. +Aucun RANSAC, USAC, MAGSAC, calcul d'inliers ou backend géométrique n'appartient à ce ticket. + +## Position in reconstruction pipeline + +La chaîne d'ownership est : + +```text +Feature Set → Candidate Pair → Match Result → Geometric Verification Result +``` + +Le masque indexe exclusivement l'ordre des entrées du Match File canonique du Match Result. Il +n'indexe directement ni les features, ni la Candidate Pair, ni un ordre temporaire de backend. + +## Scientific ownership + +Le parent scientifique est `match_result_id`. L'API accepte uniquement un Match Result existant, +`MATCHED`, avec `match_count` strictement positif. `NO_MATCH` et les erreurs runtime ne peuvent pas +produire de résultat géométrique. + +## Parent Match Result + +Le Match Store reste propriétaire de la validation du Match File. La création consulte le parent +et son `match_count` en DB ; elle ne relit pas l'asset. Un load valide aussi l'existence et l'état du +parent afin qu'une ligne corrompue ne soit jamais rendue comme résultat valide. + +## Persistent identity + +L'identité demandée et unique est : + +```text +(match_result_id, verifier_kind, verifier_version, parameter_fingerprint) +``` + +Le fingerprint est le SHA-256 opaque de 32 octets déjà standard dans le projet. Il représentera +un encodage de paramètres versionné, stable, à ordre de champs explicite et, pour les nombres +binaires, little-endian. Aucun timestamp, résultat, PID, durée ou identifiant matériel n'y entre. + +## Verifier kind + +v1 supporte uniquement `FUNDAMENTAL`, valeur persistante stable 1. Aucun comportement fictif +`ESSENTIAL` ou `HOMOGRAPHY` n'est réservé dans l'API publique. + +## Persistent states + +- `GEOMETRIC_REJECTED=1` : calcul scientifique terminé, critère non satisfait ; +- `GEOMETRIC_VERIFIED=2` : calcul scientifique terminé, critère satisfait. + +`FAILED`, `RUNNING`, `PAUSED` et `CANCELLED` appartiennent au Task Runtime. REJECTED peut conserver +un nombre d'inliers non nul. + +## Model representation + +FUNDAMENTAL utilise neuf colonnes SQLite `REAL`, en ordre ligne-major `m00` à `m22`. SQLite +convertit les valeurs numériques en binary64 sans exposer une ABI C. VERIFIED exige les neuf +valeurs présentes et finies. REJECTED exige les neuf valeurs NULL. Le modèle n'impose ni rang 2, +ni déterminant, ni normalisation ou échelle canonique ; ces règles relèvent du futur verifier. + +## Inlier representation + +Le masque est un BLOB SQLite obligatoire de taille exacte `ceil(match_count / 8)`. Pour l'entrée +`i`, `byte_index=i/8`, `bit_index=i%8` et le masque vaut `1u << bit_index`. Le bit 0 est donc le bit +de poids faible de l'octet 0. Cette convention est indépendante de l'endianness CPU et de l'ABI. +Les bits de padding du dernier octet valent zéro et le popcount est exactement `inlier_count`. + +Le masque existe pour REJECTED comme pour VERIFIED. Avec 8192 matches, il mesure au maximum +1024 octets. Un BLOB SQLite évite les milliers de lignes secondaires et la publication, le hash, +le nettoyage et la récupération d'un asset externe d'environ 1 Kio. Une liste `uint32_t` serait +jusqu'à 32 fois plus grande au cas dense et aurait un encodage supplémentaire à versionner. + +## Invariants + +- `0 <= inlier_count <= parent.match_count <= 8192` ; +- longueur, padding et popcount du masque sont canoniques ; +- REJECTED possède un masque cohérent et aucun modèle ; +- VERIFIED possède un masque cohérent et exactement neuf valeurs finies ; +- kind, version et fingerprint ont une sérialisation stable ; +- une ligne publiée est complète et immutable. + +Exemple : pour 100 matches, FUNDAMENTAL v1/fingerprint X peut publier REJECTED avec 23 inliers, +un masque de 13 octets et aucun modèle. Une autre identité peut publier VERIFIED avec 67 inliers, +le même format de masque et une matrice 3×3 finie. + +## Persistence semantics + +Une création valide puis insère identité, état, masque et modèle dans une transaction courte. Le +calcul futur se fera entièrement avant cette transaction. SQLite fournit l'atomicité ; aucun asset +ou journal secondaire n'est créé. + +## Reuse + +Le reuse cherche uniquement l'identité exacte, jamais le résultat le plus récent. Une identité +existante retourne une erreur de contrainte à `create`; le runtime fera `find`, validera puis +réutilisera. `INSERT OR REPLACE` est interdit, même si le nouveau contenu semble identique. + +## Invalidations + +Un nouveau Match Result possède un nouvel ID et ne réutilise donc aucun ancien résultat +géométrique. La FK emploie `ON DELETE CASCADE` : supprimer explicitement le parent supprime ses +enfants et ne crée pas d'orphelin. Aucun moteur d'invalidation parallèle n'est nécessaire. + +## Project DB schema + +Project DB v12 ajoute `geometric_verification_results`, une contrainte UNIQUE sur l'identité et un +index de pagination `(match_result_id, geometric_verification_result_id)`. Les CHECK SQL portent +les bornes scalaires, tailles locales et nullabilité modèle/état. La cohérence avec le parent, le +padding, le popcount et la finitude restent validés en C. + +## API + +L'API publique implémente : + +- `lardon3d_project_db_create_geometric_verification_result()` ; +- `lardon3d_project_db_load_geometric_verification_result()` ; +- `lardon3d_project_db_find_geometric_verification_result()` ; +- `lardon3d_project_db_list_geometric_verification_results()`. + +La liste est bornée à 256 entrées, filtrée par parent puis ordonnée par ID croissant avec curseur. +Le résultat en mémoire contient son `created_at` et son masque dans une capacité fixe de 1024 +octets : aucun ownership dynamique ni fonction de destruction. Les fonctions copient fingerprint, +masque et neuf coefficients ; l'appelant conserve ses entrées. + +Parent absent retourne `NOT_FOUND`; parent NO_MATCH ou parent incohérent retourne `CONSTRAINT` à +la création. Masque, modèle ou arguments locaux invalides retournent `INVALID_ARGUMENT`; duplicate +identity retourne `CONSTRAINT`. Un loader qui rencontre une ligne ou un parent incohérent retourne +`CORRUPT`, sans résultat partiel. + +## Resource bounds + +Un résultat contient au plus 1024 octets de masque et 72 octets de valeurs numériques, plus de +petites métadonnées. Une page est bornée. Le loader vérifie les entiers et tailles SQLite avant +tout cast ou copie. Il n'existe ni cache global, ni lecture non bornée, ni Content Store associé. +Le Match File parent mesure au plus 98 336 octets ; le futur job peut donc rester une petite unité. + +## Error ownership + +Seuls les résultats scientifiques terminés sont persistés. OOM, exception, annulation, timeout, +device lost, I/O transitoire ou panne de thread appartiennent à l'exécution de tâche. État du modèle +et état d'exécution sont deux contrats distincts. + +## Recovery semantics + +Après commit, le résultat est complet et réutilisable après réouverture. Avant commit, le rollback +ne laisse aucune ligne partielle. Un loader rejette toute ligne incohérente comme corruption au +lieu de réparer ou d'interpréter au mieux. + +## Future verifier execution contract + +Le prochain ticket prendra un Match Result et son Match File borné. L'accès nécessaire existe via +`lardon3d_feature_reader_keypoints()`, borné à 256 keypoints par appel ; l'intégration devra relier +les deux Feature Sets et les indices du Match File sans modifier le Feature Store. Le verifier +estimera hors transaction, dérivera état/masque/modèle, publiera en une courte transaction, +checkpoint puis libérera les buffers. Une paire est l'unité atomique. Task Runtime et Resource +Governor décideront admission, threads et lots ; zram/swap ne sont jamais un budget. + +Un backend reste hors identité seulement s'il est scientifiquement transparent. Sinon son +algorithme ou contrat doit apparaître dans kind/version/fingerprint avant publication. Toute seed +influençant le résultat doit avoir une politique déterministe versionnée ou être couverte par le +fingerprint. Aucun nombre de threads ou hardware ID n'est un paramètre scientifique par défaut. + +## Explicitly out of scope + +Le calcul géométrique, le choix RANSAC/USAC/MAGSAC, OpenCV geometry, GPU, Vulkan, OpenCL, shader, +task kind, worker, checkpoint et nouvelle orchestration sont explicitement hors périmètre. + +## Versioning + +Project DB schema version 12 décrit le stockage. `verifier_version` décrit indépendamment le +contrat scientifique. Changer un algorithme n'impose une migration DB que si la représentation +persistante change. diff --git a/docs/architecture/match_result.md b/docs/architecture/match_result.md index da2af51..8aa6f9a 100644 --- a/docs/architecture/match_result.md +++ b/docs/architecture/match_result.md @@ -61,4 +61,7 @@ contrainte `UNIQUE`. Le prochain modèle possède ses propres statistiques géométriques, dont un éventuel `inlier_count`. -NEXT: GEOMETRIC VERIFICATION MODEL +Geometric Verification Model v1 est désormais l'enfant scientifique persistant +de ce résultat dans Project DB v12. + +NEXT: GEOMETRIC VERIFIER v1 diff --git a/docs/architecture/matcher.md b/docs/architecture/matcher.md index d5925e8..251e6c9 100644 --- a/docs/architecture/matcher.md +++ b/docs/architecture/matcher.md @@ -92,6 +92,10 @@ estimation couvrant ce working set ; aucune seconde logique de budget n'est introduite. Sa ligne durable `matcher_tasks` appartient au schéma Project DB v11 ; le Match Result reste le contrat publié en v10. +Project DB v12 ajoute un enfant `Geometric Verification Result` référencé par +`match_result_id`. Le Matcher ne calcule, ne stocke et ne valide aucun inlier +géométrique ; seul son Match File canonique définit l'ordre indexé par le masque. + ### Backend Vulkan ORB La frontière évaluée remplace uniquement KNN Hamming par un compute top-2 : un @@ -113,4 +117,4 @@ mêmes paires attendues, l'ordre et la sérialisation stables. Aucune promesse bit-à-bit cross-platform n'est faite pour les distances L2 flottantes. ORB/Hamming bénéficie d'une garantie plus forte grâce à sa distance entière. -NEXT: GEOMETRIC VERIFICATION MODEL +NEXT: GEOMETRIC VERIFIER v1 diff --git a/docs/architecture/project_database.md b/docs/architecture/project_database.md index 5b378f2..61ba7da 100644 --- a/docs/architecture/project_database.md +++ b/docs/architecture/project_database.md @@ -1,6 +1,7 @@ # Base de données projet Lardon3D -> Version courante : **v11**. La migration transactionnelle v10→v11 ajoute +> Version courante : **v12**. La migration transactionnelle v11→v12 ajoute le +> modèle immutable `geometric_verification_results`. La migration v10→v11 ajoute > `matcher_tasks` pour la tâche Matcher durable. La version v10 publiée ajoute > uniquement `match_results` pour le Match Result Model. La migration v8→v9 > ajoute la table `candidate_pair_generate_tasks` pour la tâche durable @@ -396,19 +397,65 @@ CREATE INDEX match_results_feature_set_b_idx - `lardon3d_project_db_record_matcher_task()` — UPSERT configuration/curseur - `lardon3d_project_db_load_matcher_task()` — SELECT par task_id +## Schéma v12 implémenté + +La migration v11→v12 ajoute uniquement `geometric_verification_results`. Le +parent est un Match Result `MATCHED`; sa validation interligne reste dans l'API. + +Schéma abrégé (la chaîne SQL exécutable canonique reste dans `src/project_db.c`) : + +```sql +CREATE TABLE geometric_verification_results( + geometric_verification_result_id INTEGER PRIMARY KEY AUTOINCREMENT + CHECK(geometric_verification_result_id>0), + match_result_id INTEGER NOT NULL + REFERENCES match_results(match_result_id) ON DELETE CASCADE, + verifier_kind INTEGER NOT NULL CHECK(verifier_kind=1), + verifier_version INTEGER NOT NULL + CHECK(verifier_version>0 AND verifier_version<=4294967295), + parameter_fingerprint BLOB NOT NULL + CHECK(length(parameter_fingerprint)=32), + status INTEGER NOT NULL CHECK(status IN (1,2)), + inlier_count INTEGER NOT NULL + CHECK(inlier_count>=0 AND inlier_count<=8192), + inlier_mask BLOB NOT NULL + CHECK(length(inlier_mask)>=1 AND length(inlier_mask)<=1024), + model_m00 REAL, model_m01 REAL, model_m02 REAL, + model_m10 REAL, model_m11 REAL, model_m12 REAL, + model_m20 REAL, model_m21 REAL, model_m22 REAL, + created_at INTEGER NOT NULL CHECK(created_at>=0), + CHECK(/* REJECTED: neuf NULL ; VERIFIED: neuf non-NULL */), + UNIQUE(match_result_id, verifier_kind, verifier_version, + parameter_fingerprint) +); +CREATE INDEX geometric_verification_results_parent_idx + ON geometric_verification_results( + match_result_id, geometric_verification_result_id + ); +``` + +Les valeurs stables sont FUNDAMENTAL=1, GEOMETRIC_REJECTED=1 et +GEOMETRIC_VERIFIED=2. REJECTED interdit le modèle ; VERIFIED exige neuf valeurs +finies. Pour les deux états, l'API impose taille canonique, padding nul et +popcount exact du masque, ainsi que `inlier_count <= parent.match_count`. +L'index parent sert la liste paginée ; la contrainte UNIQUE sert le find exact. +Le contrat complet, dont l'ordre des bits, est dans +`geometric_verification.md`. + ## Ouverture et migrations -Une DB vide reçoit la chaîne de schémas jusqu'à v11 dans une transaction +Une DB vide reçoit la chaîne de schémas jusqu'à v12 dans une transaction `BEGIN IMMEDIATE`. Une DB v1 reçoit transactionnellement les colonnes nullable `task_kind` et `task_kind_version`, puis les migrations v2→v3. Les anciennes lignes restent `NULL/NULL`, sans type inventé et sans perte des projets, tâches, checkpoints ou artefacts. Une interruption ou erreur provoque un rollback complet. Les DB v1, -v2, v3, v4, v5, v6, v7, v8, v9 et v10 sont migrées séquentiellement vers v11. +v2, v3, v4, v5, v6, v7, v8, v9, v10 et v11 sont migrées séquentiellement vers v12. Une v10 publiée est validée comme telle avant que v10→v11 crée `matcher_tasks` ; son absence n'est donc pas une corruption. Une version future est refusée et une DB contenant des tables sans métadonnée de version est considérée corrompue. La fonction interne de migration applique uniquement la chaîne séquentielle connue jusqu'à -v11 ; une valeur hors de 1..11 est refusée. +v12 ; une valeur hors de 1..12 est refusée. La failure injectée v12 rollbacke +la table, l'index et le changement de version, laissant une vraie v11 utilisable. Migration v1→v2 exacte, exécutée entre `BEGIN IMMEDIATE` et `COMMIT` : @@ -545,8 +592,8 @@ ouvert. ## Statut -**IMPLEMENTED** — SQLite système, schéma v11 et migrations séquentielles -v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11, identité projet, transactions +**IMPLEMENTED** — SQLite système, schéma v12 et migrations séquentielles +v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11→v12, 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, diff --git a/docs/architecture/reconstruction_pipeline.md b/docs/architecture/reconstruction_pipeline.md index 416acb6..60b216d 100644 --- a/docs/architecture/reconstruction_pipeline.md +++ b/docs/architecture/reconstruction_pipeline.md @@ -97,10 +97,20 @@ Index, persistance, canonicalisation, idempotence et tâche durable. | **Orchestration** | `matcher.run` traite les Candidate Pairs par pages et lots durables de 1/2/4/8. | **Statut :** IMPLEMENTED v1 — Matcher, Match Store, reprise idempotente et Task -durable. La vérification géométrique reste l'étape suivante, non commencée. +durable. Le calcul Geometric Verifier reste l'étape suivante, non commencée. Le Match Result appartient à Project DB v10 et la tâche durable `matcher.run` à Project DB v11. +### F2. Geometric Verification + +Project DB v12 implémente uniquement le modèle scientifique persistant. Chaque +résultat appartient à un Match Result `MATCHED`, possède une identité exacte, +un masque compact borné et, si VERIFIED, une matrice fondamentale 3×3 finie. +La publication est atomique et immutable. Le calcul, son task kind et le choix +d'algorithme restent planifiés dans Geometric Verifier v1. + +**Statut :** MODEL IMPLEMENTED v1 — VERIFIER PLANNED. + --- ### G. Tracks @@ -229,7 +239,8 @@ Image Catalog (B) ──► Feature Store (C) ## Statut du pipeline Import, Image Catalog, Feature Extraction, Feature Store, Visual Index, -Candidate Pair et Matching v1 sont **IMPLEMENTED**. Geometric Verification, +Candidate Pair, Matching v1 et Geometric Verification Model v1 sont +**IMPLEMENTED**. Geometric Verifier, Tracks et SfM sont **PLANNED**. Ce document décrit la vision architecturale cible du pipeline de diff --git a/docs/architecture/resource_aware_pipeline.md b/docs/architecture/resource_aware_pipeline.md index a88f8b7..29fab6b 100644 --- a/docs/architecture/resource_aware_pipeline.md +++ b/docs/architecture/resource_aware_pipeline.md @@ -55,6 +55,14 @@ chaque paire et entre les lots. Un crash après publication mais avant le checkpoint revoit la paire : le Matcher réutilise alors le Match Result et ne recalcule pas les descripteurs. +## Geometric Verification Model + +Project DB v12 stocke un résultat borné à 1024 octets de masque et neuf +binary64. Le modèle ne lit aucun signal matériel et ne modifie pas le Governor. +Le futur verifier calculera une paire hors transaction, publiera par une courte +transaction, checkpoint puis libérera ses buffers avant réadmission. Admission, +threads et lots resteront exclusivement décidés par Runtime et Governor. + ## GPU et files La Radeon 780M est UMA : toute mémoire GPU compte aussi comme pression RAM. Un diff --git a/docs/architecture/task_system.md b/docs/architecture/task_system.md index 75043e9..ea52959 100644 --- a/docs/architecture/task_system.md +++ b/docs/architecture/task_system.md @@ -111,6 +111,11 @@ par lots adaptatifs de 1, 2, 4 ou 8. Il persiste le curseur effectue une rupture de séquence avant le suivant. Une paire repassée après un crash est réutilisée par son Match Result. +Geometric Verification Model v1 n'ajoute aucun task kind ni état runtime. +Project DB v12 ne contient que les deux états scientifiques terminaux. Le futur +Geometric Verifier v1 devra obtenir réservation, pause/cancel, lot et checkpoint +dans un ticket séparé avant toute exécution. + Le chemin de production de l'import ne possède plus de thread ni de drapeau d'annulation privés. Son wrapper TUI ne fait qu'enqueue/cancel/observer la tâche générique. Chaque callback traite un lot borné, checkpoint hors mutex de diff --git a/include/lardon3d/project_db.h b/include/lardon3d/project_db.h index a2e3e4c..da06946 100644 --- a/include/lardon3d/project_db.h +++ b/include/lardon3d/project_db.h @@ -9,7 +9,7 @@ #include enum { - LARDON3D_PROJECT_DB_SCHEMA_VERSION = 11, + LARDON3D_PROJECT_DB_SCHEMA_VERSION = 12, LARDON3D_PROJECT_DB_ID_CAPACITY = 65, LARDON3D_PROJECT_DB_KIND_CAPACITY = 65, LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096, @@ -18,6 +18,9 @@ enum { LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX = 256, LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX = 256, LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX = 256, + LARDON3D_PROJECT_DB_GEOMETRIC_RESULT_PAGE_MAX = 256, + LARDON3D_PROJECT_DB_INLIER_MASK_MAX = 1024, + LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS = 9, LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY = 256, LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY = 256, LARDON3D_PROJECT_DB_SHA256_SIZE = 32, @@ -158,6 +161,30 @@ typedef struct { int64_t created_at; } Lardon3DProjectDbMatchResult; +typedef enum { + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL = 1, +} Lardon3DGeometricVerifierKind; + +typedef enum { + LARDON3D_GEOMETRIC_REJECTED = 1, + LARDON3D_GEOMETRIC_VERIFIED = 2, +} Lardon3DGeometricVerificationStatus; + +typedef struct { + uint64_t geometric_verification_result_id; + uint64_t match_result_id; + Lardon3DGeometricVerifierKind verifier_kind; + uint32_t verifier_version; + unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE]; + Lardon3DGeometricVerificationStatus status; + uint32_t inlier_count; + size_t inlier_mask_size; + unsigned char inlier_mask[LARDON3D_PROJECT_DB_INLIER_MASK_MAX]; + bool has_model; + double model[LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS]; + int64_t created_at; +} Lardon3DProjectDbGeometricVerificationResult; + typedef enum { LARDON3D_PROJECT_DB_IMAGE_REGISTERED = 0, LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT @@ -507,4 +534,24 @@ Lardon3DProjectDbResult lardon3d_project_db_list_match_results( Lardon3DProjectDb *database, uint64_t after_match_result_id, Lardon3DProjectDbMatchResult *results, size_t capacity, size_t *count); +Lardon3DProjectDbResult lardon3d_project_db_create_geometric_verification_result( + Lardon3DProjectDb *database, uint64_t match_result_id, + Lardon3DGeometricVerifierKind verifier_kind, uint32_t verifier_version, + const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE], + Lardon3DGeometricVerificationStatus status, uint32_t inlier_count, + const unsigned char *inlier_mask, size_t inlier_mask_size, const double *model, + int64_t created_at, Lardon3DProjectDbGeometricVerificationResult *result); +Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verification_result( + Lardon3DProjectDb *database, uint64_t geometric_verification_result_id, + Lardon3DProjectDbGeometricVerificationResult *result); +Lardon3DProjectDbResult lardon3d_project_db_find_geometric_verification_result( + Lardon3DProjectDb *database, uint64_t match_result_id, + Lardon3DGeometricVerifierKind verifier_kind, uint32_t verifier_version, + const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE], + Lardon3DProjectDbGeometricVerificationResult *result); +Lardon3DProjectDbResult lardon3d_project_db_list_geometric_verification_results( + Lardon3DProjectDb *database, uint64_t match_result_id, + uint64_t after_geometric_verification_result_id, + Lardon3DProjectDbGeometricVerificationResult *results, size_t capacity, size_t *count); + #endif diff --git a/meson.build b/meson.build index e637b18..ea62743 100644 --- a/meson.build +++ b/meson.build @@ -507,6 +507,22 @@ match_result_test = executable( test('match-result', match_result_test, timeout: 30) +geometric_verification_test = executable( + 'test-geometric-verification', + sources: [ + 'tests/test_geometric_verification.c', + 'src/project_db.c', + 'src/task.c', + 'src/resource_governor.c', + 'src/resource_snapshot.c', + ], + c_args: ['-DLARDON3D_PROJECT_DB_TESTING'], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl], +) + +test('geometric-verification', geometric_verification_test, timeout: 30) + project_test = executable( 'test-project', sources: [ diff --git a/src/project_db.c b/src/project_db.c index 993bb19..239d160 100644 --- a/src/project_db.c +++ b/src/project_db.c @@ -238,6 +238,31 @@ static const char schema_matcher_task_v11[] = "matcher_kind INTEGER NOT NULL CHECK(matcher_kind BETWEEN 0 AND 2)," "ratio_threshold REAL NOT NULL CHECK(ratio_threshold>0.0 AND ratio_threshold<1.0));"; +static const char schema_geometric_verification_v12[] = + "CREATE TABLE geometric_verification_results(" + "geometric_verification_result_id INTEGER PRIMARY KEY AUTOINCREMENT " + "CHECK(geometric_verification_result_id>0)," + "match_result_id INTEGER NOT NULL REFERENCES match_results(match_result_id) ON DELETE CASCADE," + "verifier_kind INTEGER NOT NULL CHECK(verifier_kind=1)," + "verifier_version INTEGER NOT NULL CHECK(verifier_version>0 AND verifier_version<=4294967295)," + "parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32)," + "status INTEGER NOT NULL CHECK(status IN (1,2))," + "inlier_count INTEGER NOT NULL CHECK(inlier_count>=0 AND inlier_count<=8192)," + "inlier_mask BLOB NOT NULL CHECK(length(inlier_mask)>=1 AND length(inlier_mask)<=1024)," + "model_m00 REAL,model_m01 REAL,model_m02 REAL," + "model_m10 REAL,model_m11 REAL,model_m12 REAL," + "model_m20 REAL,model_m21 REAL,model_m22 REAL," + "created_at INTEGER NOT NULL CHECK(created_at>=0)," + "CHECK((status=1 AND model_m00 IS NULL AND model_m01 IS NULL AND model_m02 IS NULL AND " + "model_m10 IS NULL AND model_m11 IS NULL AND model_m12 IS NULL AND model_m20 IS NULL AND " + "model_m21 IS NULL AND model_m22 IS NULL) OR " + "(status=2 AND model_m00 IS NOT NULL AND model_m01 IS NOT NULL AND model_m02 IS NOT NULL AND " + "model_m10 IS NOT NULL AND model_m11 IS NOT NULL AND model_m12 IS NOT NULL AND " + "model_m20 IS NOT NULL AND model_m21 IS NOT NULL AND model_m22 IS NOT NULL))," + "UNIQUE(match_result_id,verifier_kind,verifier_version,parameter_fingerprint));" + "CREATE INDEX geometric_verification_results_parent_idx ON " + "geometric_verification_results(match_result_id,geometric_verification_result_id);"; + 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 : ""); @@ -327,7 +352,7 @@ 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 != 8 && from_version != 9 && from_version != 10 && from_version != 11) { return LARDON3D_PROJECT_DB_CORRUPT; } Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration"); @@ -568,6 +593,22 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int "finish schema v11 migration"); } } + if (result == LARDON3D_PROJECT_DB_OK && from_version < 12) { + result = execute(database, schema_geometric_verification_v12, + "migrate schema v11 to v12"); +#ifdef LARDON3D_PROJECT_DB_TESTING + const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V12"); + if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) { + result = execute(database, "INSERT INTO missing_test_table VALUES(1)", + "forced migration v12 failure"); + } +#endif + if (result == LARDON3D_PROJECT_DB_OK) { + result = execute(database, + "UPDATE metadata SET value=12 WHERE key='schema_version' AND value=11", + "finish schema v12 migration"); + } + } if (result == LARDON3D_PROJECT_DB_OK) { result = execute(database, "COMMIT", "commit migration"); } @@ -682,7 +723,8 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje "feature_support_members", "candidate_pairs", "matcher_tasks", - "match_results"}; + "match_results", + "geometric_verification_results"}; for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) && result == LARDON3D_PROJECT_DB_OK; ++index) { @@ -3751,6 +3793,366 @@ Lardon3DProjectDbResult lardon3d_project_db_list_match_results( return db_result; } +static size_t geometric_mask_size(uint32_t match_count) { + return ((size_t)match_count + 7U) / 8U; +} + +static uint32_t geometric_mask_popcount(const unsigned char *mask, size_t size) { + uint32_t count = 0; + for (size_t byte_index = 0; byte_index < size; ++byte_index) { + unsigned int value = mask[byte_index]; + while (value != 0U) { + value &= value - 1U; + ++count; + } + } + return count; +} + +static bool valid_geometric_mask(const unsigned char *mask, size_t size, uint32_t match_count, + uint32_t inlier_count) { + size_t expected_size = geometric_mask_size(match_count); + if (!mask || size != expected_size || expected_size == 0 || + expected_size > LARDON3D_PROJECT_DB_INLIER_MASK_MAX || inlier_count > match_count) { + return false; + } + unsigned int used_bits = match_count % 8U; + if (used_bits != 0U) { + unsigned int padding_mask = 0xffU << used_bits; + if (((unsigned int)mask[size - 1] & padding_mask) != 0U) { + return false; + } + } + return geometric_mask_popcount(mask, size) == inlier_count; +} + +static bool valid_geometric_model(Lardon3DGeometricVerificationStatus status, + const double *model) { + if (status == LARDON3D_GEOMETRIC_REJECTED) { + return model == NULL; + } + if (status != LARDON3D_GEOMETRIC_VERIFIED || !model) { + return false; + } + for (size_t index = 0; index < LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS; ++index) { + if (!isfinite(model[index])) { + return false; + } + } + return true; +} + +static const char geometric_result_columns[] = + "g.geometric_verification_result_id,g.match_result_id,g.verifier_kind," + "g.verifier_version,g.parameter_fingerprint,g.status,g.inlier_count,g.inlier_mask," + "g.model_m00,g.model_m01,g.model_m02,g.model_m10,g.model_m11,g.model_m12," + "g.model_m20,g.model_m21,g.model_m22,g.created_at,m.result_status,m.match_count "; + +static bool read_geometric_result(sqlite3_stmt *statement, + Lardon3DProjectDbGeometricVerificationResult *result) { + if (!statement || !result) { + return false; + } + for (int column = 0; column <= 7; ++column) { + int expected_type = column == 4 || column == 7 ? SQLITE_BLOB : SQLITE_INTEGER; + if (sqlite3_column_type(statement, column) != expected_type) { + return false; + } + } + if (sqlite3_column_type(statement, 17) != SQLITE_INTEGER || + sqlite3_column_type(statement, 18) != SQLITE_INTEGER || + sqlite3_column_type(statement, 19) != SQLITE_INTEGER) { + return false; + } + sqlite3_int64 id = sqlite3_column_int64(statement, 0); + sqlite3_int64 parent_id = sqlite3_column_int64(statement, 1); + sqlite3_int64 kind = sqlite3_column_int64(statement, 2); + sqlite3_int64 version = sqlite3_column_int64(statement, 3); + sqlite3_int64 status = sqlite3_column_int64(statement, 5); + sqlite3_int64 inlier_count = sqlite3_column_int64(statement, 6); + sqlite3_int64 created_at = sqlite3_column_int64(statement, 17); + sqlite3_int64 parent_status = sqlite3_column_int64(statement, 18); + sqlite3_int64 match_count = sqlite3_column_int64(statement, 19); + int fingerprint_size = sqlite3_column_bytes(statement, 4); + int mask_size = sqlite3_column_bytes(statement, 7); + const void *fingerprint = sqlite3_column_blob(statement, 4); + const void *mask = sqlite3_column_blob(statement, 7); + if (id <= 0 || parent_id <= 0 || kind != LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL || + version <= 0 || version > UINT32_MAX || + (status != LARDON3D_GEOMETRIC_REJECTED && status != LARDON3D_GEOMETRIC_VERIFIED) || + inlier_count < 0 || inlier_count > 8192 || created_at < 0 || + parent_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED || match_count <= 0 || + match_count > 8192 || fingerprint_size != LARDON3D_PROJECT_DB_SHA256_SIZE || + !fingerprint || mask_size <= 0 || mask_size > LARDON3D_PROJECT_DB_INLIER_MASK_MAX || !mask || + !valid_geometric_mask(mask, (size_t)mask_size, (uint32_t)match_count, + (uint32_t)inlier_count)) { + return false; + } + double model[LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS] = {0}; + bool has_model = status == LARDON3D_GEOMETRIC_VERIFIED; + for (int index = 0; index < LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS; ++index) { + int type = sqlite3_column_type(statement, 8 + index); + if ((!has_model && type != SQLITE_NULL) || + (has_model && type != SQLITE_FLOAT && type != SQLITE_INTEGER)) { + return false; + } + if (has_model) { + model[index] = sqlite3_column_double(statement, 8 + index); + if (!isfinite(model[index])) { + return false; + } + } + } + memset(result, 0, sizeof(*result)); + result->geometric_verification_result_id = (uint64_t)id; + result->match_result_id = (uint64_t)parent_id; + result->verifier_kind = (Lardon3DGeometricVerifierKind)kind; + result->verifier_version = (uint32_t)version; + memcpy(result->parameter_fingerprint, fingerprint, LARDON3D_PROJECT_DB_SHA256_SIZE); + result->status = (Lardon3DGeometricVerificationStatus)status; + result->inlier_count = (uint32_t)inlier_count; + result->inlier_mask_size = (size_t)mask_size; + memcpy(result->inlier_mask, mask, (size_t)mask_size); + result->has_model = has_model; + memcpy(result->model, model, sizeof(model)); + result->created_at = created_at; + return true; +} + +static Lardon3DProjectDbResult read_single_geometric_result( + Lardon3DProjectDb *database, sqlite3_stmt *statement, + Lardon3DProjectDbGeometricVerificationResult *result) { + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) { + (void)sqlite3_finalize(statement); + return LARDON3D_PROJECT_DB_NOT_FOUND; + } + if (code != SQLITE_ROW) { + Lardon3DProjectDbResult db_result = sqlite_result(database, code, "read geometric result"); + (void)sqlite3_finalize(statement); + return db_result; + } + bool valid = read_geometric_result(statement, result); + code = sqlite3_step(statement); + (void)sqlite3_finalize(statement); + return valid && code == SQLITE_DONE ? LARDON3D_PROJECT_DB_OK : LARDON3D_PROJECT_DB_CORRUPT; +} + +Lardon3DProjectDbResult lardon3d_project_db_create_geometric_verification_result( + Lardon3DProjectDb *database, uint64_t match_result_id, + Lardon3DGeometricVerifierKind verifier_kind, uint32_t verifier_version, + const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE], + Lardon3DGeometricVerificationStatus status, uint32_t inlier_count, + const unsigned char *inlier_mask, size_t inlier_mask_size, const double *model, + int64_t created_at, Lardon3DProjectDbGeometricVerificationResult *result) { + if (!database || !valid_catalog_id(match_result_id) || + verifier_kind != LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL || verifier_version == 0 || + !parameter_fingerprint || inlier_count > 8192 || !inlier_mask || inlier_mask_size == 0 || + inlier_mask_size > LARDON3D_PROJECT_DB_INLIER_MASK_MAX || + !valid_geometric_model(status, model) || created_at < 0 || !result) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + memset(result, 0, sizeof(*result)); + (void)pthread_mutex_lock(&database->mutex); + Lardon3DProjectDbResult db_result = + execute(database, "BEGIN IMMEDIATE", "begin geometric result creation"); + sqlite3_stmt *statement = NULL; + uint32_t match_count = 0; + if (db_result == LARDON3D_PROJECT_DB_OK) { + db_result = prepare(database, + "SELECT result_status,match_count FROM match_results " + "WHERE match_result_id=?1", + &statement); + } + if (db_result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) { + db_result = LARDON3D_PROJECT_DB_NOT_FOUND; + } else if (code != SQLITE_ROW || sqlite3_column_type(statement, 0) != SQLITE_INTEGER || + sqlite3_column_type(statement, 1) != SQLITE_INTEGER) { + db_result = LARDON3D_PROJECT_DB_CORRUPT; + } else { + sqlite3_int64 parent_status = sqlite3_column_int64(statement, 0); + sqlite3_int64 parent_count = sqlite3_column_int64(statement, 1); + if (parent_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED || parent_count <= 0 || + parent_count > 8192) { + db_result = LARDON3D_PROJECT_DB_CONSTRAINT; + } else { + match_count = (uint32_t)parent_count; + } + } + (void)sqlite3_finalize(statement); + statement = NULL; + } + if (db_result == LARDON3D_PROJECT_DB_OK && + !valid_geometric_mask(inlier_mask, inlier_mask_size, match_count, inlier_count)) { + db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + if (db_result == LARDON3D_PROJECT_DB_OK) { + db_result = prepare( + database, + "INSERT INTO geometric_verification_results(" + "match_result_id,verifier_kind,verifier_version,parameter_fingerprint,status," + "inlier_count,inlier_mask,model_m00,model_m01,model_m02,model_m10,model_m11,model_m12," + "model_m20,model_m21,model_m22,created_at) " + "VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17)", + &statement); + } + if (db_result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id); + (void)sqlite3_bind_int(statement, 2, (int)verifier_kind); + (void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)verifier_version); + (void)sqlite3_bind_blob(statement, 4, parameter_fingerprint, + LARDON3D_PROJECT_DB_SHA256_SIZE, SQLITE_TRANSIENT); + (void)sqlite3_bind_int(statement, 5, (int)status); + (void)sqlite3_bind_int64(statement, 6, (sqlite3_int64)inlier_count); + (void)sqlite3_bind_blob(statement, 7, inlier_mask, (int)inlier_mask_size, SQLITE_TRANSIENT); + for (int index = 0; index < LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS; ++index) { + if (model) { + (void)sqlite3_bind_double(statement, 8 + index, model[index]); + } else { + (void)sqlite3_bind_null(statement, 8 + index); + } + } + (void)sqlite3_bind_int64(statement, 17, created_at); + db_result = step_done(database, statement, "create geometric result"); + statement = NULL; + } + uint64_t result_id = 0; + if (db_result == LARDON3D_PROJECT_DB_OK) { + sqlite3_int64 inserted_id = sqlite3_last_insert_rowid(database->connection); + if (inserted_id <= 0) { + db_result = LARDON3D_PROJECT_DB_CORRUPT; + } else { + result_id = (uint64_t)inserted_id; + db_result = execute(database, "COMMIT", "commit geometric result creation"); + } + } + if (db_result != LARDON3D_PROJECT_DB_OK) { + (void)execute(database, "ROLLBACK", "rollback geometric result creation"); + } + (void)pthread_mutex_unlock(&database->mutex); + if (db_result != LARDON3D_PROJECT_DB_OK) { + return db_result; + } + result->geometric_verification_result_id = result_id; + result->match_result_id = match_result_id; + result->verifier_kind = verifier_kind; + result->verifier_version = verifier_version; + memcpy(result->parameter_fingerprint, parameter_fingerprint, + LARDON3D_PROJECT_DB_SHA256_SIZE); + result->status = status; + result->inlier_count = inlier_count; + result->inlier_mask_size = inlier_mask_size; + memcpy(result->inlier_mask, inlier_mask, inlier_mask_size); + result->has_model = model != NULL; + if (model) { + memcpy(result->model, model, sizeof(result->model)); + } + result->created_at = created_at; + return LARDON3D_PROJECT_DB_OK; +} + +Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verification_result( + Lardon3DProjectDb *database, uint64_t geometric_verification_result_id, + Lardon3DProjectDbGeometricVerificationResult *result) { + if (!database || !valid_catalog_id(geometric_verification_result_id) || !result) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + memset(result, 0, sizeof(*result)); + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + char sql[1024]; + (void)snprintf(sql, sizeof(sql), "SELECT %sFROM geometric_verification_results g " + "JOIN match_results m ON m.match_result_id=g.match_result_id " + "WHERE g.geometric_verification_result_id=?1", + geometric_result_columns); + Lardon3DProjectDbResult db_result = prepare(database, sql, &statement); + if (db_result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)geometric_verification_result_id); + db_result = read_single_geometric_result(database, statement, result); + } + (void)pthread_mutex_unlock(&database->mutex); + return db_result; +} + +Lardon3DProjectDbResult lardon3d_project_db_find_geometric_verification_result( + Lardon3DProjectDb *database, uint64_t match_result_id, + Lardon3DGeometricVerifierKind verifier_kind, uint32_t verifier_version, + const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE], + Lardon3DProjectDbGeometricVerificationResult *result) { + if (!database || !valid_catalog_id(match_result_id) || + verifier_kind != LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL || verifier_version == 0 || + !parameter_fingerprint || !result) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + memset(result, 0, sizeof(*result)); + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + char sql[1280]; + (void)snprintf(sql, sizeof(sql), "SELECT %sFROM geometric_verification_results g " + "JOIN match_results m ON m.match_result_id=g.match_result_id " + "WHERE g.match_result_id=?1 AND g.verifier_kind=?2 AND " + "g.verifier_version=?3 AND g.parameter_fingerprint=?4", + geometric_result_columns); + Lardon3DProjectDbResult db_result = prepare(database, sql, &statement); + if (db_result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id); + (void)sqlite3_bind_int(statement, 2, (int)verifier_kind); + (void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)verifier_version); + (void)sqlite3_bind_blob(statement, 4, parameter_fingerprint, + LARDON3D_PROJECT_DB_SHA256_SIZE, SQLITE_TRANSIENT); + db_result = read_single_geometric_result(database, statement, result); + } + (void)pthread_mutex_unlock(&database->mutex); + return db_result; +} + +Lardon3DProjectDbResult lardon3d_project_db_list_geometric_verification_results( + Lardon3DProjectDb *database, uint64_t match_result_id, + uint64_t after_geometric_verification_result_id, + Lardon3DProjectDbGeometricVerificationResult *results, size_t capacity, size_t *count) { + if (count) { + *count = 0; + } + if (!database || !valid_catalog_id(match_result_id) || + after_geometric_verification_result_id > INT64_MAX || !results || !count || capacity == 0 || + capacity > LARDON3D_PROJECT_DB_GEOMETRIC_RESULT_PAGE_MAX) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + char sql[1280]; + (void)snprintf(sql, sizeof(sql), "SELECT %sFROM geometric_verification_results g " + "JOIN match_results m ON m.match_result_id=g.match_result_id " + "WHERE g.match_result_id=?1 AND " + "g.geometric_verification_result_id>?2 " + "ORDER BY g.geometric_verification_result_id LIMIT ?3", + geometric_result_columns); + Lardon3DProjectDbResult db_result = prepare(database, sql, &statement); + if (db_result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id); + (void)sqlite3_bind_int64(statement, 2, + (sqlite3_int64)after_geometric_verification_result_id); + (void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity); + int code = SQLITE_DONE; + while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) { + if (!read_geometric_result(statement, &results[*count])) { + db_result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + ++*count; + } + if (db_result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) { + db_result = sqlite_result(database, code, "list geometric results"); + } + (void)sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&database->mutex); + return db_result; +} + #ifdef LARDON3D_PROJECT_DB_TESTING Lardon3DProjectDbResult lardon3d_project_db_test_delete_feature_identity( Lardon3DProjectDb *database, uint64_t feature_set_id, uint64_t feature_asset_id) { diff --git a/tests/test_geometric_verification.c b/tests/test_geometric_verification.c new file mode 100644 index 0000000..567aadd --- /dev/null +++ b/tests/test_geometric_verification.c @@ -0,0 +1,463 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \ + return false; \ + } \ + } while (0) + +typedef struct { + uint64_t matched_id; + uint64_t no_match_id; + uint64_t second_matched_id; + uint64_t candidate_pair_id; + uint64_t feature_set_id_a; + uint64_t feature_set_id_b; +} Parents; + +static bool execute_sql(const char *path, const char *sql) { + sqlite3 *connection = NULL; + if (sqlite3_open(path, &connection) != SQLITE_OK) { + return false; + } + bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK; + return sqlite3_close(connection) == SQLITE_OK && ok; +} + +static bool query_integer(const char *path, const char *sql, sqlite3_int64 expected) { + sqlite3 *connection = NULL; + sqlite3_stmt *statement = NULL; + if (sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK || + sqlite3_prepare_v2(connection, sql, -1, &statement, NULL) != SQLITE_OK) { + if (statement) (void)sqlite3_finalize(statement); + if (connection) (void)sqlite3_close(connection); + return false; + } + bool matches = sqlite3_step(statement) == SQLITE_ROW && + sqlite3_column_int64(statement, 0) == expected && + sqlite3_step(statement) == SQLITE_DONE; + return sqlite3_finalize(statement) == SQLITE_OK && sqlite3_close(connection) == SQLITE_OK && + matches; +} + +static void asset_path(const unsigned char hash[32], const char *kind, + char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]) { + static const char digits[] = "0123456789abcdef"; + char hex[65]; + for (size_t index = 0; index < 32; ++index) { + hex[index * 2] = digits[hash[index] >> 4]; + hex[index * 2 + 1] = digits[hash[index] & 15U]; + } + hex[64] = '\0'; + (void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, "assets/%s/%c%c/%s", kind, hex[0], + hex[1], hex); +} + +static bool create_match_result(Lardon3DProjectDb *database, uint64_t pair_id, uint64_t fs_a, + uint64_t fs_b, uint32_t version, unsigned char fingerprint_byte, + int status, uint32_t match_count, uint64_t *id) { + unsigned char fingerprint[32] = {0}; + unsigned char hash[32] = {0}; + char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + memset(fingerprint, fingerprint_byte, sizeof(fingerprint)); + memset(hash, (unsigned char)(fingerprint_byte + 64U), sizeof(hash)); + asset_path(hash, "matches", path); + Lardon3DProjectDbMatchResult result; + Lardon3DProjectDbResult db_result = lardon3d_project_db_create_match_result( + database, pair_id, fs_a, fs_b, "model-test", version, fingerprint, status, match_count, + status == LARDON3D_MATCH_RESULT_STATUS_MATCHED ? hash : NULL, + status == LARDON3D_MATCH_RESULT_STATUS_MATCHED ? path : NULL, + status == LARDON3D_MATCH_RESULT_STATUS_MATCHED ? 32U + (uint64_t)match_count * 12U : 0, + version, &result); + if (db_result != LARDON3D_PROJECT_DB_OK) { + return false; + } + *id = result.match_result_id; + return true; +} + +static bool create_parents(Lardon3DProjectDb *database, Parents *parents) { + Lardon3DProjectDbScanSet scanset; + if (lardon3d_project_db_create_scanset(database, "Geometry-model", &scanset) != + LARDON3D_PROJECT_DB_OK) { + return false; + } + unsigned char image_hash_a[32] = {0x11}; + unsigned char image_hash_b[32] = {0x22}; + char image_path_a[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + char image_path_b[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + asset_path(image_hash_a, "images", image_path_a); + asset_path(image_hash_b, "images", image_path_b); + Lardon3DProjectDbImageRegisterStatus register_status; + Lardon3DProjectDbImage image_a; + Lardon3DProjectDbImage image_b; + if (lardon3d_project_db_register_image(database, scanset.scanset_id, image_hash_a, image_path_a, + 10, "a.jpg", "/a.jpg", 0, 1, ®ister_status, + &image_a) != LARDON3D_PROJECT_DB_OK || + lardon3d_project_db_register_image(database, scanset.scanset_id, image_hash_b, image_path_b, + 10, "b.jpg", "/b.jpg", 0, 2, ®ister_status, + &image_b) != LARDON3D_PROJECT_DB_OK) { + return false; + } + Lardon3DProjectDbCandidatePair pair; + if (lardon3d_project_db_create_candidate_pair(database, image_a.image_id, image_b.image_id, 3, + &pair) != LARDON3D_PROJECT_DB_OK) { + return false; + } + unsigned char feature_hash_a[32] = {0x33}; + unsigned char feature_hash_b[32] = {0x44}; + char feature_path_a[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + char feature_path_b[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + asset_path(feature_hash_a, "features", feature_path_a); + asset_path(feature_hash_b, "features", feature_path_b); + Lardon3DProjectDbFeatureSet feature_a; + Lardon3DProjectDbFeatureSet feature_b; + if (lardon3d_project_db_register_feature_set( + database, image_a.image_id, "orb", 1, feature_hash_a, image_hash_a, 8192, 1, 32, + feature_hash_a, feature_path_a, 100, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 4, + &feature_a) != LARDON3D_PROJECT_DB_OK || + lardon3d_project_db_register_feature_set( + database, image_b.image_id, "orb", 1, feature_hash_b, image_hash_b, 8192, 1, 32, + feature_hash_b, feature_path_b, 100, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 5, + &feature_b) != LARDON3D_PROJECT_DB_OK) { + return false; + } + parents->candidate_pair_id = pair.candidate_pair_id; + parents->feature_set_id_a = feature_a.feature_set_id; + parents->feature_set_id_b = feature_b.feature_set_id; + return create_match_result(database, pair.candidate_pair_id, feature_a.feature_set_id, + feature_b.feature_set_id, 1, 0x51, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 9, &parents->matched_id) && + create_match_result(database, pair.candidate_pair_id, feature_a.feature_set_id, + feature_b.feature_set_id, 2, 0x52, + LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, &parents->no_match_id) && + create_match_result(database, pair.candidate_pair_id, feature_a.feature_set_id, + feature_b.feature_set_id, 3, 0x53, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 8192, + &parents->second_matched_id); +} + +static bool create_v11_database(const char *path) { + Lardon3DProjectDb *database = NULL; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + if (lardon3d_project_db_open(path, &database, error) != LARDON3D_PROJECT_DB_OK) { + return false; + } + lardon3d_project_db_close(database); + return execute_sql(path, + "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE geometric_verification_results;" + "UPDATE metadata SET value=11 WHERE key='schema_version';" + "COMMIT;PRAGMA foreign_keys=ON;"); +} + +static bool test_model_api(const char *path) { + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + Lardon3DProjectDb *database = NULL; + CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_schema_version(database) == 12); + Parents parents; + CHECK(create_parents(database, &parents)); + + unsigned char fingerprint_a[32]; + unsigned char fingerprint_b[32]; + memset(fingerprint_a, 0xa1, sizeof(fingerprint_a)); + memset(fingerprint_b, 0xb2, sizeof(fingerprint_b)); + unsigned char mask_a[2] = {0x01, 0x00}; + double model[9] = {0.0, -1.0, 1e-300, 1e300, 4.0, 5.0, 6.0, 7.0, 8.0}; + Lardon3DProjectDbGeometricVerificationResult verified; + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, + fingerprint_a, LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), model, 10, + &verified) == LARDON3D_PROJECT_DB_OK); + CHECK(verified.has_model && verified.inlier_mask_size == 2 && + memcmp(verified.model, model, sizeof(model)) == 0); + + unsigned char rejected_mask[2] = {0x03, 0x00}; + Lardon3DProjectDbGeometricVerificationResult rejected; + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 2, + fingerprint_b, LARDON3D_GEOMETRIC_REJECTED, 2, rejected_mask, + sizeof(rejected_mask), NULL, 11, &rejected) == LARDON3D_PROJECT_DB_OK); + CHECK(!rejected.has_model && rejected.inlier_count == 2); + + unsigned char zero_mask[2] = {0}; + Lardon3DProjectDbGeometricVerificationResult zero_inliers; + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, + fingerprint_b, LARDON3D_GEOMETRIC_REJECTED, 0, zero_mask, sizeof(zero_mask), NULL, 11, + &zero_inliers) == LARDON3D_PROJECT_DB_OK); + + Lardon3DProjectDbGeometricVerificationResult loaded; + CHECK(lardon3d_project_db_load_geometric_verification_result( + database, verified.geometric_verification_result_id, &loaded) == + LARDON3D_PROJECT_DB_OK); + CHECK(loaded.match_result_id == parents.matched_id && loaded.has_model && + memcmp(loaded.model, model, sizeof(model)) == 0 && + memcmp(loaded.inlier_mask, mask_a, sizeof(mask_a)) == 0); + CHECK(lardon3d_project_db_find_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, + fingerprint_a, &loaded) == LARDON3D_PROJECT_DB_OK && + loaded.geometric_verification_result_id == verified.geometric_verification_result_id); + + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, + fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 2, rejected_mask, + sizeof(rejected_mask), NULL, 12, &loaded) == LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.no_match_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, + fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 0, mask_a, 1, NULL, 12, + &loaded) == LARDON3D_PROJECT_DB_CONSTRAINT); + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, UINT64_C(9223372036854775807), + LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, fingerprint_a, + LARDON3D_GEOMETRIC_REJECTED, 0, mask_a, 1, NULL, 12, + &loaded) == LARDON3D_PROJECT_DB_NOT_FOUND); + + unsigned char bad_padding[2] = {0x01, 0x80}; + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, + fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 1, bad_padding, sizeof(bad_padding), + NULL, 12, &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, + fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 2, mask_a, sizeof(mask_a), NULL, 12, + &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + unsigned char all_inliers[2] = {0xff, 0x01}; + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 4, + fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 9, all_inliers, sizeof(all_inliers), NULL, + 12, &loaded) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 5, + fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 10, all_inliers, sizeof(all_inliers), NULL, + 12, &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, + fingerprint_a, LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), NULL, 12, + &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, + fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 1, mask_a, sizeof(mask_a), model, 12, + &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + double bad_model[9] = {0}; + bad_model[4] = NAN; + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, + fingerprint_a, LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), bad_model, 12, + &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + bad_model[4] = INFINITY; + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3, + fingerprint_a, LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), bad_model, 12, + &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + unsigned char max_mask[LARDON3D_PROJECT_DB_INLIER_MASK_MAX] = {0}; + max_mask[0] = 0x01; + max_mask[1023] = 0x80; + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.second_matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, + fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 2, max_mask, sizeof(max_mask), NULL, 13, + &loaded) == LARDON3D_PROJECT_DB_OK); + CHECK(loaded.inlier_mask_size == 1024 && loaded.inlier_count == 2); + + static const uint32_t boundary_counts[] = {1, 2, 7, 8, 9, 63, 64, 65, 8191}; + for (size_t boundary_index = 0; + boundary_index < sizeof(boundary_counts) / sizeof(boundary_counts[0]); ++boundary_index) { + uint32_t match_count = boundary_counts[boundary_index]; + uint64_t parent_id = 0; + CHECK(create_match_result(database, parents.candidate_pair_id, parents.feature_set_id_a, + parents.feature_set_id_b, (uint32_t)(10U + boundary_index), + (unsigned char)(0x60U + boundary_index), + LARDON3D_MATCH_RESULT_STATUS_MATCHED, match_count, &parent_id)); + unsigned char boundary_mask[LARDON3D_PROJECT_DB_INLIER_MASK_MAX] = {0}; + size_t boundary_size = ((size_t)match_count + 7U) / 8U; + uint32_t last_index = match_count - 1U; + boundary_mask[last_index / 8U] = (unsigned char)(1U << (last_index % 8U)); + unsigned char boundary_fingerprint[32]; + memset(boundary_fingerprint, (int)(0xc0U + boundary_index), + sizeof(boundary_fingerprint)); + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parent_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, + boundary_fingerprint, LARDON3D_GEOMETRIC_REJECTED, 1, boundary_mask, + boundary_size, NULL, 20 + (int64_t)boundary_index, &loaded) == + LARDON3D_PROJECT_DB_OK); + CHECK(loaded.inlier_mask_size == boundary_size && + loaded.inlier_mask[last_index / 8U] == (unsigned char)(1U << (last_index % 8U))); + } + + Lardon3DProjectDbGeometricVerificationResult page[2]; + size_t count = 0; + CHECK(lardon3d_project_db_list_geometric_verification_results( + database, parents.matched_id, 0, page, 1, &count) == LARDON3D_PROJECT_DB_OK && + count == 1); + uint64_t cursor = page[0].geometric_verification_result_id; + CHECK(lardon3d_project_db_list_geometric_verification_results( + database, parents.matched_id, cursor, page, 2, &count) == LARDON3D_PROJECT_DB_OK && + count == 2 && page[0].geometric_verification_result_id > cursor); + CHECK(lardon3d_project_db_list_geometric_verification_results( + database, parents.matched_id, 0, page, + LARDON3D_PROJECT_DB_GEOMETRIC_RESULT_PAGE_MAX + 1U, &count) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + uint64_t verified_id = verified.geometric_verification_result_id; + lardon3d_project_db_close(database); + database = NULL; + CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_geometric_verification_result(database, verified_id, &loaded) == + LARDON3D_PROJECT_DB_OK && memcmp(loaded.model, model, sizeof(model)) == 0); + lardon3d_project_db_close(database); + return true; +} + +static bool loader_rejects_after_sql(const char *path, const char *sql) { + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + Lardon3DProjectDb *database = NULL; + if (!execute_sql(path, sql) || + lardon3d_project_db_open(path, &database, error) != LARDON3D_PROJECT_DB_OK) { + return false; + } + Lardon3DProjectDbGeometricVerificationResult result; + bool rejected = lardon3d_project_db_load_geometric_verification_result(database, 1, &result) == + LARDON3D_PROJECT_DB_CORRUPT; + lardon3d_project_db_close(database); + return rejected; +} + +static bool test_corruption(const char *path) { + static const char reset[] = + "PRAGMA ignore_check_constraints=ON;" + "UPDATE geometric_verification_results SET verifier_kind=1,verifier_version=1," + "parameter_fingerprint=zeroblob(32),status=2,inlier_count=1,inlier_mask=x'0100'," + "model_m00=0,model_m01=0,model_m02=0,model_m10=0,model_m11=0,model_m12=0," + "model_m20=0,model_m21=0,model_m22=0 WHERE geometric_verification_result_id=1;"; + CHECK(execute_sql(path, reset)); + + static const char *corruptions[] = { + "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "SET verifier_kind=9 WHERE geometric_verification_result_id=1;", + "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "SET verifier_kind=1,verifier_version=0 WHERE geometric_verification_result_id=1;", + "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "SET verifier_version=1,parameter_fingerprint=x'01' " + "WHERE geometric_verification_result_id=1;", + "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "SET parameter_fingerprint=zeroblob(32),status=9 " + "WHERE geometric_verification_result_id=1;", + "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "SET status=2,inlier_count=9000 WHERE geometric_verification_result_id=1;", + "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "SET inlier_count=1,inlier_mask=x'03' WHERE geometric_verification_result_id=1;", + "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "SET inlier_mask=x'0180' WHERE geometric_verification_result_id=1;", + "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "SET inlier_mask=x'0300' WHERE geometric_verification_result_id=1;", + "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "SET inlier_mask=x'0100',model_m00=NULL WHERE geometric_verification_result_id=1;", + "PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results " + "SET model_m00=1e999 WHERE geometric_verification_result_id=1;", + }; + for (size_t index = 0; index < sizeof(corruptions) / sizeof(corruptions[0]); ++index) { + CHECK(execute_sql(path, reset)); + CHECK(loader_rejects_after_sql(path, corruptions[index])); + } + + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + Lardon3DProjectDb *database = NULL; + CHECK(execute_sql(path, reset)); + CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); + Lardon3DProjectDbGeometricVerificationResult result; + CHECK(lardon3d_project_db_load_geometric_verification_result(database, 1, &result) == + LARDON3D_PROJECT_DB_OK); + uint64_t parent_id = result.match_result_id; + lardon3d_project_db_close(database); + char parent_corruption[512]; + CHECK(snprintf(parent_corruption, sizeof(parent_corruption), + "PRAGMA ignore_check_constraints=ON;UPDATE match_results SET result_status=0 " + "WHERE match_result_id=%llu;", + (unsigned long long)parent_id) > 0); + CHECK(loader_rejects_after_sql(path, parent_corruption)); + return true; +} + +static bool test_migration(const char *v11_path, const char *failed_path) { + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + Lardon3DProjectDb *database = NULL; + CHECK(create_v11_database(v11_path)); + CHECK(query_integer(v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 11)); + CHECK(query_integer(v11_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='geometric_verification_results'", + 0)); + CHECK(lardon3d_project_db_open(v11_path, &database, error) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_schema_version(database) == 12); + Parents parents; + CHECK(create_parents(database, &parents)); + unsigned char fingerprint[32] = {0x91}; + unsigned char mask[2] = {0x01, 0x00}; + Lardon3DProjectDbGeometricVerificationResult migrated_result; + CHECK(lardon3d_project_db_create_geometric_verification_result( + database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, + fingerprint, LARDON3D_GEOMETRIC_REJECTED, 1, mask, sizeof(mask), NULL, 30, + &migrated_result) == LARDON3D_PROJECT_DB_OK); + uint64_t migrated_result_id = migrated_result.geometric_verification_result_id; + lardon3d_project_db_close(database); + CHECK(query_integer(v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); + CHECK(query_integer(v11_path, + "SELECT count(*) FROM sqlite_master WHERE type='index' AND " + "name='geometric_verification_results_parent_idx'", + 1)); + CHECK(lardon3d_project_db_open(v11_path, &database, error) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_load_geometric_verification_result( + database, migrated_result_id, &migrated_result) == LARDON3D_PROJECT_DB_OK && + migrated_result.match_result_id == parents.matched_id); + lardon3d_project_db_close(database); + + CHECK(create_v11_database(failed_path)); + CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V12", "1", 1) == 0); + CHECK(lardon3d_project_db_open(failed_path, &database, error) == LARDON3D_PROJECT_DB_IO_ERROR); + CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V12") == 0); + CHECK(query_integer(failed_path, "SELECT value FROM metadata WHERE key='schema_version'", 11)); + CHECK(query_integer(failed_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='geometric_verification_results'", + 0)); + CHECK(lardon3d_project_db_open(failed_path, &database, error) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_schema_version(database) == 12); + lardon3d_project_db_close(database); + return true; +} + +static bool run_test(void) { + char directory[] = "/tmp/lardon3d-geometric-model-XXXXXX"; + CHECK(mkdtemp(directory)); + char database_path[512]; + char v11_path[512]; + char failed_path[512]; + CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0); + CHECK(snprintf(v11_path, sizeof(v11_path), "%s/v11.db", directory) > 0); + CHECK(snprintf(failed_path, sizeof(failed_path), "%s/failed-v12.db", directory) > 0); + CHECK(test_model_api(database_path)); + CHECK(test_corruption(database_path)); + CHECK(test_migration(v11_path, failed_path)); + CHECK(unlink(database_path) == 0); + CHECK(unlink(v11_path) == 0); + CHECK(unlink(failed_path) == 0); + CHECK(rmdir(directory) == 0); + return true; +} + +int main(void) { + return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_match_result.c b/tests/test_match_result.c index a8ca46b..a9d8e16 100644 --- a/tests/test_match_result.c +++ b/tests/test_match_result.c @@ -48,6 +48,7 @@ static bool create_v9_database(const char *path) { if (sqlite3_open(path, &connection) != SQLITE_OK) return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE geometric_verification_results;" "DROP TABLE matcher_tasks;" "DROP TABLE match_results;" "UPDATE metadata SET value=9 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;"; @@ -93,7 +94,7 @@ static bool run_test(void) { 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) == 11); + CHECK(database && lardon3d_project_db_schema_version(database) == 12); Lardon3DProjectDbScanSet scanset; CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) == @@ -452,7 +453,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) == 11); + CHECK(lardon3d_project_db_schema_version(database) == 12); /* Verify persistence: load previously created results */ CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) == @@ -492,10 +493,10 @@ static bool run_test(void) { CHECK(create_v9_database(v9_path)); CHECK(query_integer(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) == 11); + CHECK(lardon3d_project_db_schema_version(database) == 12); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 11)); + CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(query_integer(v9_path, "SELECT count(*) FROM sqlite_master WHERE type='table' AND " "name='match_results'", 1)); @@ -514,7 +515,7 @@ static bool run_test(void) { "SELECT count(*) FROM sqlite_master WHERE type='table' AND " "name='matcher_tasks'", 0)); CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 11); + lardon3d_project_db_schema_version(database) == 12); lardon3d_project_db_close(database); database = NULL; diff --git a/tests/test_matcher_task.c b/tests/test_matcher_task.c index 5f05d00..f46626d 100644 --- a/tests/test_matcher_task.c +++ b/tests/test_matcher_task.c @@ -89,6 +89,7 @@ static bool downgrade_project_to_historical_v10(const char *database_path) { } static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE geometric_verification_results;" "DROP TABLE matcher_tasks;" "UPDATE metadata SET value=10 WHERE key='schema_version';" "COMMIT;PRAGMA foreign_keys=ON;"; @@ -350,7 +351,7 @@ static bool run_test(void) { "name='matcher_tasks'", 0)); CHECK(reopen_runtime(&fixture)); - CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 11); + CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 12); CHECK(query_integer(database_path, "SELECT count(*) FROM sqlite_master WHERE type='table' AND " "name='matcher_tasks'", diff --git a/tests/test_project_db.c b/tests/test_project_db.c index 155ff00..49bac46 100644 --- a/tests/test_project_db.c +++ b/tests/test_project_db.c @@ -89,7 +89,7 @@ static bool create_future_database(const char *path) { } bool ok = sqlite3_exec(connection, "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" - "INSERT INTO metadata VALUES('schema_version',12);", + "INSERT INTO metadata VALUES('schema_version',13);", NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; } @@ -103,6 +103,7 @@ static bool create_v7_database(const char *path) { if (sqlite3_open(path, &connection) != SQLITE_OK) return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE geometric_verification_results;" "DROP TABLE matcher_tasks;" "DROP TABLE match_results;" "DROP TABLE candidate_pair_generate_tasks;" @@ -121,6 +122,7 @@ static bool create_v6_database(const char *path) { if (sqlite3_open(path, &connection) != SQLITE_OK) return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE geometric_verification_results;" "DROP TABLE matcher_tasks;" "DROP TABLE match_results;" "DROP TABLE candidate_pair_generate_tasks;" @@ -149,6 +151,7 @@ static bool create_v10_database(const char *path) { } static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE geometric_verification_results;" "DROP TABLE matcher_tasks;" "UPDATE metadata SET value=10 WHERE key='schema_version';" "COMMIT;PRAGMA foreign_keys=ON;"; @@ -335,7 +338,7 @@ static bool run_test(void) { 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) == 11); + CHECK(database && lardon3d_project_db_schema_version(database) == 12); bool legacy_pending = true; CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) == LARDON3D_PROJECT_DB_OK && @@ -602,7 +605,7 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_INVALID_ARGUMENT); lardon3d_project_db_close(contexts[0].database); database = NULL; - CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 11)); + CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(query_integer(database_path, "SELECT count(*) FROM tasks WHERE task_id=1", 1)); CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK); @@ -624,7 +627,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) == 11); + CHECK(lardon3d_project_db_schema_version(database) == 12); CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK && strcmp(loaded_project.stable_id, "legacy-project") == 0); CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK); @@ -634,7 +637,7 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_OK); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 11)); + CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(create_v1_database(failed_migration_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0); @@ -659,7 +662,7 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_OK); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 11)); + CHECK(query_integer(v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(create_v2_database(failed_v3_migration_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0); @@ -687,7 +690,7 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_OK); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 11)); + CHECK(query_integer(v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(create_v3_database(failed_v4_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0); @@ -704,7 +707,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) == 11); + CHECK(lardon3d_project_db_schema_version(database) == 12); CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) == LARDON3D_PROJECT_DB_OK); @@ -743,24 +746,24 @@ static bool run_test(void) { fprintf(stderr, "Nouvelle tentative migration v7 (%d): %s\n", (int)retry_v7, error); } CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 11); + lardon3d_project_db_schema_version(database) == 12); lardon3d_project_db_close(database); database = NULL; CHECK(create_v5_database(direct_v5_path)); CHECK(query_integer(direct_v5_path, "SELECT value FROM metadata WHERE key='schema_version'", 5)); CHECK(lardon3d_project_db_open(direct_v5_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 11); + lardon3d_project_db_schema_version(database) == 12); lardon3d_project_db_close(database); database = NULL; CHECK(create_v7_database(v8_path)); CHECK(query_integer(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) == 11); + CHECK(lardon3d_project_db_schema_version(database) == 12); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 11)); + CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(create_v7_database(failed_v8_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0); @@ -771,7 +774,7 @@ static bool run_test(void) { "SELECT count(*) FROM sqlite_master WHERE type='table' AND " "name='candidate_pairs'", 0)); CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 11); + lardon3d_project_db_schema_version(database) == 12); lardon3d_project_db_close(database); database = NULL; @@ -786,10 +789,10 @@ static bool run_test(void) { "name='matcher_tasks'", 0)); CHECK(lardon3d_project_db_open(v10_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 11); + lardon3d_project_db_schema_version(database) == 12); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 11)); + CHECK(query_integer(v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(query_integer(v10_path, "SELECT count(*) FROM sqlite_master WHERE type='table' AND " "name='matcher_tasks'", @@ -812,7 +815,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) == 11); + CHECK(lardon3d_project_db_schema_version(database) == 12); lardon3d_project_db_close(database); database = NULL;