feat(reconstruction): add durable geometric verifier

This commit is contained in:
fy59 2026-08-09 15:28:22 +02:00
parent 558f00cd6f
commit 70a5a18ad6
25 changed files with 3166 additions and 81 deletions

View file

@ -14,7 +14,9 @@ Lardon3D est un moteur de photogrammétrie Linux qui privilégie :
- **Traçabilité** : historique des opérations et métriques - **Traçabilité** : historique des opérations et métriques
- **Enrichissement progressif** : reconstruction incrémentale - **Enrichissement progressif** : reconstruction incrémentale
Lardon3D ne vise pas simplement "dossier de photos → objet 3D", mais "ensemble progressif d'observations et de contraintes → reconstruction géométrique persistante, enrichissable et versionnable". Lardon3D ne vise pas simplement "dossier de photos → objet 3D", mais un ensemble
progressif d'observations et de contraintes donnant une reconstruction géométrique
persistante, enrichissable et versionnable.
## État actuel ## État actuel
@ -27,8 +29,9 @@ Lardon3D ne vise pas simplement "dossier de photos → objet 3D", mais "ensemble
- **Image View** : vues triées et filtrées pour la TUI - **Image View** : vues triées et filtrées pour la TUI
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences - **Task** : moteur de tâches avec pause/reprise, annulation et séquences
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre - **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
- **Project Database v12** : tâches, catalogue, matching et résultats géométriques durables - **Project Database v13** : résultats géométriques et tâche Geometric Verifier durables
- **Geometric Verification Model v1** : identité, masque d'inliers et modèle 3×3 persistants - **Geometric Verification Model v1** : identité, masque d'inliers et modèle 3×3 persistants
- **Geometric Verifier v1** : Fundamental USAC/MAGSAC, reprise et lots resource-aware
- **Task Kind Registry** : identité métier durable et reconstruction runtime explicite - **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 - **Recovery projet** : reprise automatique sélective et bornée des imports récupérables
- **Task Queue** : file FIFO avec sélection adaptative et backpressure - **Task Queue** : file FIFO avec sélection adaptative et backpressure
@ -113,6 +116,7 @@ Acquisitions
- [Match Result](docs/architecture/match_result.md) - [Match Result](docs/architecture/match_result.md)
- [Matcher](docs/architecture/matcher.md) - [Matcher](docs/architecture/matcher.md)
- [Geometric Verification](docs/architecture/geometric_verification.md) - [Geometric Verification](docs/architecture/geometric_verification.md)
- [Geometric Verifier](docs/architecture/geometric_verifier.md)
- [Backend Vulkan ORB](docs/architecture/vulkan_matcher.md) - [Backend Vulkan ORB](docs/architecture/vulkan_matcher.md)
- [Viewer](docs/architecture/viewer.md) - [Viewer](docs/architecture/viewer.md)
- [Revue des fondations](docs/architecture/foundation_review.md) - [Revue des fondations](docs/architecture/foundation_review.md)
@ -154,12 +158,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, Lardon3D est en développement actif. La persistance des tâches, le catalogue,
le Feature Store multipasse, le Visual Index ORB, Candidate Pair Generator le Feature Store multipasse, le Visual Index ORB, Candidate Pair Generator
et Matcher v1 sont implémentés. Geometric Verification Model v1 est implémenté ; Matcher v1, Geometric Verification Model v1 et Geometric Verifier Fundamental v1
le Geometric Verifier reste planifié. Le runtime Feature + Matcher emploie des tâches sont implémentés. Le runtime Feature + Matcher + Verifier emploie des tâches durables,
durables, de petits lots, le Resource Governor interactif et un hot path Vulkan de petits lots, le Resource Governor interactif et un hot path Vulkan ORB exact avec
ORB exact avec fallback CPU. La feasibility Vulkan SIFT/RootSIFT a été rejetée ; fallback CPU. La feasibility Vulkan SIFT/RootSIFT a été rejetée ; ces deux matchers
ces deux matchers restent sur OpenCV L2. DAG générique, restent sur OpenCV L2. DAG générique, Tracks, SfM et viewer restent des tickets
calcul de vérification géométrique, SfM et viewer restent des tickets séparés planifiés. séparés planifiés.
## Licence ## Licence

View file

@ -0,0 +1,301 @@
# Geometric Verifier v1
## Scope
Ce document décrit l'exécution scientifique qui transforme un Match Result `MATCHED` en résultat
Fundamental `GEOMETRIC_REJECTED` ou `GEOMETRIC_VERIFIED`. Le contrat persistant reste défini par
[`geometric_verification.md`](geometric_verification.md). Tracks, pose, Essential, compétition
Homography, triangulation et SfM sont hors périmètre.
## Inputs
Le parent DB fournit les deux Feature Set IDs, le compte, le chemin, la taille et le SHA-256 du
Match File. Le reader Feature Store ouvre séparément chaque Feature Set validé et expose les
keypoints par plages d'au plus 256. Le verifier n'a besoin d'aucun descriptor : charger les blocs
ORB ou SIFT/RootSIFT serait inutile et est interdit dans le chemin normal.
Les keypoints persistants portent des coordonnées `binary32`. `x/y` sont exprimés en pixels de
l'image exactement décodée par OpenCV lors de l'extraction, avec origine en haut à gauche et
positions subpixel possibles. Les dimensions décodées sont disponibles dans les métadonnées du
Feature File.
## Fundamental matrix contract
Le seul modèle v1 est une matrice Fundamental 3×3. Une sortie acceptée doit être unique, finie,
de norme non nulle et canonique avant publication. V1 ne projette pas la matrice vers le rang 2.
## Input ordering
L'entrée `i` de l'estimator correspond exactement à l'entrée `i` du Match File :
`feature_index_a` sélectionne le Feature Set A et `feature_index_b` le Feature Set B. Le Match
File impose déjà des indices A strictement croissants ; le verifier ne trie et ne filtre pas les
correspondances. Toute corruption d'index est une erreur d'exécution, jamais un rejet scientifique.
## Coordinate representation
Le stockage source reste `binary32`. Sur 1024 points, bruit 0,75 px et 50 % d'outliers, Point2f et
Point2d ont produit le même masque et la même qualité, en 3,58 et 3,55 ms. La production convertit
vers Point2d pour rendre le calcul et la sortie binary64 explicites, pour 256 Kio au maximum.
Aucune mise à l'échelle par résolution ni conversion de repère n'est appliquée implicitement.
## Algorithm candidates
OpenCV 5 installé expose `FM_RANSAC`, `USAC_DEFAULT`, `USAC_ACCURATE`, `USAC_PROSAC` et
`USAC_MAGSAC`. La shortlist Gate A est FM_RANSAC comme baseline, puis USAC_DEFAULT,
USAC_MAGSAC et USAC_ACCURATE. PROSAC est `NOT_APPLICABLE` en v1 : la distance descriptor est
persistée mais l'ordre canonique suit l'index de query, pas un classement de qualité benchmarké.
## Benchmark methodology
Un corpus synthétique déterministe avec Fundamental ground truth couvrira bruit, outliers,
résolutions, tailles, géométries saines, faibles et adversariales. Les méthodes seront comparées
par précision/recall du masque, erreurs épipolaires, échecs, repeatability, temps et ressources.
Le benchmark lourd restera hors build et suite par défaut. Aucune fixture photo réelle ne sera
revendiquée sans fixture non sensible présente dans le dépôt.
La campagne Gate A du 9 août 2026 utilise OpenCV 5.0.0, Clang 22.1.8, une seed fixe et 32
répétitions. Elle couvre 7 à 8192 points, 0 à 100 % d'outliers, bruit 0 à 1,5 px, 1280×720 à
4000×3000, baseline faible/large, concentration, quasi-colinéarité, planéité, rotation dominante
et duplications. Aucune fixture photo réelle représentative n'existe dans le dépôt.
| Algorithme | P/R 1024, 30 % | P/R 8192, 70 % | Médiane/p95/pire 8192 | Seed locale | Stable 32× |
|---|---:|---:|---:|---|---|
| FM_RANSAC | 0,998/0,720 | 0,993/0,413 | 316,4/318,9/319,7 ms | non | oui observé |
| USAC_DEFAULT | 0,996/0,960 | 0,997/0,959 | 43,0/43,2/44,8 ms | preset non | oui |
| USAC_MAGSAC | 0,993/0,965 | 0,994/0,962 | 11,4/12,0/12,1 ms | preset non | oui |
| USAC_ACCURATE | 0,996/0,960 | 0,996/0,961 | 30,5/32,0/32,1 ms | preset non | oui |
| MAGSAC params v1 | 0,997/0,957 | 0,996/0,894 | 10,8/11,0/11,4 ms | oui | oui |
FM_RANSAC est rejeté pour son recall et son pire temps. DEFAULT et ACCURATE n'améliorent pas assez
la qualité pour leur coût. La production emploie des UsacParams explicites : la seed par appel
prime sur la variation du cas extrême liée à la seed fixe. À bruit 0,75 px/50 % d'outliers, les
seuils 0,5/1,0/1,5/2,0/3,0 donnent des recalls 0,535/0,811/0,961/0,990/1,000 et des precisions
0,996/0,988/0,990/0,986/0,985. Le compromis retenu est 1,5 px.
## Determinism
USAC expose `cv::UsacParams::randomGeneratorState`, un entier par appel, ainsi que les paramètres
de sampling, score, optimisation locale et polishing. Cette API est préférable à une mutation de
`cv::theRNG()` process-global. FM_RANSAC restera une baseline scientifique tant que son contrôle
RNG et sa repeatability n'ont pas été mesurés.
Les cinq candidats ont donné un hash modèle+masque identique sur 32 appels et dans trois processus
distincts. La garantie v1 reste intra-environnement : mêmes octets, ordre, configuration, seed,
OpenCV 5.0.0 et architecture. Aucun bit-exact cross-version ou cross-architecture n'est promis.
## Random seed policy
La policy v1 calcule SHA-256 sur `L3DGVSE1`, le SHA-256 du Match File puis le fingerprint. Les
quatre premiers octets sont décodés little-endian et les 31 bits faibles alimentent
`randomGeneratorState`. La policy est version 1.
## Parameter fingerprint
Le fingerprint v1 est SHA-256 des 84 octets suivants. Les entiers sont little-endian ; les doubles
sont leurs bits IEEE-754 binary64 écrits comme `uint64_t` little-endian. NaN/Inf sont refusés et
le seul champ autorisant zéro signé, `min_inlier_ratio`, normalise `-0.0` en `+0.0`. Aucun octet ne
provient d'un dump de structure.
| Offset | Taille | Champ |
|---:|---:|---|
| 0 | 8 | domaine ASCII `L3DGVFP1` |
| 8 | 4 | version encodage = 1 |
| 12 | 4 | kind FUNDAMENTAL = 1 |
| 16 | 4 | verifier version = 1 |
| 20 | 4 | algorithme USAC_MAGSAC explicite = 1 |
| 24 | 8 | threshold binary64 |
| 32 | 8 | confidence binary64 |
| 40 | 4 | max iterations |
| 44 | 4 | minimum inlier count |
| 48 | 8 | minimum inlier ratio binary64 |
| 56 | 4 | seed policy version |
| 60 | 4 | canonicalisation version |
| 64 | 1 | représentation Point2d = 2 |
| 65 | 1 | sampler uniforme = 0 |
| 66 | 1 | score MAGSAC = 2 |
| 67 | 1 | isParallel = 0 |
| 68 | 1 | LO inner = 1 |
| 69 | 4 | LO iterations = 5 |
| 73 | 4 | LO sample size = 14 |
| 77 | 1 | neighbor grid = 1 |
| 78 | 1 | COV polisher = 3 |
| 79 | 4 | polisher iterations = 3 |
| 83 | 1 | réservé nul |
Le vector golden de la configuration production commence par les 84 octets hexadécimaux
`4c33444756465031...0300000000` et donne le SHA-256
`ddb44bb070c62be66c405946e89cbb49c084f8f30a21d6f408dc239225b7bbd0`. Pour un Match File SHA
composé de 31 octets nuls puis `01`, cette configuration donne la seed décimale `1910542150`.
Les politiques de
ressources, hardware, PSI, lot, worker et réservation CPU ne sont ni des champs ni des entrées.
## Acceptance policy
Un modèle candidat qui échoue à cette policy publie REJECTED avec son masque et son compte
d'inliers, sans matrice. La production exige `inlier_count >= 16` et
`inlier_count / match_count >= 0,20`. Les cas 100 % faux produisent 10/64, 14/256, 26/1024 et
28/4096 inliers, ratio maximal 0,15625. Les scènes saines produisent 45/64, 129/256 et 297/1024 ;
la faible baseline produit 126/256.
## Fundamental matrix canonicalization
La production adopte cette canonicalisation version 1. Les neuf valeurs doivent être finies. La
norme de Frobenius est calculée avec une accumulation `hypot` résistante au débordement ; zéro est
refusé. Le premier coefficient de valeur absolue strictement maximale gagne, donc un tie conserve
le plus petit index ligne-major. Après division, le signe rend ce pivot positif et les zéros signés
sont normalisés à `+0.0`. Sur 8192/70 %, les singular values sont
3,392e-2, 1,374e-4 et 5,915e-24. OpenCV fournit déjà rank-2 à précision numérique. V1 ne calcule
aucune SVD en production, n'impose aucun seuil de rang et n'effectue aucune post-projection rank-2.
Les validations production portent uniquement sur la forme 3×3 unique, la finitude et la norme.
## Inlier mask generation
Le masque OpenCV est validé en type, taille et valeurs, puis converti sans réordonnancement vers
le bitset LSB-first du modèle. Les frontières 7/8/9, 63/64/65 et 8191/8192 sont testées.
Le core conserve strictement l'ordre d'entrée du Match File. Les tests utilisent des indices B
permutés et des masques non contigus ; le bit `i` publié reste l'élément `i` du fichier, jamais
l'index de feature. Les tailles 1, 2, 7, 8, 9, 63, 64, 65, 8191 et 8192, le padding nul et le
popcount sont couverts avec le Model v1 inchangé.
## Scientific rejection
Un nombre de matches inférieur au minimum réel, l'absence de modèle sur entrée valide ou l'échec
de l'acceptance policy produit un résultat scientifique REJECTED cohérent.
Le minimum USAC observé est sept. Moins de sept matches produit un masque zéro sans appel OpenCV ;
sept à quinze peuvent produire une hypothèse mais ne franchissent pas le support production.
## Execution failure
Match/Feature asset absent ou corrompu, index hors bornes, exception OpenCV, OOM, masque malformé,
matrice non finie ou invariant interne invalide échoue dans le Task Runtime. Aucun résultat
scientifique n'est publié dans ces cas.
Le core traduit parent absent/NO_MATCH et Feature Set absent en erreur d'exécution `NOT_FOUND` ;
asset absent, tronqué, hash divergent, ownership ou index incohérent en `CORRUPT` ; exception ou
sortie estimator malformée/non finie en `ESTIMATOR_ERROR` ; `bad_alloc` en `OUT_OF_MEMORY` ; et
échec Model en `DATABASE_ERROR`. Les seams test-only couvrent erreur estimator, mask/matrice
malformés, NaN, OOM et publication. Aucun de ces chemins ne crée de résultat scientifique.
## Resource bounds
Une unité atomique est un Match Result, au maximum 8192 correspondances. Le Match File est borné
à 98 336 octets et le bitset à 1024 octets. Aucun cache global ni préchargement de projet complet
n'est utilisé.
À 8192 matches, les allocations directement contrôlées maximales sont 98 304 octets d'entries,
393 216 octets de keypoints A/B, 262 144 octets de Point2d A/B, 1024 octets de bitset, environ
8192 octets de mask OpenCV et 72 octets de modèle, soit environ 745 Kio hors petits objets et
scratch OpenCV. Aucun descriptor ni matrice A×B n'est lu. Massif mesure 2,445 Mio de heap au pic
du test E2E complet, incluant SQLite, OpenCV, fixtures Feature Store et toutes les séquences de test.
Une réservation conservatrice de 4 Mio par job couvre ce profil mesuré.
## CPU policy
Le parallélisme OpenCV reste configuré process-wide. Le benchmark mesurera les threads réellement
consommés ; le verifier ne change pas `cv::setNumThreads()` par paire.
La campagne a consommé environ 99 % d'un CPU logique : réservation v1 d'un thread et un worker.
## GPU policy
Aucun backend Vulkan n'est implémenté avant profil du chemin CPU final. La décision attendue est
`NOT_JUSTIFIED` si les unités restent sub-millisecondes ou de quelques millisecondes.
Verdict Gate A : `NOT_JUSTIFIED`. Les cas usuels prennent 0,3 à 5,6 ms et le pire MAGSAC local
mesuré reste à 11,4 ms. Aucun backend Vulkan de vérification n'est implémenté.
## Task Runtime
L'audit Gate C conclut que le checkpoint générique v1 est insuffisant : il conserve l'état,
la progression, le compteur de séquences et les temps, mais aucun payload propre au kind. Le
reconstructeur doit retrouver les paramètres scientifiques immuables et le curseur sans les
inventer depuis un fingerprint irréversible.
`geometric_verifier_tasks` est donc la seule raison de Project DB v13. Elle doit conserver
`task_id`, `after_match_result_id`, les sept paramètres de configuration v1 et le fingerprint
calculé à la création pour validation à la reconstruction. Aucun `cv::Mat`, buffer, état RNG,
paramètre Governor ou donnée hardware n'y appartient. La tâche calcule hors transaction et publie
chaque résultat par transaction courte avant avancement du curseur.
Le Task Kind production est `geometric_verifier.run` version 1. Il pagine les Match Results par ID
strictement croissant avec une page de `batch + 1`, et traite des lots Governor 1/2/4/8. Les
parents autres que `MATCHED` avec `match_count > 0` sont seulement traversés par le curseur. Une
unité éligible appelle le core, qui reuse l'identité exacte avant toute lecture d'asset.
WHY GENERIC TASK PERSISTENCE IS INSUFFICIENT: aucun champ de payload métier dans le snapshot v1.
REQUIRED DURABLE FIELDS: configuration scientifique v1, fingerprint et dernier Match Result
publié puis checkpointé.
WHY EXISTING DB CANNOT STORE THEM: `tasks` et `checkpoints` ne portent que le résumé générique ;
aucune table v12 ne possède une ligne 1:1 adaptée à ce Task Kind.
## Checkpoint/recovery
La pagination suit `match_result_id` croissant sans supposer des IDs contigus. Le résultat est
publié avant que `after_match_result_id` avance en mémoire ; le curseur n'est persisté qu'après le
lot. Après chaque lot non terminal, `task_sequence_break()` rend la réservation au Governor.
Le test de crash publie puis interrompt avant checkpoint du curseur, ferme runtime et DB, recharge
le checkpoint antérieur et reconstruit le Task Kind. Le parent est revu, son résultat exact est
réutilisé, puis le curseur progresse.
## Cancellation
Pause et annulation sont coopératives avant chaque parent et entre lots. Une petite estimation
OpenCV engagée finit et publie avant l'arrêt ; aucun résultat scientifique CANCELLED n'est créé.
Les résultats déjà publiés restent durables.
## Backend policy
Un backend n'est transparent pour l'identité que si ses sorties scientifiques sont équivalentes
selon le contrat. V1 possède une seule implémentation CPU de production.
## Core publication and reuse
Le core charge les métadonnées DB, relâche les mutex internes après chaque API, lit les assets et
calcule sans transaction longue, puis appelle une publication Model v1 courte. Une identité exacte
VERIFIED ou REJECTED est retournée avant toute lecture Feature/Match et sans appel estimator. Une
contrainte concurrente déclenche un unique `find` de l'identité, jamais un overwrite ou une
récursion. Changer un paramètre scientifique produit un autre fingerprint et un autre résultat.
Les tests E2E utilisent le vrai Project DB v13, deux Feature Files à 8192 points, des Match Files
hashés, le vrai MAGSAC et le Model v1. VERIFIED est rechargé après close/reopen avec modèle et
masque bit-identiques ; REJECTED conserve son support et est également réutilisé.
## Production algorithm
UsacParams explicites, sampler uniforme, score MAGSAC, non parallèle et seed locale par appel.
Les champs LO et polishing effectifs sont encodés explicitement ; aucun preset enum caché.
## Production parameters
FUNDAMENTAL version 1 ; seuil 1,5 px ; confiance 0,999 ; 5000 itérations ; 16 inliers ; ratio 0,20 ;
seed policy 1 ; canonicalisation 1 ; Point2d. Tous les champs scientifiques appartiennent au
fingerprint version 1.
## Validation
Gate A couvre corpus, comparaison, seed et repeatability. Gate B couvre fingerprint/seed golden,
canonicalisation, mapping bit à bit, frontières d'acceptation, E2E DB, reuse, corruption,
publication, 8192 matches et ASan/UBSan. Gate C couvre Task, publication avant curseur et reprise.
Gate D a exécuté 1000 parents configurés dans la vraie Task, puis les reprises et variantes de
configuration du test : environ 2001 traversées réutilisées en 5,870 s, soit environ 341/s. Ce
run valide pagination, checkpoints, Governor et reuse ; il n'est pas une mesure de latence MAGSAC
et n'en revendique ni médiane ni p95. Le RSS pic observé est 25 964 Kio pour le processus de test
complet. `MemAvailable` passe de 10 702 988 à 10 692 916 Kio ; `pswpin/pswpout` restent 0/0 ; en
fin de run, PSI avg10 vaut 0,34 % CPU, 0 % mémoire et 0 % I/O. Le chemin calculé reste couvert par
le vrai E2E MAGSAC Gate B et ses bornes, sans campagne scientifique répétée.
TSan couvre core, Task, sequencing et Governor (4/4), avec uniquement la suppression OpenCV
existante. Le build CPU-only couvre la suite normale (31/31). La suite normale ne contient ni
benchmark lourd ni stress. Le clean build Clang/Clang++ et la campagne normale finale passent
32/32 avec ORB Vulkan matériel sur Radeon 780M RADV PHOENIX.
## Out of scope
Tracks, model competition, classification planaire ou faible parallaxe, Essential, calibration,
pose, triangulation, bundle adjustment, SfM et Vulkan RANSAC.

View file

@ -1,7 +1,8 @@
# Base de données projet Lardon3D # Base de données projet Lardon3D
> Version courante : **v12**. La migration transactionnelle v11→v12 ajoute le > Version courante : **v13**. La migration transactionnelle v12→v13 ajoute
> modèle immutable `geometric_verification_results`. La migration v10→v11 ajoute > uniquement `geometric_verifier_tasks` pour la tâche durable. La migration
> 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 > `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 > uniquement `match_results` pour le Match Result Model. La migration v8→v9
> ajoute la table `candidate_pair_generate_tasks` pour la tâche durable > ajoute la table `candidate_pair_generate_tasks` pour la tâche durable
@ -12,7 +13,8 @@
## Vision ## Vision
La base de données projet stocke les métadonnées de reconstruction et les relations entre les entités. Elle est conçue pour être légère, persistante et permettre la reprise après interruption. La base de données projet stocke les métadonnées de reconstruction et les relations entre les
entités. Elle est conçue pour être légère, persistante et permettre la reprise après interruption.
## Structure conceptuelle ## Structure conceptuelle
@ -338,6 +340,14 @@ Le curseur est le dernier `candidate_pair_id` checkpointé. Il n'implique ni
continuité des IDs ni liste persistée de Candidate Pairs. La configuration ne continuité des IDs ni liste persistée de Candidate Pairs. La configuration ne
peut pas changer lors d'un UPSERT ; seul le curseur avance. peut pas changer lors d'un UPSERT ; seul le curseur avance.
## Schéma v13 implémenté
La migration v12→v13 ajoute uniquement `geometric_verifier_tasks`. Elle porte
le curseur `after_match_result_id`, les sept paramètres scientifiques v1 et le
fingerprint de contrôle. L'UPSERT autorise seulement le curseur à évoluer ; la
configuration reste immuable. La migration est transactionnelle, son rollback
forcé conserve une vraie v12 sans la table et un retry termine en v13.
Le Match Result ci-dessous reste le contrat publié de v10 : Le Match Result ci-dessous reste le contrat publié de v10 :
```sql ```sql
@ -379,10 +389,13 @@ CREATE INDEX match_results_feature_set_b_idx
``` ```
**Invariants** : **Invariants** :
- `UNIQUE(candidate_pair_id, feature_set_id_a, feature_set_id_b, matcher_kind, matcher_version, parameter_fingerprint)` : identité déterministe 6 parties - `UNIQUE(candidate_pair_id, feature_set_id_a, feature_set_id_b, matcher_kind, matcher_version,
- `candidate_pair_id` référence `candidate_pairs(candidate_pair_id)` avec l'action par défaut (NO ACTION) parameter_fingerprint)` : identité déterministe 6 parties
- `candidate_pair_id` référence `candidate_pairs(candidate_pair_id)` avec l'action par défaut
(NO ACTION)
- `feature_set_id_a` et `feature_set_id_b` référencent `feature_sets(feature_set_id)` - `feature_set_id_a` et `feature_set_id_b` référencent `feature_sets(feature_set_id)`
- `feature_set_id_a` appartient à `image_id_a` de la Candidate Pair, `feature_set_id_b` appartient à `image_id_b` (validé par l'API create) - `feature_set_id_a` appartient à `image_id_a` de la Candidate Pair, `feature_set_id_b` appartient
à `image_id_b` (validé par l'API create)
- `NO_MATCH` impose `match_count=0` et aucun asset - `NO_MATCH` impose `match_count=0` et aucun asset
- `MATCHED` impose `match_count>0` et SHA/path/taille complets - `MATCHED` impose `match_count>0` et SHA/path/taille complets
- les échecs d'exécution restent dans le Task Runtime et ne créent pas de ligne - les échecs d'exécution restent dans le Task Runtime et ne créent pas de ligne
@ -392,7 +405,8 @@ CREATE INDEX match_results_feature_set_b_idx
**API** : **API** :
- `lardon3d_project_db_create_match_result()` — INSERT avec validation des contraintes - `lardon3d_project_db_create_match_result()` — INSERT avec validation des contraintes
- `lardon3d_project_db_load_match_result()` — SELECT par ID - `lardon3d_project_db_load_match_result()` — SELECT par ID
- `lardon3d_project_db_find_match_result()` — SELECT par (candidate_pair_id, feature_set_id_a, feature_set_id_b, matcher_kind, matcher_version, parameter_fingerprint) - `lardon3d_project_db_find_match_result()` — SELECT par (candidate_pair_id, feature_set_id_a,
feature_set_id_b, matcher_kind, matcher_version, parameter_fingerprint)
- `lardon3d_project_db_list_match_results()` — SELECT paginé ORDER BY id - `lardon3d_project_db_list_match_results()` — SELECT paginé ORDER BY id
- `lardon3d_project_db_record_matcher_task()` — UPSERT configuration/curseur - `lardon3d_project_db_record_matcher_task()` — UPSERT configuration/curseur
- `lardon3d_project_db_load_matcher_task()` — SELECT par task_id - `lardon3d_project_db_load_matcher_task()` — SELECT par task_id
@ -444,18 +458,20 @@ Le contrat complet, dont l'ordre des bits, est dans
## Ouverture et migrations ## Ouverture et migrations
Une DB vide reçoit la chaîne de schémas jusqu'à v12 dans une transaction Une DB vide reçoit la chaîne de schémas jusqu'à v13 dans une transaction
`BEGIN IMMEDIATE`. Une DB v1 reçoit transactionnellement les colonnes nullable `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 `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 `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, artefacts. Une interruption ou erreur provoque un rollback complet. Les DB v1
v2, v3, v4, v5, v6, v7, v8, v9, v10 et v11 sont migrées séquentiellement vers v12. à v12 sont migrées séquentiellement vers v13.
Une v10 publiée est validée comme telle avant que v10→v11 crée 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 `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 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'à interne de migration applique uniquement la chaîne séquentielle connue jusqu'à
v12 ; une valeur hors de 1..12 est refusée. La failure injectée v12 rollbacke v13 ; une valeur hors de 1..13 est refusée. La failure injectée v12 rollbacke
la table, l'index et le changement de version, laissant une vraie v11 utilisable. la table, l'index et le changement de version, laissant une vraie v11 utilisable.
La failure injectée v13 conserve une vraie v12 sans `geometric_verifier_tasks` ;
un retry applique ensuite v12→v13.
Migration v1→v2 exacte, exécutée entre `BEGIN IMMEDIATE` et `COMMIT` : Migration v1→v2 exacte, exécutée entre `BEGIN IMMEDIATE` et `COMMIT` :
@ -592,8 +608,8 @@ ouvert.
## Statut ## Statut
**IMPLEMENTED** — SQLite système, schéma v12 et migrations séquentielles **IMPLEMENTED** — SQLite système, schéma v13 et migrations séquentielles
v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11→v12, identité projet, transactions v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11→v12→v13, identité projet, transactions
tâche+checkpoint, pagination de reprise et artefacts génériques. tâche+checkpoint, pagination de reprise et artefacts génériques.
**IMPLEMENTED** — ouverture/fermeture avec le projet, identité INI/DB cohérente, **IMPLEMENTED** — ouverture/fermeture avec le projet, identité INI/DB cohérente,

View file

@ -97,19 +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. | | **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 **Statut :** IMPLEMENTED v1 — Matcher, Match Store, reprise idempotente et Task
durable. Le calcul Geometric Verifier reste l'étape suivante, non commencée. durable. Le Geometric Verifier consomme désormais ces résultats.
Le Match Result appartient à Project DB v10 et la tâche durable `matcher.run` Le Match Result appartient à Project DB v10 et la tâche durable `matcher.run`
à Project DB v11. à Project DB v11.
### F2. Geometric Verification ### F2. Geometric Verification
Project DB v12 implémente uniquement le modèle scientifique persistant. Chaque Project DB v12 implémente le modèle scientifique persistant. Chaque
résultat appartient à un Match Result `MATCHED`, possède une identité exacte, 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. 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 La publication est atomique et immutable. Project DB v13 ajoute uniquement la
d'algorithme restent planifiés dans Geometric Verifier v1. persistance de `geometric_verifier.run` v1. Le calcul Fundamental emploie
USAC/MAGSAC avec configuration, seed et fingerprint déterministes.
**Statut :** MODEL IMPLEMENTED v1 — VERIFIER PLANNED. **Statut :** IMPLEMENTED v1 — MODEL, VERIFIER ET TASK DURABLE.
--- ---

View file

@ -55,13 +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 checkpoint revoit la paire : le Matcher réutilise alors le Match Result et ne
recalcule pas les descripteurs. recalcule pas les descripteurs.
## Geometric Verification Model ## Geometric Verification
Project DB v12 stocke un résultat borné à 1024 octets de masque et neuf 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. binary64. `geometric_verifier.run` v1 traite un Match Result atomique, publie
Le futur verifier calculera une paire hors transaction, publiera par une courte par une transaction courte puis checkpoint son curseur par lots 1/2/4/8 avant
transaction, checkpoint puis libérera ses buffers avant réadmission. Admission, `task_sequence_break()`. Sa ligne durable appartient à Project DB v13. Le job
threads et lots resteront exclusivement décidés par Runtime et Governor. réserve un CPU logique et 4 Mio, sans slot GPU. Admission, pression, lots et
slow-start restent exclusivement décidés par Runtime et Governor.
## GPU et files ## GPU et files
@ -84,6 +85,7 @@ reste inchangée.
- hard floor `MemAvailable` : un huitième, environ 1,9 Gio ; - hard floor `MemAvailable` : un huitième, environ 1,9 Gio ;
- Feature workers : 1 ; batch : 1 image ; - Feature workers : 1 ; batch : 1 image ;
- Matcher workers : 1 ; lots adaptatifs 1, 2, 4 ou 8 Candidate Pairs ; - Matcher workers : 1 ; lots adaptatifs 1, 2, 4 ou 8 Candidate Pairs ;
- Geometric Verifier workers : 1 ; lots adaptatifs 1, 2, 4 ou 8 Match Results ;
- profondeur de la Task Queue : 64 tâches légères, un seul callback actif ; - profondeur de la Task Queue : 64 tâches légères, un seul callback actif ;
- PSI CPU avg10 : nouvelle admission suspendue à 20 % ; - PSI CPU avg10 : nouvelle admission suspendue à 20 % ;
- PSI mémoire avg10 : nouvelle admission suspendue à 1 % ; - PSI mémoire avg10 : nouvelle admission suspendue à 1 % ;
@ -93,6 +95,13 @@ Le benchmark Matcher 8192 mesure environ 70 ms ORB et 135 ms SIFT à 12 threads,
contre 68 ms et 127 ms à 16 threads : le profil interactif abandonne environ contre 68 ms et 127 ms à 16 threads : le profil interactif abandonne environ
37 % de latence isolée pour réserver quatre threads logiques au desktop. 37 % de latence isolée pour réserver quatre threads logiques au desktop.
Le run soutenu Geometric Verifier traverse environ 2001 parents réutilisés en
5,870 s via Task, DB, checkpoints et Governor. Le processus de test culmine à
25 964 Kio RSS ; `MemAvailable` reste au-dessus de 10,69 Gio et les compteurs
swap restent nuls. PSI avg10 final vaut 0,34 % CPU et 0 % mémoire/I/O. Cette
mesure valide le chemin resource-aware et la reprise ; elle ne prétend pas être
une distribution de latence estimator-only.
## Limites ## Limites
Le profil maximal explicite et les pools multi-workers restent hors périmètre. Le profil maximal explicite et les pools multi-workers restent hors périmètre.

View file

@ -69,3 +69,8 @@ l'identité Feature Set et le curseur `after_candidate_pair_id`. Il traite une
Candidate Pair atomique à la fois dans des lots bornés à huit, checkpoint le Candidate Pair atomique à la fois dans des lots bornés à huit, checkpoint le
curseur et repasse par le Governor entre les lots. La table durable curseur et repasse par le Governor entre les lots. La table durable
`matcher_tasks` est introduite par Project DB v11, après le Match Result v10. `matcher_tasks` est introduite par Project DB v11, après le Match Result v10.
**IMPLEMENTED** — `geometric_verifier.run`, version 1, recharge la configuration
Fundamental immuable, en revalide le fingerprint et reprend `after_match_result_id`.
Project DB v13 ajoute uniquement `geometric_verifier_tasks`, car le checkpoint
générique v1 ne possède aucun payload propre au kind.

View file

@ -111,10 +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 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. crash est réutilisée par son Match Result.
Geometric Verification Model v1 n'ajoute aucun task kind ni état runtime. **IMPLEMENTED** — `geometric_verifier.run` v1 traite un Match Result atomique à
Project DB v12 ne contient que les deux états scientifiques terminaux. Le futur la fois, par lots adaptatifs de 1, 2, 4 ou 8. Project DB v13 conserve sa
Geometric Verifier v1 devra obtenir réservation, pause/cancel, lot et checkpoint configuration scientifique et `after_match_result_id`. Chaque résultat est
dans un ticket séparé avant toute exécution. publié avant le curseur ; pause, annulation, checkpoint et rupture de séquence
restent coopératifs aux frontières des parents et des lots.
Le chemin de production de l'import ne possède plus de thread ni de drapeau 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 d'annulation privés. Son wrapper TUI ne fait qu'enqueue/cancel/observer la

View file

@ -70,6 +70,30 @@ de parité couvrent exactement le top-2 jusqu'à 8192, le Match File complet et
fallback CPU. Ces nombres décrivent la machine mesurée et ne sont pas un contrat fallback CPU. Ces nombres décrivent la machine mesurée et ne sont pas un contrat
portable de latence. portable de latence.
## Geometric Verifier Fundamental — Gate A
Le 9 août 2026, `benchmark-geometric-verifier` a comparé FM_RANSAC, USAC_DEFAULT, USAC_MAGSAC,
USAC_ACCURATE et MAGSAC à seed locale sur le Ryzen 7 8845HS, OpenCV 5.0.0 et Clang 22.1.8. Sur
8192 correspondances, 70 % d'outliers et bruit 0,75 px, le candidat local mesure
10,80/11,04/11,35 ms médiane/p95/pire. FM_RANSAC mesure 316,44/318,87/319,66 ms avec seulement
0,413 de recall.
Avant campagne, PSI CPU/mémoire/I/O avg10 était nul, `MemAvailable` environ 9,72 Gio et
`pswpin/pswpout` nul. Après 30,4 s à environ 99 % d'un CPU, PSI et swap restaient nuls,
`MemAvailable` environ 9,74 Gio et le capteur CPU observé passait d'environ 54,6 à 55 °C. Ce sont
des proxies objectifs, pas une mesure subjective de fluidité.
Le coût court et le faible working set rendent Vulkan `NOT_JUSTIFIED` pour ce verifier sur la
Radeon 780M. Le GPU reste utilisé uniquement par le Matcher ORB existant.
Le profil production réserve un CPU logique, 4 Mio et un worker, avec lots 1/2/4/8. Un run de la
vraie Task sur 1000 parents, suivi de ses chemins de reprise/configuration, a traversé environ
2001 parents réutilisés en 5,870 s (environ 341/s). Ce chiffre inclut DB, checkpoints et fixture ;
il ne remplace pas la latence estimator-only Gate A. Le processus de test a culminé à 25 964 Kio
RSS. `MemAvailable` a varié de 10 702 988 à 10 692 916 Kio, sans swap ; PSI avg10 final était
0,34 % CPU et 0 % mémoire/I/O. Les latences médiane/p95 par parent et le RSS début/fin n'étaient
pas mesurables avec ce harness et ne sont donc pas extrapolés.
## Feasibility Vulkan SIFT / RootSIFT ## Feasibility Vulkan SIFT / RootSIFT
Sur le même RADV PHOENIX, `shaderFloat64`, les timestamps compute, un subgroup Sur le même RADV PHOENIX, `shaderFloat64`, les timestamps compute, un subgroup

View file

@ -0,0 +1,65 @@
#ifndef LARDON3D_GEOMETRIC_VERIFIER_H
#define LARDON3D_GEOMETRIC_VERIFIER_H
#include <stdbool.h>
#include <stdint.h>
#include <lardon3d/project_db.h>
enum {
LARDON3D_GEOMETRIC_VERIFIER_VERSION = 1,
LARDON3D_GEOMETRIC_VERIFIER_FINGERPRINT_SIZE = 84,
LARDON3D_GEOMETRIC_VERIFIER_MINIMUM_MATCHES = 7,
};
typedef enum {
LARDON3D_GEOMETRIC_ALGORITHM_USAC_MAGSAC = 1,
LARDON3D_GEOMETRIC_ALGORITHM_USAC_DEFAULT = 2,
} Lardon3DGeometricVerifierAlgorithm;
typedef struct {
double threshold_pixels;
double confidence;
uint32_t max_iterations;
uint32_t min_inlier_count;
double min_inlier_ratio;
uint32_t seed_policy_version;
uint32_t canonicalization_version;
} Lardon3DGeometricVerifierParameters;
typedef enum {
LARDON3D_GEOMETRIC_VERIFIER_OK = 0,
LARDON3D_GEOMETRIC_VERIFIER_INVALID_ARGUMENT,
LARDON3D_GEOMETRIC_VERIFIER_NOT_FOUND,
LARDON3D_GEOMETRIC_VERIFIER_CORRUPT,
LARDON3D_GEOMETRIC_VERIFIER_OUT_OF_MEMORY,
LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR,
LARDON3D_GEOMETRIC_VERIFIER_DATABASE_ERROR,
} Lardon3DGeometricVerifierResult;
Lardon3DGeometricVerifierParameters
lardon3d_geometric_verifier_default_parameters(void);
bool lardon3d_geometric_verifier_parameters_valid(
const Lardon3DGeometricVerifierParameters *parameters);
bool lardon3d_geometric_verifier_fingerprint_bytes(
const Lardon3DGeometricVerifierParameters *parameters,
Lardon3DGeometricVerifierAlgorithm algorithm, uint32_t verifier_version,
unsigned char bytes[LARDON3D_GEOMETRIC_VERIFIER_FINGERPRINT_SIZE]);
void lardon3d_geometric_verifier_fingerprint(
const Lardon3DGeometricVerifierParameters *parameters,
unsigned char fingerprint[32]);
bool lardon3d_geometric_verifier_canonicalize(double model[9]);
uint32_t lardon3d_geometric_verifier_seed(const unsigned char match_sha256[32],
const unsigned char fingerprint[32]);
Lardon3DGeometricVerifierResult lardon3d_geometric_verifier_verify_and_publish(
const char *project_path, Lardon3DProjectDb *database,
uint64_t match_result_id,
const Lardon3DGeometricVerifierParameters *parameters,
Lardon3DProjectDbGeometricVerificationResult *result, bool *reused);
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TESTING
void lardon3d_geometric_verifier_test_reset_estimator_calls(void);
uint32_t lardon3d_geometric_verifier_test_estimator_calls(void);
#endif
#endif

View file

@ -0,0 +1,35 @@
#ifndef LARDON3D_GEOMETRIC_VERIFIER_TASK_H
#define LARDON3D_GEOMETRIC_VERIFIER_TASK_H
#include <stdbool.h>
#include <stdint.h>
#include <lardon3d/app_state.h>
#include <lardon3d/geometric_verifier.h>
#include <lardon3d/task_kind_registry.h>
#define LARDON3D_GEOMETRIC_VERIFIER_TASK_KIND "geometric_verifier.run"
enum {
LARDON3D_GEOMETRIC_VERIFIER_TASK_KIND_VERSION = 1,
LARDON3D_GEOMETRIC_VERIFIER_TASK_MINIMUM_BATCH = 1,
LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH = 8,
};
typedef struct {
Lardon3DGeometricVerifierParameters verifier;
} Lardon3DGeometricVerifierTaskConfiguration;
Lardon3DTask *lardon3d_project_create_geometric_verifier_task(
Lardon3DAppState *state,
const Lardon3DGeometricVerifierTaskConfiguration *configuration,
uint64_t *task_id);
bool lardon3d_project_enqueue_geometric_verifier_task(
Lardon3DAppState *state,
const Lardon3DGeometricVerifierTaskConfiguration *configuration,
uint64_t *task_id);
bool lardon3d_geometric_verifier_task_reconstruct(
const Lardon3DTaskDurableSnapshot *snapshot, void *context,
Lardon3DTaskKindBinding *binding);
#endif

View file

@ -73,6 +73,9 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_g
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task(
Lardon3DAppState *state, const Lardon3DTask *task, Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbMatcherTask *parameters); const Lardon3DProjectDbMatcherTask *parameters);
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_geometric_verifier_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbGeometricVerifierTask *parameters);
Lardon3DProjectDbResult lardon3d_project_list_recoverable(Lardon3DAppState *state, Lardon3DProjectDbResult lardon3d_project_list_recoverable(Lardon3DAppState *state,
const Lardon3DTaskKindRegistry *registry, const Lardon3DTaskKindRegistry *registry,
uint64_t after_task_id, uint64_t after_task_id,

View file

@ -9,7 +9,7 @@
#include <lardon3d/task.h> #include <lardon3d/task.h>
enum { enum {
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 12, LARDON3D_PROJECT_DB_SCHEMA_VERSION = 13,
LARDON3D_PROJECT_DB_ID_CAPACITY = 65, LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65, LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096, LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
@ -144,6 +144,19 @@ typedef struct {
float ratio_threshold; float ratio_threshold;
} Lardon3DProjectDbMatcherTask; } Lardon3DProjectDbMatcherTask;
typedef struct {
uint64_t task_id;
uint64_t after_match_result_id;
double threshold_pixels;
double confidence;
uint32_t max_iterations;
uint32_t min_inlier_count;
double min_inlier_ratio;
uint32_t seed_policy_version;
uint32_t canonicalization_version;
unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE];
} Lardon3DProjectDbGeometricVerifierTask;
typedef struct { typedef struct {
uint64_t match_result_id; uint64_t match_result_id;
uint64_t candidate_pair_id; uint64_t candidate_pair_id;
@ -508,6 +521,14 @@ Lardon3DProjectDbResult lardon3d_project_db_record_matcher_task(
Lardon3DProjectDbResult lardon3d_project_db_load_matcher_task( Lardon3DProjectDbResult lardon3d_project_db_load_matcher_task(
Lardon3DProjectDb *database, uint64_t task_id, Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DProjectDbMatcherTask *parameters); Lardon3DProjectDbMatcherTask *parameters);
Lardon3DProjectDbResult lardon3d_project_db_record_geometric_verifier_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *task_kind, uint32_t task_kind_version,
const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbGeometricVerifierTask *parameters, int64_t updated_at);
Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verifier_task(
Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DProjectDbGeometricVerifierTask *parameters);
Lardon3DProjectDbResult lardon3d_project_db_create_match_result( Lardon3DProjectDbResult lardon3d_project_db_create_match_result(
Lardon3DProjectDb *database, uint64_t candidate_pair_id, uint64_t feature_set_id_a, Lardon3DProjectDb *database, uint64_t candidate_pair_id, uint64_t feature_set_id_a,

View file

@ -28,7 +28,7 @@ opencv = dependency(
'opencv5', 'opencv5',
required: true, required: true,
include_type: 'system', include_type: 'system',
modules: ['opencv_core', 'opencv_imgcodecs', 'opencv_features2d'], modules: ['opencv_core', 'opencv_imgcodecs', 'opencv_features2d', 'opencv_geometry'],
) )
opencv_benchmark = dependency( opencv_benchmark = dependency(
'opencv5', 'opencv5',
@ -37,6 +37,12 @@ opencv_benchmark = dependency(
modules: ['opencv_core', 'opencv_imgcodecs', 'opencv_features2d', 'opencv_imgproc'], modules: ['opencv_core', 'opencv_imgcodecs', 'opencv_features2d', 'opencv_imgproc'],
) )
opencv_geometry = dependency(
'opencv5',
version: '>=5.0.0',
modules: ['opencv_core', 'opencv_geometry'],
)
vulkan_orb = get_option('vulkan_orb') vulkan_orb = get_option('vulkan_orb')
vulkan = dependency('vulkan', required: vulkan_orb) vulkan = dependency('vulkan', required: vulkan_orb)
glslc = find_program('glslc', required: vulkan_orb) glslc = find_program('glslc', required: vulkan_orb)
@ -134,6 +140,7 @@ executable(
'src/candidate_pair_gen.c', 'src/candidate_pair_gen.c',
'src/candidate_pair_task.c', 'src/candidate_pair_task.c',
'src/matcher_task.c', 'src/matcher_task.c',
'src/geometric_verifier_task.c',
'src/image_view.c', 'src/image_view.c',
'src/project.c', 'src/project.c',
'src/project_db.c', 'src/project_db.c',
@ -147,9 +154,10 @@ executable(
'src/hardware_profile.c', 'src/hardware_profile.c',
'src/match_file.c', 'src/match_file.c',
'src/matcher.cpp', 'src/matcher.cpp',
'src/geometric_verifier.cpp',
] + matcher_backend_sources, ] + matcher_backend_sources,
include_directories: include_directories('include'), include_directories: include_directories('include'),
dependencies: [ncursesw, threads, sqlite3, openssl, opencv] dependencies: [ncursesw, threads, sqlite3, openssl, opencv, opencv_geometry]
+ matcher_backend_dependencies, + matcher_backend_dependencies,
) )
@ -185,6 +193,7 @@ import_task_test = executable(
'src/task_kind_registry.c', 'src/task_kind_registry.c',
'src/task_kinds.c', 'src/task_kinds.c',
'src/matcher_task.c', 'src/matcher.cpp', 'src/match_file.c', 'src/matcher_task.c', 'src/matcher.cpp', 'src/match_file.c',
'src/geometric_verifier_task.c', 'src/geometric_verifier.cpp',
'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c', 'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c',
'src/feature_store.c', 'src/feature_store.c',
'src/visual_index.c', 'src/visual_index.c',
@ -324,6 +333,7 @@ candidate_pair_task_test = executable(
'src/project.c', 'src/project_db.c', 'src/task.c', 'src/project.c', 'src/project_db.c', 'src/task.c',
'src/task_checkpoint.c', 'src/task_kind_registry.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c',
'src/task_kinds.c', 'src/matcher_task.c', 'src/matcher.cpp', 'src/task_kinds.c', 'src/matcher_task.c', 'src/matcher.cpp',
'src/geometric_verifier_task.c', 'src/geometric_verifier.cpp',
'src/match_file.c', 'src/task_queue.c', 'src/import.c', 'src/match_file.c', 'src/task_queue.c', 'src/import.c',
'src/import_task.c', 'src/image_catalog.c', 'src/import_task.c', 'src/image_catalog.c',
'src/image_catalog_persistent.c', 'src/image_view.c', 'src/image_catalog_persistent.c', 'src/image_view.c',
@ -353,6 +363,7 @@ matcher_task_test = executable(
'src/project_db.c', 'src/task.c', 'src/task_checkpoint.c', 'src/project_db.c', 'src/task.c', 'src/task_checkpoint.c',
'src/task_kind_registry.c', 'src/task_kinds.c', 'src/task_queue.c', 'src/task_kind_registry.c', 'src/task_kinds.c', 'src/task_queue.c',
'src/matcher_task.c', 'src/matcher.cpp', 'src/match_file.c', 'src/matcher_task.c', 'src/matcher.cpp', 'src/match_file.c',
'src/geometric_verifier_task.c', 'src/geometric_verifier.cpp',
'src/feature_store.c', 'src/resource_governor.c', 'src/feature_store.c', 'src/resource_governor.c',
'src/resource_snapshot.c', 'src/hardware_profile.c', 'src/resource_snapshot.c', 'src/hardware_profile.c',
'src/import.c', 'src/import_task.c', 'src/image_catalog.c', 'src/import.c', 'src/import_task.c', 'src/image_catalog.c',
@ -378,6 +389,7 @@ feature_task_test = executable(
'src/project_db.c', 'src/task.c', 'src/task_checkpoint.c', 'src/project_db.c', 'src/task.c', 'src/task_checkpoint.c',
'src/task_kind_registry.c', 'src/task_kinds.c', 'src/task_queue.c', 'src/task_kind_registry.c', 'src/task_kinds.c', 'src/task_queue.c',
'src/matcher_task.c', 'src/matcher.cpp', 'src/match_file.c', 'src/matcher_task.c', 'src/matcher.cpp', 'src/match_file.c',
'src/geometric_verifier_task.c', 'src/geometric_verifier.cpp',
'src/import.c', 'src/import_task.c', 'src/image_catalog.c', 'src/import.c', 'src/import_task.c', 'src/image_catalog.c',
'src/image_catalog_persistent.c', 'src/image_view.c', 'src/image_catalog_persistent.c', 'src/image_view.c',
'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c', 'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c',
@ -411,6 +423,7 @@ precision_consolidation_test = executable(
'src/project.c', 'src/project_db.c', 'src/task.c', 'src/project.c', 'src/project_db.c', 'src/task.c',
'src/task_checkpoint.c', 'src/task_kind_registry.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c',
'src/task_kinds.c', 'src/matcher_task.c', 'src/matcher.cpp', 'src/task_kinds.c', 'src/matcher_task.c', 'src/matcher.cpp',
'src/geometric_verifier_task.c', 'src/geometric_verifier.cpp',
'src/match_file.c', 'src/task_queue.c', 'src/import.c', 'src/match_file.c', 'src/task_queue.c', 'src/import.c',
'src/import_task.c', 'src/image_catalog.c', 'src/import_task.c', 'src/image_catalog.c',
'src/image_catalog_persistent.c', 'src/image_view.c', 'src/image_catalog_persistent.c', 'src/image_view.c',
@ -535,6 +548,7 @@ project_test = executable(
'src/task_kind_registry.c', 'src/task_kind_registry.c',
'src/task_kinds.c', 'src/task_kinds.c',
'src/matcher_task.c', 'src/matcher.cpp', 'src/match_file.c', 'src/matcher_task.c', 'src/matcher.cpp', 'src/match_file.c',
'src/geometric_verifier_task.c', 'src/geometric_verifier.cpp',
'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c', 'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c',
'src/feature_store.c', 'src/feature_store.c',
'src/visual_index.c', 'src/visual_index.c',
@ -763,3 +777,57 @@ executable(
include_directories: include_directories('include'), include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv] + matcher_backend_dependencies, dependencies: [threads, sqlite3, openssl, opencv] + matcher_backend_dependencies,
) )
executable(
'benchmark-geometric-verifier',
sources: ['tests/benchmark_geometric_verifier.cpp'],
build_by_default: false,
dependencies: [opencv_geometry],
)
geometric_verifier_test = executable(
'test-geometric-verifier-core',
sources: [
'tests/test_geometric_verifier_core.cpp',
'src/geometric_verifier.cpp',
'src/match_file.c',
'src/feature_store.c',
'src/feature_extractor_opencv.cpp',
'src/project_db.c',
'src/task.c',
'src/resource_governor.c',
'src/resource_snapshot.c',
'src/image_catalog_persistent.c',
'src/app_state.c',
],
cpp_args: ['-DLARDON3D_GEOMETRIC_VERIFIER_TESTING'],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv_geometry],
)
test('geometric-verifier-core', geometric_verifier_test, timeout: 60)
geometric_verifier_task_test = executable(
'test-geometric-verifier-task',
sources: [
'tests/test_geometric_verifier_task.c', 'src/app_state.c', 'src/project.c',
'src/project_db.c', 'src/task.c', 'src/task_checkpoint.c',
'src/task_kind_registry.c', 'src/task_kinds.c', 'src/task_queue.c',
'src/geometric_verifier_task.c', 'src/geometric_verifier.cpp',
'src/match_file.c', 'src/feature_store.c', 'src/resource_governor.c',
'src/resource_snapshot.c', 'src/import.c', 'src/import_task.c',
'src/image_catalog.c', 'src/image_catalog_persistent.c', 'src/image_view.c',
'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c',
'src/feature_extractor_opencv.cpp', 'src/visual_index.c',
'src/visual_index_task.c', 'src/candidate_pair_gen.c',
'src/candidate_pair_task.c', 'src/matcher_task.c', 'src/matcher.cpp',
] + matcher_backend_sources,
c_args: [
'-DLARDON3D_PROJECT_DB_TESTING',
'-DLARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING',
],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv] + matcher_backend_dependencies,
)
test('geometric-verifier-task', geometric_verifier_task_test, timeout: 60,
env: opencv_test_environment)

411
src/geometric_verifier.cpp Normal file
View file

@ -0,0 +1,411 @@
#include <algorithm>
#include <climits>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <fcntl.h>
#include <new>
#include <opencv2/calib3d.hpp>
#include <openssl/evp.h>
#include <string>
#include <unistd.h>
#include <vector>
extern "C" {
#include <lardon3d/feature_store.h>
#include <lardon3d/geometric_verifier.h>
#include <lardon3d/match_file.h>
}
namespace {
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TESTING
uint32_t estimator_calls;
#endif
void put_u32(unsigned char *bytes, uint32_t value) {
for (unsigned int index = 0; index < 4; ++index)
bytes[index] = static_cast<unsigned char>(value >> (8U * index));
}
void put_u64(unsigned char *bytes, uint64_t value) {
for (unsigned int index = 0; index < 8; ++index)
bytes[index] = static_cast<unsigned char>(value >> (8U * index));
}
uint64_t double_bits(double value) {
uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
return bits;
}
bool digest(const unsigned char *bytes, size_t size, unsigned char output[32]) {
unsigned int output_size = 0;
return EVP_Digest(bytes, size, output, &output_size, EVP_sha256(), nullptr) ==
1 &&
output_size == 32;
}
double normalized_zero(double value) { return value == 0.0 ? 0.0 : value; }
bool join_path(char output[4096], const char *root, const char *relative) {
if (!root || !relative || relative[0] == '/' || std::strstr(relative, ".."))
return false;
int length = std::snprintf(output, 4096, "%s/%s", root, relative);
return length > 0 && length < 4096;
}
bool read_points(const char *project_path,
const Lardon3DProjectDbFeatureSet &set,
std::vector<Lardon3DFeatureKeypoint> &points) {
Lardon3DFeatureReader *reader = nullptr;
Lardon3DFeatureFileMetadata metadata;
if (lardon3d_feature_reader_open(project_path, &set, &reader, &metadata) !=
LARDON3D_FEATURE_STORE_OK)
return false;
points.resize(set.feature_count);
bool ok = metadata.feature_count == set.feature_count;
for (uint32_t start = 0; ok && start < set.feature_count; start += 256) {
size_t count = std::min<size_t>(256, set.feature_count - start);
ok = lardon3d_feature_reader_keypoints(reader, start, points.data() + start,
count) == LARDON3D_FEATURE_STORE_OK;
}
lardon3d_feature_reader_close(reader);
return ok;
}
bool estimate_fundamental(const std::vector<cv::Point2d> &points_a,
const std::vector<cv::Point2d> &points_b,
const Lardon3DGeometricVerifierParameters *parameters,
uint32_t seed, cv::Mat &model, cv::Mat &mask) {
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TESTING
++estimator_calls;
const char *behavior = std::getenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR");
if (behavior && std::strcmp(behavior, "error") == 0)
return false;
if (behavior && std::strcmp(behavior, "bad-alloc") == 0)
throw std::bad_alloc();
if (behavior && std::strcmp(behavior, "empty") == 0)
return true;
if (behavior && std::strcmp(behavior, "malformed-mask") == 0) {
model = cv::Mat::eye(3, 3, CV_64F);
mask = cv::Mat::ones(1, 1, CV_8U);
return true;
}
if (behavior && std::strcmp(behavior, "malformed-model") == 0) {
model = cv::Mat::eye(2, 2, CV_64F);
mask = cv::Mat::ones(static_cast<int>(points_a.size()), 1, CV_8U);
return true;
}
if (behavior && std::strcmp(behavior, "nan-model") == 0) {
model = cv::Mat::eye(3, 3, CV_64F);
model.at<double>(0, 0) = NAN;
mask = cv::Mat::ones(static_cast<int>(points_a.size()), 1, CV_8U);
return true;
}
if (behavior && std::strcmp(behavior, "controlled") == 0) {
static const double fundamental[9] = {0.0, 0.0, 0.0, 0.0, 0.0,
-1.0, 0.0, 1.0, 0.0};
model = cv::Mat(3, 3, CV_64F, const_cast<double *>(fundamental)).clone();
mask = cv::Mat::zeros(static_cast<int>(points_a.size()), 1, CV_8U);
const char *bits = std::getenv("LARDON3D_TEST_GEOMETRIC_MASK");
if (!bits || std::strlen(bits) != points_a.size())
return false;
for (size_t index = 0; index < points_a.size(); ++index) {
if (bits[index] != '0' && bits[index] != '1')
return false;
mask.ptr<unsigned char>()[index] =
static_cast<unsigned char>(bits[index] - '0');
}
return true;
}
#endif
cv::UsacParams params;
params.confidence = parameters->confidence;
params.isParallel = false;
params.loIterations = 5;
params.loMethod = cv::LOCAL_OPTIM_INNER_LO;
params.loSampleSize = 14;
params.maxIterations = static_cast<int>(parameters->max_iterations);
params.neighborsSearch = cv::NEIGH_GRID;
params.randomGeneratorState = static_cast<int>(seed);
params.sampler = cv::SAMPLING_UNIFORM;
params.score = cv::SCORE_METHOD_MAGSAC;
params.threshold = parameters->threshold_pixels;
params.final_polisher = cv::COV_POLISHER;
params.final_polisher_iterations = 3;
model = cv::findFundamentalMat(points_a, points_b, mask, params);
return true;
}
} // namespace
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TESTING
extern "C" void lardon3d_geometric_verifier_test_reset_estimator_calls(void) {
estimator_calls = 0;
}
extern "C" uint32_t lardon3d_geometric_verifier_test_estimator_calls(void) {
return estimator_calls;
}
#endif
extern "C" Lardon3DGeometricVerifierParameters
lardon3d_geometric_verifier_default_parameters(void) {
return {1.5, 0.999, 5000, 16, 0.20, 1, 1};
}
extern "C" bool lardon3d_geometric_verifier_parameters_valid(
const Lardon3DGeometricVerifierParameters *p) {
return p && std::isfinite(p->threshold_pixels) && p->threshold_pixels > 0.0 &&
std::isfinite(p->confidence) && p->confidence > 0.0 &&
p->confidence < 1.0 && p->max_iterations > 0 &&
p->max_iterations <= static_cast<uint32_t>(INT_MAX) &&
p->min_inlier_count > 0 &&
p->min_inlier_count <= LARDON3D_MATCH_FILE_MAX_MATCHES &&
std::isfinite(p->min_inlier_ratio) && p->min_inlier_ratio >= 0.0 &&
p->min_inlier_ratio <= 1.0 && p->seed_policy_version == 1 &&
p->canonicalization_version == 1;
}
extern "C" bool lardon3d_geometric_verifier_fingerprint_bytes(
const Lardon3DGeometricVerifierParameters *p,
Lardon3DGeometricVerifierAlgorithm algorithm, uint32_t verifier_version,
unsigned char bytes[LARDON3D_GEOMETRIC_VERIFIER_FINGERPRINT_SIZE]) {
if (!bytes)
return false;
std::memset(bytes, 0, LARDON3D_GEOMETRIC_VERIFIER_FINGERPRINT_SIZE);
if (!lardon3d_geometric_verifier_parameters_valid(p) ||
verifier_version == 0 ||
(algorithm != LARDON3D_GEOMETRIC_ALGORITHM_USAC_MAGSAC &&
algorithm != LARDON3D_GEOMETRIC_ALGORITHM_USAC_DEFAULT))
return false;
std::memcpy(bytes, "L3DGVFP1", 8);
put_u32(bytes + 8, 1);
put_u32(bytes + 12, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL);
put_u32(bytes + 16, verifier_version);
put_u32(bytes + 20, static_cast<uint32_t>(algorithm));
put_u64(bytes + 24, double_bits(p->threshold_pixels));
put_u64(bytes + 32, double_bits(p->confidence));
put_u32(bytes + 40, p->max_iterations);
put_u32(bytes + 44, p->min_inlier_count);
put_u64(bytes + 48, double_bits(normalized_zero(p->min_inlier_ratio)));
put_u32(bytes + 56, p->seed_policy_version);
put_u32(bytes + 60, p->canonicalization_version);
bytes[64] = 2; // Point2d.
bytes[65] = cv::SAMPLING_UNIFORM;
bytes[66] = algorithm == LARDON3D_GEOMETRIC_ALGORITHM_USAC_MAGSAC
? cv::SCORE_METHOD_MAGSAC
: cv::SCORE_METHOD_MSAC;
bytes[67] = 0; // isParallel=false.
bytes[68] = cv::LOCAL_OPTIM_INNER_LO;
put_u32(bytes + 69, 5);
put_u32(bytes + 73, 14);
bytes[77] = cv::NEIGH_GRID;
bytes[78] = cv::COV_POLISHER;
put_u32(bytes + 79, 3);
bytes[83] = 0;
return true;
}
extern "C" void lardon3d_geometric_verifier_fingerprint(
const Lardon3DGeometricVerifierParameters *p, unsigned char output[32]) {
if (!output)
return;
std::memset(output, 0, 32);
unsigned char bytes[LARDON3D_GEOMETRIC_VERIFIER_FINGERPRINT_SIZE];
if (!lardon3d_geometric_verifier_fingerprint_bytes(
p, LARDON3D_GEOMETRIC_ALGORITHM_USAC_MAGSAC,
LARDON3D_GEOMETRIC_VERIFIER_VERSION, bytes))
return;
(void)digest(bytes, sizeof(bytes), output);
}
extern "C" uint32_t
lardon3d_geometric_verifier_seed(const unsigned char match_sha256[32],
const unsigned char fingerprint[32]) {
if (!match_sha256 || !fingerprint)
return 0;
unsigned char bytes[72];
std::memcpy(bytes, "L3DGVSE1", 8);
std::memcpy(bytes + 8, match_sha256, 32);
std::memcpy(bytes + 40, fingerprint, 32);
unsigned char hash[32];
if (!digest(bytes, sizeof(bytes), hash))
return 0;
uint32_t value = static_cast<uint32_t>(hash[0]) |
static_cast<uint32_t>(hash[1]) << 8U |
static_cast<uint32_t>(hash[2]) << 16U |
static_cast<uint32_t>(hash[3]) << 24U;
return value & 0x7fffffffU;
}
extern "C" bool lardon3d_geometric_verifier_canonicalize(double model[9]) {
if (!model)
return false;
double norm = 0.0;
size_t pivot = 0;
for (size_t index = 0; index < 9; ++index) {
if (!std::isfinite(model[index]))
return false;
norm = std::hypot(norm, model[index]);
if (std::fabs(model[index]) > std::fabs(model[pivot]))
pivot = index;
}
if (!std::isfinite(norm) || norm == 0.0)
return false;
const double sign = model[pivot] < 0.0 ? -1.0 : 1.0;
for (size_t index = 0; index < 9; ++index)
model[index] = normalized_zero(sign * model[index] / norm);
return true;
}
extern "C" Lardon3DGeometricVerifierResult
lardon3d_geometric_verifier_verify_and_publish(
const char *project_path, Lardon3DProjectDb *db, uint64_t match_id,
const Lardon3DGeometricVerifierParameters *p,
Lardon3DProjectDbGeometricVerificationResult *result, bool *reused) {
if (!project_path || !db || match_id == 0 || !p || !result || !reused ||
!lardon3d_geometric_verifier_parameters_valid(p))
return LARDON3D_GEOMETRIC_VERIFIER_INVALID_ARGUMENT;
*reused = false;
unsigned char fingerprint[32];
lardon3d_geometric_verifier_fingerprint(p, fingerprint);
Lardon3DProjectDbResult found =
lardon3d_project_db_find_geometric_verification_result(
db, match_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL,
LARDON3D_GEOMETRIC_VERIFIER_VERSION, fingerprint, result);
if (found == LARDON3D_PROJECT_DB_OK) {
*reused = true;
return LARDON3D_GEOMETRIC_VERIFIER_OK;
}
if (found != LARDON3D_PROJECT_DB_NOT_FOUND)
return LARDON3D_GEOMETRIC_VERIFIER_DATABASE_ERROR;
try {
Lardon3DProjectDbMatchResult parent;
if (lardon3d_project_db_load_match_result(db, match_id, &parent) !=
LARDON3D_PROJECT_DB_OK ||
parent.result_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED ||
!parent.has_match_asset)
return LARDON3D_GEOMETRIC_VERIFIER_NOT_FOUND;
Lardon3DProjectDbFeatureSet set_a, set_b;
if (lardon3d_project_db_load_feature_set(
db, parent.feature_set_id_a, &set_a) != LARDON3D_PROJECT_DB_OK ||
lardon3d_project_db_load_feature_set(db, parent.feature_set_id_b,
&set_b) != LARDON3D_PROJECT_DB_OK)
return LARDON3D_GEOMETRIC_VERIFIER_NOT_FOUND;
char path[4096];
Lardon3DMatchFileHeader header;
if (!join_path(path, project_path, parent.match_asset_path) ||
lardon3d_match_file_validate_asset(
path, parent.match_asset_sha256, parent.match_asset_size_bytes,
&header, set_a.feature_set_id, set_b.feature_set_id,
set_a.feature_count,
set_b.feature_count) != LARDON3D_MATCH_FILE_OK ||
header.match_count != parent.match_count)
return LARDON3D_GEOMETRIC_VERIFIER_CORRUPT;
std::vector<Lardon3DMatchFileEntry> entries(parent.match_count);
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
uint32_t count = 0;
bool read_ok =
fd >= 0 &&
lardon3d_match_file_read(fd, &header, entries.data(), entries.size(),
&count, set_a.feature_set_id,
set_b.feature_set_id, set_a.feature_count,
set_b.feature_count) == LARDON3D_MATCH_FILE_OK;
if (fd >= 0)
(void)close(fd);
if (!read_ok || count != parent.match_count)
return LARDON3D_GEOMETRIC_VERIFIER_CORRUPT;
std::vector<Lardon3DFeatureKeypoint> keypoints_a, keypoints_b;
if (!read_points(project_path, set_a, keypoints_a) ||
!read_points(project_path, set_b, keypoints_b))
return LARDON3D_GEOMETRIC_VERIFIER_CORRUPT;
std::vector<cv::Point2d> points_a, points_b;
points_a.reserve(count);
points_b.reserve(count);
for (const auto &entry : entries) {
points_a.emplace_back(keypoints_a[entry.feature_index_a].x,
keypoints_a[entry.feature_index_a].y);
points_b.emplace_back(keypoints_b[entry.feature_index_b].x,
keypoints_b[entry.feature_index_b].y);
}
cv::Mat mask;
cv::Mat model;
if (count >= LARDON3D_GEOMETRIC_VERIFIER_MINIMUM_MATCHES) {
uint32_t seed = lardon3d_geometric_verifier_seed(
parent.match_asset_sha256, fingerprint);
if (!estimate_fundamental(points_a, points_b, p, seed, model, mask))
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
}
std::vector<unsigned char> bitset((count + 7U) / 8U, 0);
uint32_t inliers = 0;
double canonical[9] = {};
if (!model.empty()) {
if (model.rows != 3 || model.cols != 3 || model.channels() != 1 ||
model.total() != 9 || mask.type() != CV_8U || mask.total() != count)
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
for (uint32_t index = 0; index < count; ++index) {
unsigned char value = mask.ptr<unsigned char>()[index];
if (value > 1)
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
if (value) {
bitset[index / 8U] |= static_cast<unsigned char>(1U << (index % 8U));
++inliers;
}
}
cv::Mat doubles;
model.convertTo(doubles, CV_64F);
std::memcpy(canonical, doubles.ptr<double>(), sizeof(canonical));
if (!lardon3d_geometric_verifier_canonicalize(canonical))
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
} else if (!mask.empty()) {
if (mask.type() != CV_8U || mask.total() != count)
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
for (uint32_t index = 0; index < count; ++index)
if (mask.ptr<unsigned char>()[index] != 0)
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
}
bool accepted = !model.empty() && inliers >= p->min_inlier_count &&
static_cast<double>(inliers) / count >= p->min_inlier_ratio;
int64_t now = static_cast<int64_t>(std::time(nullptr));
if (now < 0)
return LARDON3D_GEOMETRIC_VERIFIER_DATABASE_ERROR;
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TESTING
const char *publication_failure =
std::getenv("LARDON3D_TEST_GEOMETRIC_PUBLICATION_FAILURE");
if (publication_failure && std::strcmp(publication_failure, "1") == 0)
return LARDON3D_GEOMETRIC_VERIFIER_DATABASE_ERROR;
#endif
Lardon3DProjectDbResult created =
lardon3d_project_db_create_geometric_verification_result(
db, match_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL,
LARDON3D_GEOMETRIC_VERIFIER_VERSION, fingerprint,
accepted ? LARDON3D_GEOMETRIC_VERIFIED
: LARDON3D_GEOMETRIC_REJECTED,
inliers, bitset.data(), bitset.size(),
accepted ? canonical : nullptr, now, result);
if (created == LARDON3D_PROJECT_DB_CONSTRAINT) {
Lardon3DProjectDbResult concurrent =
lardon3d_project_db_find_geometric_verification_result(
db, match_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL,
LARDON3D_GEOMETRIC_VERIFIER_VERSION, fingerprint, result);
if (concurrent == LARDON3D_PROJECT_DB_OK) {
*reused = true;
return LARDON3D_GEOMETRIC_VERIFIER_OK;
}
return LARDON3D_GEOMETRIC_VERIFIER_DATABASE_ERROR;
}
return created == LARDON3D_PROJECT_DB_OK
? LARDON3D_GEOMETRIC_VERIFIER_OK
: LARDON3D_GEOMETRIC_VERIFIER_DATABASE_ERROR;
} catch (const std::bad_alloc &) {
return LARDON3D_GEOMETRIC_VERIFIER_OUT_OF_MEMORY;
} catch (const cv::Exception &) {
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
} catch (...) {
return LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR;
}
}

View file

@ -0,0 +1,321 @@
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <lardon3d/geometric_verifier_task.h>
#include <lardon3d/project.h>
#include <lardon3d/task_queue.h>
enum {
GEOMETRIC_VERIFIER_PAGE_CAPACITY =
LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH + 1,
GEOMETRIC_VERIFIER_MEMORY_BYTES = 4 * 1024 * 1024,
GEOMETRIC_VERIFIER_CPU_THREADS = 1,
};
typedef struct {
char project_path[PATH_MAX];
Lardon3DProjectDb *database;
Lardon3DResourceGovernor *governor;
Lardon3DProjectDbGeometricVerifierTask durable;
} Lardon3DGeometricVerifierTaskContext;
static void destroy_context(void *userdata) { free(userdata); }
static Lardon3DGeometricVerifierParameters
parameters_from_durable(const Lardon3DProjectDbGeometricVerifierTask *durable) {
return (Lardon3DGeometricVerifierParameters){
.threshold_pixels = durable->threshold_pixels,
.confidence = durable->confidence,
.max_iterations = durable->max_iterations,
.min_inlier_count = durable->min_inlier_count,
.min_inlier_ratio = durable->min_inlier_ratio,
.seed_policy_version = durable->seed_policy_version,
.canonicalization_version = durable->canonicalization_version,
};
}
static void runtime_state(const Lardon3DGeometricVerifierTaskContext *context,
Lardon3DAppState *state) {
lardon3d_app_state_init(state);
state->project_loaded = true;
state->project_db = context->database;
state->resource_governor = context->governor;
(void)snprintf(state->project_path, sizeof(state->project_path), "%s",
context->project_path);
}
static void finished_callback(const Lardon3DTask *task, void *userdata) {
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING
const char *skip = getenv("LARDON3D_TEST_GEOMETRIC_SKIP_FINISHED_CHECKPOINT");
if (skip && strcmp(skip, "1") == 0) {
return;
}
#endif
Lardon3DGeometricVerifierTaskContext *context = userdata;
Lardon3DAppState state;
runtime_state(context, &state);
(void)lardon3d_project_checkpoint_geometric_verifier_task(&state, task,
&context->durable);
}
static uint64_t elapsed_ns(struct timespec begin, struct timespec end) {
uint64_t seconds =
end.tv_sec >= begin.tv_sec ? (uint64_t)(end.tv_sec - begin.tv_sec) : 0;
long nanoseconds = end.tv_nsec - begin.tv_nsec;
if (nanoseconds < 0 && seconds > 0) {
--seconds;
nanoseconds += 1000000000L;
}
return seconds * 1000000000ULL + (uint64_t)nanoseconds;
}
static bool process_parent(Lardon3DTask *task,
Lardon3DGeometricVerifierTaskContext *context,
const Lardon3DProjectDbMatchResult *parent) {
if (!lardon3d_task_checkpoint(task)) {
return false;
}
if (parent->result_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED ||
parent->match_count == 0) {
return true;
}
Lardon3DGeometricVerifierParameters parameters =
parameters_from_durable(&context->durable);
Lardon3DProjectDbGeometricVerificationResult result;
bool reused = false;
if (lardon3d_geometric_verifier_verify_and_publish(
context->project_path, context->database, parent->match_result_id,
&parameters, &result, &reused) != LARDON3D_GEOMETRIC_VERIFIER_OK) {
return lardon3d_task_fail(task, "Vérification géométrique impossible.");
}
#ifdef LARDON3D_GEOMETRIC_VERIFIER_TASK_TESTING
const char *pause = getenv("LARDON3D_TEST_GEOMETRIC_PAUSE_AFTER_PUBLICATION");
if (pause && strcmp(pause, "1") == 0) {
(void)lardon3d_task_pause(task);
return lardon3d_task_checkpoint(task);
}
#endif
return true;
}
static bool checkpoint_batch(Lardon3DTask *task,
Lardon3DGeometricVerifierTaskContext *context,
unsigned int progress, uint64_t processed) {
char message[LARDON3D_TASK_MESSAGE_CAPACITY];
(void)snprintf(message, sizeof(message), "Match Results traités:%lu",
(unsigned long)processed);
if (!lardon3d_task_set_progress(task, progress, message)) {
return false;
}
Lardon3DAppState state;
runtime_state(context, &state);
return lardon3d_project_checkpoint_geometric_verifier_task(
&state, task, &context->durable) ==
LARDON3D_PROJECT_TASK_CHECKPOINT_OK;
}
static bool run(Lardon3DTask *task, void *userdata) {
Lardon3DGeometricVerifierTaskContext *context = userdata;
uint64_t processed = 0;
for (;;) {
if (!lardon3d_task_checkpoint(task)) {
return false;
}
Lardon3DTaskExecutionContract contract;
if (!lardon3d_task_execution_contract(task, &contract) ||
contract.batch_size < LARDON3D_GEOMETRIC_VERIFIER_TASK_MINIMUM_BATCH ||
contract.batch_size > LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH) {
return lardon3d_task_fail(task,
"Contrat de lot Geometric Verifier invalide.");
}
Lardon3DProjectDbMatchResult page[GEOMETRIC_VERIFIER_PAGE_CAPACITY];
size_t count = 0;
size_t capacity = contract.batch_size + 1;
if (lardon3d_project_db_list_match_results(
context->database, context->durable.after_match_result_id, page,
capacity, &count) != LARDON3D_PROJECT_DB_OK) {
return lardon3d_task_fail(task, "Pagination Match Result impossible.");
}
if (count == 0) {
return lardon3d_task_set_progress(task, 100,
"Vérification géométrique terminée.");
}
size_t batch_count =
count < contract.batch_size ? count : contract.batch_size;
struct timespec begin;
struct timespec end;
(void)clock_gettime(CLOCK_MONOTONIC, &begin);
for (size_t index = 0; index < batch_count; ++index) {
if (!process_parent(task, context, &page[index])) {
return false;
}
context->durable.after_match_result_id = page[index].match_result_id;
++processed;
}
(void)clock_gettime(CLOCK_MONOTONIC, &end);
(void)lardon3d_resource_governor_record_batch(
context->governor, LARDON3D_RESOURCE_TASK_CPU, batch_count,
elapsed_ns(begin, end), 0);
bool exhausted = count <= contract.batch_size;
if (!checkpoint_batch(task, context, exhausted ? 100U : 99U, processed)) {
return lardon3d_task_fail(task,
"Checkpoint Geometric Verifier impossible.");
}
if (exhausted) {
return lardon3d_task_set_progress(task, 100,
"Vérification géométrique terminée.");
}
Lardon3DResourceReservation *reservation = NULL;
if (!lardon3d_task_sequence_break(task, context->governor, &reservation,
&contract)) {
return false;
}
}
}
static Lardon3DGeometricVerifierTaskContext *
make_context(const Lardon3DTaskReconstructionContext *runtime,
const Lardon3DProjectDbGeometricVerifierTask *durable) {
if (!runtime || !runtime->project_path || !runtime->project_db ||
!runtime->resource_governor || !durable) {
return NULL;
}
Lardon3DGeometricVerifierParameters parameters =
parameters_from_durable(durable);
unsigned char fingerprint[32];
lardon3d_geometric_verifier_fingerprint(&parameters, fingerprint);
if (!lardon3d_geometric_verifier_parameters_valid(&parameters) ||
memcmp(fingerprint, durable->parameter_fingerprint,
sizeof(fingerprint)) != 0) {
return NULL;
}
Lardon3DGeometricVerifierTaskContext *context = calloc(1, sizeof(*context));
if (!context) {
return NULL;
}
int written = snprintf(context->project_path, sizeof(context->project_path),
"%s", runtime->project_path);
if (written <= 0 || (size_t)written >= sizeof(context->project_path)) {
free(context);
return NULL;
}
context->database = runtime->project_db;
context->governor = runtime->resource_governor;
context->durable = *durable;
return context;
}
bool lardon3d_geometric_verifier_task_reconstruct(
const Lardon3DTaskDurableSnapshot *snapshot, void *userdata,
Lardon3DTaskKindBinding *binding) {
Lardon3DTaskReconstructionContext *runtime = userdata;
if (!snapshot || !runtime || !binding) {
return false;
}
Lardon3DProjectDbGeometricVerifierTask durable;
if (lardon3d_project_db_load_geometric_verifier_task(
runtime->project_db, snapshot->id, &durable) !=
LARDON3D_PROJECT_DB_OK) {
return false;
}
Lardon3DGeometricVerifierTaskContext *context =
make_context(runtime, &durable);
if (!context) {
return false;
}
*binding = (Lardon3DTaskKindBinding){
.callback = run,
.userdata = context,
.userdata_destroy = destroy_context,
.finished_callback = finished_callback,
.finished_userdata = context,
};
return true;
}
Lardon3DTask *lardon3d_project_create_geometric_verifier_task(
Lardon3DAppState *state,
const Lardon3DGeometricVerifierTaskConfiguration *configuration,
uint64_t *task_id) {
if (task_id) {
*task_id = 0;
}
if (!state || !state->project_loaded || !state->project_db ||
!state->resource_governor || !configuration || !task_id ||
!lardon3d_geometric_verifier_parameters_valid(&configuration->verifier)) {
return NULL;
}
uint64_t id = 0;
if (lardon3d_project_db_allocate_task_id(state->project_db, &id) !=
LARDON3D_PROJECT_DB_OK) {
return NULL;
}
Lardon3DProjectDbGeometricVerifierTask durable = {
.task_id = id,
.threshold_pixels = configuration->verifier.threshold_pixels,
.confidence = configuration->verifier.confidence,
.max_iterations = configuration->verifier.max_iterations,
.min_inlier_count = configuration->verifier.min_inlier_count,
.min_inlier_ratio = configuration->verifier.min_inlier_ratio,
.seed_policy_version = configuration->verifier.seed_policy_version,
.canonicalization_version =
configuration->verifier.canonicalization_version,
};
lardon3d_geometric_verifier_fingerprint(&configuration->verifier,
durable.parameter_fingerprint);
Lardon3DTaskReconstructionContext runtime = {
.project_path = state->project_path,
.project_db = state->project_db,
.resource_governor = state->resource_governor,
};
Lardon3DGeometricVerifierTaskContext *context =
make_context(&runtime, &durable);
if (!context) {
return NULL;
}
const Lardon3DResourceEstimate estimate = {
.memory_fixed_bytes = GEOMETRIC_VERIFIER_MEMORY_BYTES,
.minimum_batch_size = LARDON3D_GEOMETRIC_VERIFIER_TASK_MINIMUM_BATCH,
.maximum_batch_size = LARDON3D_GEOMETRIC_VERIFIER_TASK_MAXIMUM_BATCH,
.desired_cpu_threads = GEOMETRIC_VERIFIER_CPU_THREADS,
.desired_io_slots = 1,
.task_class = LARDON3D_RESOURCE_TASK_CPU,
};
Lardon3DTask *task =
lardon3d_task_create_typed("Geometric Verification", &estimate,
LARDON3D_GEOMETRIC_VERIFIER_TASK_KIND,
LARDON3D_GEOMETRIC_VERIFIER_TASK_KIND_VERSION,
run, context, destroy_context);
if (!task || !lardon3d_task_assign_id(task, id) ||
!lardon3d_task_set_finished_callback(task, finished_callback, context) ||
lardon3d_project_checkpoint_geometric_verifier_task(
state, task, &durable) != LARDON3D_PROJECT_TASK_CHECKPOINT_OK) {
lardon3d_task_destroy(task);
return NULL;
}
*task_id = id;
return task;
}
bool lardon3d_project_enqueue_geometric_verifier_task(
Lardon3DAppState *state,
const Lardon3DGeometricVerifierTaskConfiguration *configuration,
uint64_t *task_id) {
if (!state || !state->task_queue) {
return false;
}
Lardon3DTask *task = lardon3d_project_create_geometric_verifier_task(
state, configuration, task_id);
if (!task) {
return false;
}
if (!lardon3d_task_queue_add(state->task_queue, task, NULL)) {
lardon3d_task_destroy(task);
return false;
}
return true;
}

View file

@ -673,7 +673,8 @@ checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbSiftExtractTask *sift_parameters, const Lardon3DProjectDbSiftExtractTask *sift_parameters,
const Lardon3DProjectDbVisualIndexUpdateTask *visual_parameters, const Lardon3DProjectDbVisualIndexUpdateTask *visual_parameters,
const Lardon3DProjectDbCandidatePairGenerateTask *candidate_parameters, const Lardon3DProjectDbCandidatePairGenerateTask *candidate_parameters,
const Lardon3DProjectDbMatcherTask *matcher_parameters) { const Lardon3DProjectDbMatcherTask *matcher_parameters,
const Lardon3DProjectDbGeometricVerifierTask *geometric_parameters) {
if (!state || !state->project_loaded || !state->project_db) { if (!state || !state->project_loaded || !state->project_db) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_NO_PROJECT; return LARDON3D_PROJECT_TASK_CHECKPOINT_NO_PROJECT;
} }
@ -736,6 +737,10 @@ checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task,
? lardon3d_project_db_record_matcher_task( ? lardon3d_project_db_record_matcher_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint, state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
matcher_parameters, now.tv_sec) matcher_parameters, now.tv_sec)
: geometric_parameters
? lardon3d_project_db_record_geometric_verifier_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
geometric_parameters, now.tv_sec)
: lardon3d_project_db_record_task(state->project_db, &snapshot, task_kind, : lardon3d_project_db_record_task(state->project_db, &snapshot, task_kind,
task_kind_version, &checkpoint, now.tv_sec); task_kind_version, &checkpoint, now.tv_sec);
if (recorded == LARDON3D_PROJECT_DB_BUSY) { if (recorded == LARDON3D_PROJECT_DB_BUSY) {
@ -751,7 +756,7 @@ checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task,
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_task(Lardon3DAppState *state, Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_task(Lardon3DAppState *state,
const Lardon3DTask *task) { const Lardon3DTask *task) {
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL); return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult Lardon3DProjectTaskCheckpointResult
@ -761,7 +766,7 @@ lardon3d_project_checkpoint_image_import_task(Lardon3DAppState *state, const Lar
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal( return checkpoint_task_internal(
state, task, source_path, scanset_id, NULL, NULL, NULL, NULL, NULL); state, task, source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_task(
@ -770,14 +775,14 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_
if (!parameters) { if (!parameters) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, parameters, NULL, NULL, NULL, NULL); return checkpoint_task_internal(state, task, NULL, 0, parameters, NULL, NULL, NULL, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_sift_extract_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_sift_extract_task(
Lardon3DAppState *state, const Lardon3DTask *task, Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbSiftExtractTask *parameters) { const Lardon3DProjectDbSiftExtractTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, parameters, NULL, NULL, NULL); return checkpoint_task_internal(state, task, NULL, 0, NULL, parameters, NULL, NULL, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_visual_index_update_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_visual_index_update_task(
@ -786,7 +791,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_visual_index_upd
if (!parameters) { if (!parameters) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, parameters, NULL, NULL); return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, parameters, NULL, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_generate_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_generate_task(
@ -795,7 +800,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_g
if (!parameters) { if (!parameters) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, parameters, NULL); return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, parameters, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task(
@ -804,7 +809,17 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task(
if (!parameters) { if (!parameters) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, parameters); return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, parameters, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_geometric_verifier_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbGeometricVerifierTask *parameters) {
if (!parameters) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
}
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL,
parameters);
} }
static bool coherent_recovery(const Lardon3DProjectDbTask *database_task, static bool coherent_recovery(const Lardon3DProjectDbTask *database_task,

View file

@ -263,6 +263,19 @@ static const char schema_geometric_verification_v12[] =
"CREATE INDEX geometric_verification_results_parent_idx ON " "CREATE INDEX geometric_verification_results_parent_idx ON "
"geometric_verification_results(match_result_id,geometric_verification_result_id);"; "geometric_verification_results(match_result_id,geometric_verification_result_id);";
static const char schema_geometric_verifier_task_v13[] =
"CREATE TABLE geometric_verifier_tasks("
"task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
"after_match_result_id INTEGER NOT NULL CHECK(after_match_result_id>=0),"
"threshold_pixels REAL NOT NULL CHECK(threshold_pixels>0.0),"
"confidence REAL NOT NULL CHECK(confidence>0.0 AND confidence<1.0),"
"max_iterations INTEGER NOT NULL CHECK(max_iterations>0 AND max_iterations<=2147483647),"
"min_inlier_count INTEGER NOT NULL CHECK(min_inlier_count BETWEEN 1 AND 8192),"
"min_inlier_ratio REAL NOT NULL CHECK(min_inlier_ratio>=0.0 AND min_inlier_ratio<=1.0),"
"seed_policy_version INTEGER NOT NULL CHECK(seed_policy_version>0),"
"canonicalization_version INTEGER NOT NULL CHECK(canonicalization_version>0),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32));";
static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) { static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) {
if (destination) { if (destination) {
(void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : ""); (void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : "");
@ -352,7 +365,8 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
} }
if (from_version != 0 && from_version != 1 && from_version != 2 && from_version != 3 && 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 != 4 && from_version != 5 && from_version != 6 && from_version != 7 &&
from_version != 8 && from_version != 9 && from_version != 10 && from_version != 11) { from_version != 8 && from_version != 9 && from_version != 10 && from_version != 11 &&
from_version != 12) {
return LARDON3D_PROJECT_DB_CORRUPT; return LARDON3D_PROJECT_DB_CORRUPT;
} }
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration"); Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
@ -609,6 +623,22 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
"finish schema v12 migration"); "finish schema v12 migration");
} }
} }
if (result == LARDON3D_PROJECT_DB_OK && from_version < 13) {
result = execute(database, schema_geometric_verifier_task_v13,
"migrate schema v12 to v13");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V13");
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 v13 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=13 WHERE key='schema_version' AND value=12",
"finish schema v13 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK) { if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit migration"); result = execute(database, "COMMIT", "commit migration");
} }
@ -724,7 +754,8 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje
"candidate_pairs", "candidate_pairs",
"matcher_tasks", "matcher_tasks",
"match_results", "match_results",
"geometric_verification_results"}; "geometric_verification_results",
"geometric_verifier_tasks"};
for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) && for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) &&
result == LARDON3D_PROJECT_DB_OK; result == LARDON3D_PROJECT_DB_OK;
++index) { ++index) {
@ -916,6 +947,7 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
const Lardon3DProjectDbVisualIndexUpdateTask *visual, const Lardon3DProjectDbVisualIndexUpdateTask *visual,
const Lardon3DProjectDbCandidatePairGenerateTask *candidate_pair, const Lardon3DProjectDbCandidatePairGenerateTask *candidate_pair,
const Lardon3DProjectDbMatcherTask *matcher, const Lardon3DProjectDbMatcherTask *matcher,
const Lardon3DProjectDbGeometricVerifierTask *geometric_verifier,
int64_t updated_at) { int64_t updated_at) {
bool typed = task_kind != NULL; bool typed = task_kind != NULL;
if (!database || !valid_durable_task(snapshot, updated_at) || if (!database || !valid_durable_task(snapshot, updated_at) ||
@ -959,6 +991,23 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
matcher->feature_extractor_version == 0 || matcher->matcher_kind < 0 || matcher->feature_extractor_version == 0 || matcher->matcher_kind < 0 ||
matcher->matcher_kind > 2 || !isfinite(matcher->ratio_threshold) || matcher->matcher_kind > 2 || !isfinite(matcher->ratio_threshold) ||
matcher->ratio_threshold <= 0.0F || matcher->ratio_threshold >= 1.0F)) || matcher->ratio_threshold <= 0.0F || matcher->ratio_threshold >= 1.0F)) ||
(geometric_verifier &&
(!valid_task_id(geometric_verifier->task_id) ||
geometric_verifier->task_id != snapshot->id ||
geometric_verifier->after_match_result_id > INT64_MAX ||
!isfinite(geometric_verifier->threshold_pixels) ||
geometric_verifier->threshold_pixels <= 0.0 ||
!isfinite(geometric_verifier->confidence) ||
geometric_verifier->confidence <= 0.0 || geometric_verifier->confidence >= 1.0 ||
geometric_verifier->max_iterations == 0 ||
geometric_verifier->max_iterations > INT_MAX ||
geometric_verifier->min_inlier_count == 0 ||
geometric_verifier->min_inlier_count > 8192 ||
!isfinite(geometric_verifier->min_inlier_ratio) ||
geometric_verifier->min_inlier_ratio < 0.0 ||
geometric_verifier->min_inlier_ratio > 1.0 ||
geometric_verifier->seed_policy_version == 0 ||
geometric_verifier->canonicalization_version == 0)) ||
(checkpoint && !valid_checkpoint(checkpoint))) { (checkpoint && !valid_checkpoint(checkpoint))) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
@ -1155,6 +1204,42 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
} }
} }
} }
if (result == LARDON3D_PROJECT_DB_OK && geometric_verifier) {
result = prepare(
database,
"INSERT INTO geometric_verifier_tasks(task_id,after_match_result_id,threshold_pixels,"
"confidence,max_iterations,min_inlier_count,min_inlier_ratio,seed_policy_version,"
"canonicalization_version,parameter_fingerprint) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,"
"?9,?10) ON CONFLICT(task_id) DO UPDATE SET after_match_result_id="
"excluded.after_match_result_id WHERE geometric_verifier_tasks.threshold_pixels="
"excluded.threshold_pixels AND geometric_verifier_tasks.confidence=excluded.confidence "
"AND geometric_verifier_tasks.max_iterations=excluded.max_iterations AND "
"geometric_verifier_tasks.min_inlier_count=excluded.min_inlier_count AND "
"geometric_verifier_tasks.min_inlier_ratio=excluded.min_inlier_ratio AND "
"geometric_verifier_tasks.seed_policy_version=excluded.seed_policy_version AND "
"geometric_verifier_tasks.canonicalization_version=excluded.canonicalization_version "
"AND geometric_verifier_tasks.parameter_fingerprint=excluded.parameter_fingerprint",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)geometric_verifier->task_id);
sqlite3_bind_int64(statement, 2,
(sqlite3_int64)geometric_verifier->after_match_result_id);
sqlite3_bind_double(statement, 3, geometric_verifier->threshold_pixels);
sqlite3_bind_double(statement, 4, geometric_verifier->confidence);
sqlite3_bind_int64(statement, 5, geometric_verifier->max_iterations);
sqlite3_bind_int64(statement, 6, geometric_verifier->min_inlier_count);
sqlite3_bind_double(statement, 7, geometric_verifier->min_inlier_ratio);
sqlite3_bind_int64(statement, 8, geometric_verifier->seed_policy_version);
sqlite3_bind_int64(statement, 9, geometric_verifier->canonicalization_version);
sqlite3_bind_blob(statement, 10, geometric_verifier->parameter_fingerprint, 32,
SQLITE_TRANSIENT);
result = step_done(database, statement, "upsert geometric verifier task");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Configuration Geometric Verifier de tâche immuable.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK && sift) { if (result == LARDON3D_PROJECT_DB_OK && sift) {
result = prepare( result = prepare(
database, database,
@ -1224,7 +1309,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, const char *task_kind, Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, const char *task_kind,
uint32_t task_kind_version, const Lardon3DProjectDbCheckpoint *checkpoint, int64_t updated_at) { uint32_t task_kind_version, const Lardon3DProjectDbCheckpoint *checkpoint, int64_t updated_at) {
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
NULL, NULL, NULL, NULL, NULL, updated_at); NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task( Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task(
@ -1235,7 +1320,8 @@ Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint,
source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, updated_at); source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL,
updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task( Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task(
@ -1246,7 +1332,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
parameters, NULL, NULL, NULL, NULL, updated_at); parameters, NULL, NULL, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task( Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task(
@ -1256,7 +1342,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task(
const Lardon3DProjectDbSiftExtractTask *parameters, int64_t updated_at) { const Lardon3DProjectDbSiftExtractTask *parameters, int64_t updated_at) {
if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
NULL, parameters, NULL, NULL, NULL, updated_at); NULL, parameters, NULL, NULL, NULL, NULL, updated_at);
} }
static bool read_task(sqlite3_stmt *statement, Lardon3DProjectDbTask *task) { static bool read_task(sqlite3_stmt *statement, Lardon3DProjectDbTask *task) {
@ -3238,7 +3324,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_visual_index_update_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, parameters, NULL, NULL, updated_at); NULL, parameters, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_load_visual_index_update_task( Lardon3DProjectDbResult lardon3d_project_db_load_visual_index_update_task(
@ -3285,7 +3371,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_candidate_pair_generate_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, parameters, NULL, updated_at); NULL, NULL, parameters, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task( Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task(
@ -3341,7 +3427,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_matcher_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, NULL, parameters, updated_at); NULL, NULL, NULL, parameters, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_load_matcher_task( Lardon3DProjectDbResult lardon3d_project_db_load_matcher_task(
@ -3390,6 +3476,73 @@ Lardon3DProjectDbResult lardon3d_project_db_load_matcher_task(
return result; return result;
} }
Lardon3DProjectDbResult lardon3d_project_db_record_geometric_verifier_task(
Lardon3DProjectDb *db, const Lardon3DTaskDurableSnapshot *snapshot,
const char *kind, uint32_t version,
const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbGeometricVerifierTask *parameters, int64_t updated_at) {
if (!snapshot || !parameters || parameters->task_id != snapshot->id) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, NULL, NULL, parameters, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verifier_task(
Lardon3DProjectDb *db, uint64_t task_id,
Lardon3DProjectDbGeometricVerifierTask *parameters) {
if (!db || !valid_task_id(task_id) || !parameters) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(parameters, 0, sizeof(*parameters));
(void)pthread_mutex_lock(&db->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
db,
"SELECT after_match_result_id,threshold_pixels,confidence,max_iterations,"
"min_inlier_count,min_inlier_ratio,seed_policy_version,canonicalization_version,"
"parameter_fingerprint FROM geometric_verifier_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);
sqlite3_int64 after = sqlite3_column_int64(statement, 0);
double threshold = sqlite3_column_double(statement, 1);
double confidence = sqlite3_column_double(statement, 2);
sqlite3_int64 iterations = sqlite3_column_int64(statement, 3);
sqlite3_int64 min_count = sqlite3_column_int64(statement, 4);
double min_ratio = sqlite3_column_double(statement, 5);
sqlite3_int64 seed_version = sqlite3_column_int64(statement, 6);
sqlite3_int64 canonical_version = sqlite3_column_int64(statement, 7);
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || after < 0 || !isfinite(threshold) || threshold <= 0.0 ||
!isfinite(confidence) || confidence <= 0.0 || confidence >= 1.0 ||
iterations <= 0 || iterations > INT_MAX || min_count <= 0 ||
min_count > 8192 || !isfinite(min_ratio) || min_ratio < 0.0 ||
min_ratio > 1.0 || seed_version <= 0 || seed_version > UINT32_MAX ||
canonical_version <= 0 || canonical_version > UINT32_MAX ||
sqlite3_column_bytes(statement, 8) != LARDON3D_PROJECT_DB_SHA256_SIZE) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
parameters->task_id = task_id;
parameters->after_match_result_id = (uint64_t)after;
parameters->threshold_pixels = threshold;
parameters->confidence = confidence;
parameters->max_iterations = (uint32_t)iterations;
parameters->min_inlier_count = (uint32_t)min_count;
parameters->min_inlier_ratio = min_ratio;
parameters->seed_policy_version = (uint32_t)seed_version;
parameters->canonicalization_version = (uint32_t)canonical_version;
memcpy(parameters->parameter_fingerprint, sqlite3_column_blob(statement, 8),
LARDON3D_PROJECT_DB_SHA256_SIZE);
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&db->mutex);
return result;
}
static bool read_match_result(sqlite3_stmt *statement, static bool read_match_result(sqlite3_stmt *statement,
Lardon3DProjectDbMatchResult *result) { Lardon3DProjectDbMatchResult *result) {
sqlite3_int64 id = sqlite3_column_int64(statement, 0); sqlite3_int64 id = sqlite3_column_int64(statement, 0);

View file

@ -1,5 +1,6 @@
#include <lardon3d/candidate_pair_task.h> #include <lardon3d/candidate_pair_task.h>
#include <lardon3d/feature_task.h> #include <lardon3d/feature_task.h>
#include <lardon3d/geometric_verifier_task.h>
#include <lardon3d/import_task.h> #include <lardon3d/import_task.h>
#include <lardon3d/matcher_task.h> #include <lardon3d/matcher_task.h>
#include <lardon3d/sift_task.h> #include <lardon3d/sift_task.h>
@ -42,6 +43,11 @@ const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) {
.kind = LARDON3D_MATCHER_TASK_KIND, .kind = LARDON3D_MATCHER_TASK_KIND,
.kind_version = LARDON3D_MATCHER_TASK_KIND_VERSION, .kind_version = LARDON3D_MATCHER_TASK_KIND_VERSION,
.reconstruct = lardon3d_matcher_task_reconstruct, .reconstruct = lardon3d_matcher_task_reconstruct,
},
{
.kind = LARDON3D_GEOMETRIC_VERIFIER_TASK_KIND,
.kind_version = LARDON3D_GEOMETRIC_VERIFIER_TASK_KIND_VERSION,
.reconstruct = lardon3d_geometric_verifier_task_reconstruct,
}}; }};
static const Lardon3DTaskKindRegistry registry = { static const Lardon3DTaskKindRegistry registry = {
.descriptors = descriptors, .descriptors = descriptors,

View file

@ -0,0 +1,455 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <numeric>
#include <opencv2/calib3d.hpp>
#include <opencv2/core/version.hpp>
#include <string>
#include <vector>
namespace {
struct Generator {
uint64_t state;
uint32_t next_u32() {
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
return static_cast<uint32_t>((state * 0x2545f4914f6cdd1dULL) >> 32);
}
double uniform() { return static_cast<double>(next_u32()) / 4294967296.0; }
double normal() {
const double u1 = std::max(uniform(), 1e-12);
const double u2 = uniform();
return std::sqrt(-2.0 * std::log(u1)) * std::cos(2.0 * CV_PI * u2);
}
};
enum class Geometry {
Healthy,
WeakBaseline,
WideBaseline,
Concentrated,
NearCollinear,
Planar,
RotationDominant,
Duplicated,
};
struct Scenario {
const char *name;
Geometry geometry;
int width;
int height;
int count;
double noise;
double outlier_ratio;
};
struct Corpus {
std::vector<cv::Point2d> first;
std::vector<cv::Point2d> second;
std::vector<unsigned char> truth;
cv::Mat fundamental;
};
struct Measurement {
bool model_found = false;
double precision = 0.0;
double recall = 0.0;
double median_sampson = 0.0;
double elapsed_ms = 0.0;
double median_ms = 0.0;
double p95_ms = 0.0;
double worst_ms = 0.0;
uint64_t output_hash = 0;
int inlier_count = 0;
};
enum class Method {
Classic,
UsacDefault,
UsacMagsac,
UsacAccurate,
SeededMagsac,
};
const char *method_name(Method method) {
switch (method) {
case Method::Classic:
return "FM_RANSAC";
case Method::UsacDefault:
return "USAC_DEFAULT";
case Method::UsacMagsac:
return "USAC_MAGSAC";
case Method::UsacAccurate:
return "USAC_ACCURATE";
case Method::SeededMagsac:
return "USAC_MAGSAC_LOCAL_SEED";
}
return "UNKNOWN";
}
cv::Mat rotation_y(double angle) {
return (cv::Mat_<double>(3, 3) << std::cos(angle), 0.0, std::sin(angle), 0.0,
1.0, 0.0, -std::sin(angle), 0.0, std::cos(angle));
}
cv::Mat skew(const cv::Vec3d &translation) {
return (cv::Mat_<double>(3, 3) << 0.0, -translation[2], translation[1],
translation[2], 0.0, -translation[0], -translation[1], translation[0],
0.0);
}
cv::Point2d project(const cv::Mat &rotation, const cv::Vec3d &translation,
const cv::Vec3d &point, double focal, double cx,
double cy) {
cv::Mat transformed = rotation * cv::Mat(point) + cv::Mat(translation);
const double x = transformed.at<double>(0) / transformed.at<double>(2);
const double y = transformed.at<double>(1) / transformed.at<double>(2);
return {focal * x + cx, focal * y + cy};
}
Corpus generate(const Scenario &scenario, uint64_t seed) {
Generator generator{seed};
const double focal =
0.85 * static_cast<double>(std::max(scenario.width, scenario.height));
const double cx = 0.5 * scenario.width;
const double cy = 0.5 * scenario.height;
double baseline = 0.35;
double angle = 0.08;
if (scenario.geometry == Geometry::WeakBaseline)
baseline = 0.025;
if (scenario.geometry == Geometry::WideBaseline)
baseline = 1.0;
if (scenario.geometry == Geometry::RotationDominant) {
baseline = 0.005;
angle = 0.25;
}
const cv::Mat rotation = rotation_y(angle);
const cv::Vec3d translation(baseline, 0.015, 0.01);
const cv::Mat intrinsic =
(cv::Mat_<double>(3, 3) << focal, 0.0, cx, 0.0, focal, cy, 0.0, 0.0, 1.0);
const cv::Mat fundamental =
intrinsic.inv().t() * skew(translation) * rotation * intrinsic.inv();
Corpus corpus;
corpus.fundamental = fundamental;
corpus.first.reserve(scenario.count);
corpus.second.reserve(scenario.count);
corpus.truth.assign(scenario.count, 1);
for (int index = 0; index < scenario.count; ++index) {
double x = 3.0 * (generator.uniform() - 0.5);
double y = 2.0 * (generator.uniform() - 0.5);
double z = 4.0 + 5.0 * generator.uniform();
if (scenario.geometry == Geometry::Concentrated) {
x *= 0.12;
y *= 0.12;
} else if (scenario.geometry == Geometry::NearCollinear) {
y = 0.015 * x + 0.002 * generator.normal();
} else if (scenario.geometry == Geometry::Planar) {
z = 6.0;
} else if (scenario.geometry == Geometry::Duplicated &&
index > scenario.count / 2) {
x = 0.1;
y = -0.1;
z = 6.0;
}
const cv::Vec3d point(x, y, z);
cv::Point2d first =
project(cv::Mat::eye(3, 3, CV_64F), cv::Vec3d(), point, focal, cx, cy);
cv::Point2d second = project(rotation, translation, point, focal, cx, cy);
first.x += scenario.noise * generator.normal();
first.y += scenario.noise * generator.normal();
second.x += scenario.noise * generator.normal();
second.y += scenario.noise * generator.normal();
corpus.first.push_back(first);
corpus.second.push_back(second);
}
const int outlier_count =
static_cast<int>(std::lround(scenario.count * scenario.outlier_ratio));
for (int index = 0; index < outlier_count; ++index) {
const int target = scenario.count - 1 - index;
corpus.second[target] = {scenario.width * generator.uniform(),
scenario.height * generator.uniform()};
corpus.truth[target] = 0;
}
return corpus;
}
double sampson(const cv::Mat &fundamental, const cv::Point2d &first,
const cv::Point2d &second) {
const cv::Mat x1 = (cv::Mat_<double>(3, 1) << first.x, first.y, 1.0);
const cv::Mat x2 = (cv::Mat_<double>(3, 1) << second.x, second.y, 1.0);
const cv::Mat line2 = fundamental * x1;
const cv::Mat line1 = fundamental.t() * x2;
const double residual = x2.dot(line2);
const double denominator = line1.at<double>(0) * line1.at<double>(0) +
line1.at<double>(1) * line1.at<double>(1) +
line2.at<double>(0) * line2.at<double>(0) +
line2.at<double>(1) * line2.at<double>(1);
return denominator > 0.0 ? residual * residual / denominator : INFINITY;
}
uint64_t hash_output(const cv::Mat &model, const cv::Mat &mask) {
uint64_t hash = 1469598103934665603ULL;
auto mix = [&](const unsigned char *bytes, size_t size) {
for (size_t index = 0; index < size; ++index) {
hash ^= bytes[index];
hash *= 1099511628211ULL;
}
};
if (!model.empty()) {
cv::Mat doubles;
model.convertTo(doubles, CV_64F);
mix(doubles.ptr<unsigned char>(), doubles.total() * doubles.elemSize());
}
if (!mask.empty())
mix(mask.ptr<unsigned char>(), mask.total() * mask.elemSize());
return hash;
}
cv::Mat estimate(Method method, const std::vector<cv::Point2d> &first,
const std::vector<cv::Point2d> &second, double threshold,
int seed, cv::Mat &mask) {
if (method == Method::SeededMagsac) {
cv::UsacParams params;
params.confidence = 0.999;
params.maxIterations = 5000;
params.randomGeneratorState = seed;
params.sampler = cv::SAMPLING_UNIFORM;
params.score = cv::SCORE_METHOD_MAGSAC;
params.threshold = threshold;
params.isParallel = false;
return cv::findFundamentalMat(first, second, mask, params);
}
int flag = cv::FM_RANSAC;
if (method == Method::UsacDefault)
flag = cv::USAC_DEFAULT;
if (method == Method::UsacMagsac)
flag = cv::USAC_MAGSAC;
if (method == Method::UsacAccurate)
flag = cv::USAC_ACCURATE;
return cv::findFundamentalMat(first, second, flag, threshold, 0.999, 5000,
mask);
}
Measurement measure(Method method, const Corpus &corpus, double threshold,
int seed, bool use_float) {
cv::Mat mask;
const auto start = std::chrono::steady_clock::now();
cv::Mat model;
try {
if (use_float) {
std::vector<cv::Point2f> first(corpus.first.begin(), corpus.first.end());
std::vector<cv::Point2f> second(corpus.second.begin(),
corpus.second.end());
model =
estimate(method, std::vector<cv::Point2d>(first.begin(), first.end()),
std::vector<cv::Point2d>(second.begin(), second.end()),
threshold, seed, mask);
} else {
model =
estimate(method, corpus.first, corpus.second, threshold, seed, mask);
}
} catch (const cv::Exception &) {
model.release();
mask.release();
}
const auto stop = std::chrono::steady_clock::now();
Measurement result;
result.elapsed_ms =
std::chrono::duration<double, std::milli>(stop - start).count();
result.model_found =
model.rows == 3 && model.cols == 3 && mask.total() == corpus.truth.size();
result.output_hash = hash_output(model, mask);
if (!result.model_found)
return result;
int true_positive = 0;
int false_positive = 0;
int false_negative = 0;
std::vector<double> errors;
for (size_t index = 0; index < corpus.truth.size(); ++index) {
const bool selected = mask.ptr<unsigned char>()[index] != 0;
if (selected)
++result.inlier_count;
if (selected && corpus.truth[index])
++true_positive;
if (selected && !corpus.truth[index])
++false_positive;
if (!selected && corpus.truth[index])
++false_negative;
if (corpus.truth[index])
errors.push_back(
sampson(model, corpus.first[index], corpus.second[index]));
}
result.precision = true_positive + false_positive
? static_cast<double>(true_positive) /
(true_positive + false_positive)
: 0.0;
result.recall = true_positive + false_negative
? static_cast<double>(true_positive) /
(true_positive + false_negative)
: 0.0;
std::sort(errors.begin(), errors.end());
result.median_sampson = errors.empty() ? 0.0 : errors[errors.size() / 2];
return result;
}
void print_measurement(const Scenario &scenario, Method method,
double threshold, const Measurement &measurement,
bool repeatable, const char *precision) {
std::printf(
"%s,%s,%s,%d,%.2f,%.2f,%.2f,%d,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f,%s\n",
scenario.name, method_name(method), precision, scenario.count,
scenario.noise, scenario.outlier_ratio, threshold,
measurement.model_found ? 1 : 0, measurement.precision,
measurement.recall, measurement.median_sampson, measurement.median_ms,
measurement.p95_ms, measurement.worst_ms, repeatable ? "yes" : "no");
}
} // namespace
int main(int argc, char **argv) {
const std::array<Scenario, 20> scenarios = {
{{"healthy_exact", Geometry::Healthy, 1920, 1080, 256, 0.0, 0.0},
{"healthy_low", Geometry::Healthy, 1920, 1080, 256, 0.35, 0.1},
{"healthy_medium", Geometry::Healthy, 1920, 1080, 1024, 0.75, 0.3},
{"healthy_high", Geometry::Healthy, 4000, 3000, 1024, 1.5, 0.5},
{"outliers_70", Geometry::Healthy, 1920, 1080, 1024, 0.75, 0.7},
{"weak_baseline", Geometry::WeakBaseline, 1920, 1080, 256, 0.5, 0.3},
{"wide_baseline", Geometry::WideBaseline, 4000, 3000, 256, 0.75, 0.3},
{"concentrated", Geometry::Concentrated, 1920, 1080, 256, 0.5, 0.3},
{"near_collinear", Geometry::NearCollinear, 1920, 1080, 256, 0.35, 0.3},
{"planar", Geometry::Planar, 1920, 1080, 256, 0.5, 0.3},
{"rotation_dominant", Geometry::RotationDominant, 1920, 1080, 256, 0.5,
0.3},
{"minimum_7", Geometry::Healthy, 1280, 720, 7, 0.0, 0.0},
{"minimum_8", Geometry::Healthy, 1280, 720, 8, 0.0, 0.0},
{"minimum_15", Geometry::Healthy, 1280, 720, 15, 0.5, 0.3},
{"small_16", Geometry::Healthy, 1280, 720, 16, 0.5, 0.3},
{"all_false", Geometry::Healthy, 1920, 1080, 256, 0.5, 1.0},
{"duplicates", Geometry::Duplicated, 1920, 1080, 256, 0.35, 0.3},
{"large_4096", Geometry::Healthy, 4000, 3000, 4096, 0.75, 0.5},
{"large_8192", Geometry::Healthy, 4000, 3000, 8192, 0.75, 0.7}}};
const std::array<Method, 5> methods = {
Method::Classic, Method::UsacDefault, Method::UsacMagsac,
Method::UsacAccurate, Method::SeededMagsac};
if (argc == 2 && std::strcmp(argv[1], "--params") == 0) {
const cv::UsacParams params;
std::printf("parallel=%d lo_iterations=%d lo_method=%d lo_sample=%d "
"neighbors=%d sampler=%d "
"score=%d polisher=%d polisher_iterations=%d\n",
params.isParallel ? 1 : 0, params.loIterations, params.loMethod,
params.loSampleSize, params.neighborsSearch, params.sampler,
params.score, params.final_polisher,
params.final_polisher_iterations);
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--determinism-only") == 0) {
const Scenario difficult{
"cross_process", Geometry::Healthy, 4000, 3000, 1024, 0.75, 0.7};
const Corpus corpus = generate(difficult, 0x6c6172646f6eULL);
for (Method method : methods) {
const Measurement result =
measure(method, corpus, 1.5, 0x4c334431, false);
std::printf("%s,%016llx\n", method_name(method),
static_cast<unsigned long long>(result.output_hash));
}
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--acceptance-only") == 0) {
const std::array<Scenario, 8> acceptance = {
{{"good_64", Geometry::Healthy, 1920, 1080, 64, 0.75, 0.3},
{"good_256", Geometry::Healthy, 1920, 1080, 256, 0.75, 0.5},
{"good_1024", Geometry::Healthy, 1920, 1080, 1024, 0.75, 0.7},
{"false_64", Geometry::Healthy, 1920, 1080, 64, 0.75, 1.0},
{"false_256", Geometry::Healthy, 1920, 1080, 256, 0.75, 1.0},
{"false_1024", Geometry::Healthy, 1920, 1080, 1024, 0.75, 1.0},
{"false_4096", Geometry::Healthy, 4000, 3000, 4096, 0.75, 1.0},
{"weak_256", Geometry::WeakBaseline, 1920, 1080, 256, 0.75, 0.5}}};
for (const Scenario &scenario : acceptance) {
const Corpus corpus = generate(scenario, 0x6c6172646f6eULL);
const Measurement result =
measure(Method::SeededMagsac, corpus, 1.5, 0x4c334431, false);
std::printf("%s,count=%d,ratio=%.6f,precision=%.6f,recall=%.6f\n",
scenario.name, result.inlier_count,
static_cast<double>(result.inlier_count) / scenario.count,
result.precision, result.recall);
}
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--rank-only") == 0) {
const Scenario scenario{"rank", Geometry::Healthy, 4000, 3000, 8192, 0.75,
0.7};
const Corpus corpus = generate(scenario, 0x6c6172646f6eULL);
cv::Mat mask;
cv::Mat model = estimate(Method::SeededMagsac, corpus.first, corpus.second,
1.5, 0x4c334431, mask);
cv::Mat singular_values;
cv::SVD::compute(model, singular_values);
std::printf("singular_values=%.17g,%.17g,%.17g\n",
singular_values.at<double>(0), singular_values.at<double>(1),
singular_values.at<double>(2));
return 0;
}
std::printf(
"# corpus=lardon3d-geometric-v1 compiler=%s opencv=%s threads=%d seed=%d "
"confidence=0.999 max_iterations=5000\n",
__VERSION__, CV_VERSION, cv::getNumThreads(), 0x4c334431);
std::printf("scenario,algorithm,points,count,noise,outliers,threshold,found,"
"precision,recall,"
"median_sampson,median_ms,p95_ms,worst_ms,repeatable_32x\n");
for (const Scenario &scenario : scenarios) {
const Corpus corpus = generate(scenario, 0x6c6172646f6eULL);
for (Method method : methods) {
const Measurement first = measure(method, corpus, 1.5, 0x4c334431, false);
Measurement summary = first;
bool repeatable = true;
std::vector<double> timings;
timings.reserve(32);
for (int repetition = 0; repetition < 32; ++repetition) {
const Measurement repeated =
measure(method, corpus, 1.5, 0x4c334431, false);
repeatable = repeatable && repeated.output_hash == first.output_hash;
timings.push_back(repeated.elapsed_ms);
}
std::sort(timings.begin(), timings.end());
summary.median_ms = timings[timings.size() / 2];
summary.p95_ms = timings[30];
summary.worst_ms = timings.back();
print_measurement(scenario, method, 1.5, summary, repeatable, "f64");
}
}
const Scenario tuning{
"threshold_tuning", Geometry::Healthy, 1920, 1080, 1024, 0.75, 0.5};
const Corpus tuning_corpus = generate(tuning, 0x6c6172646f6eULL);
for (double threshold : {0.5, 1.0, 1.5, 2.0, 3.0}) {
const Measurement value = measure(Method::SeededMagsac, tuning_corpus,
threshold, 0x4c334431, false);
Measurement summary = value;
summary.median_ms = value.elapsed_ms;
summary.p95_ms = value.elapsed_ms;
summary.worst_ms = value.elapsed_ms;
print_measurement(tuning, Method::SeededMagsac, threshold, summary, true,
"f64");
}
for (bool use_float : {true, false}) {
const Measurement value = measure(Method::SeededMagsac, tuning_corpus, 1.5,
0x4c334431, use_float);
Measurement summary = value;
summary.median_ms = value.elapsed_ms;
summary.p95_ms = value.elapsed_ms;
summary.worst_ms = value.elapsed_ms;
print_measurement(tuning, Method::SeededMagsac, 1.5, summary, true,
use_float ? "f32" : "f64");
}
return 0;
}

View file

@ -157,6 +157,7 @@ static bool create_v11_database(const char *path) {
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
return execute_sql(path, return execute_sql(path,
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE geometric_verifier_tasks;"
"DROP TABLE geometric_verification_results;" "DROP TABLE geometric_verification_results;"
"UPDATE metadata SET value=11 WHERE key='schema_version';" "UPDATE metadata SET value=11 WHERE key='schema_version';"
"COMMIT;PRAGMA foreign_keys=ON;"); "COMMIT;PRAGMA foreign_keys=ON;");
@ -166,7 +167,7 @@ static bool test_model_api(const char *path) {
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDb *database = NULL; Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 12); CHECK(lardon3d_project_db_schema_version(database) == 13);
Parents parents; Parents parents;
CHECK(create_parents(database, &parents)); CHECK(create_parents(database, &parents));
@ -401,7 +402,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) {
"name='geometric_verification_results'", "name='geometric_verification_results'",
0)); 0));
CHECK(lardon3d_project_db_open(v11_path, &database, error) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_open(v11_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 12); CHECK(lardon3d_project_db_schema_version(database) == 13);
Parents parents; Parents parents;
CHECK(create_parents(database, &parents)); CHECK(create_parents(database, &parents));
unsigned char fingerprint[32] = {0x91}; unsigned char fingerprint[32] = {0x91};
@ -413,7 +414,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) {
&migrated_result) == LARDON3D_PROJECT_DB_OK); &migrated_result) == LARDON3D_PROJECT_DB_OK);
uint64_t migrated_result_id = migrated_result.geometric_verification_result_id; uint64_t migrated_result_id = migrated_result.geometric_verification_result_id;
lardon3d_project_db_close(database); 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 value FROM metadata WHERE key='schema_version'", 13));
CHECK(query_integer(v11_path, CHECK(query_integer(v11_path,
"SELECT count(*) FROM sqlite_master WHERE type='index' AND " "SELECT count(*) FROM sqlite_master WHERE type='index' AND "
"name='geometric_verification_results_parent_idx'", "name='geometric_verification_results_parent_idx'",
@ -434,7 +435,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) {
"name='geometric_verification_results'", "name='geometric_verification_results'",
0)); 0));
CHECK(lardon3d_project_db_open(failed_path, &database, error) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_open(failed_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 12); CHECK(lardon3d_project_db_schema_version(database) == 13);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
return true; return true;
} }

View file

@ -0,0 +1,807 @@
#include <cerrno>
#include <climits>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <dirent.h>
#include <fcntl.h>
#include <limits>
#include <openssl/evp.h>
#include <sqlite3.h>
#include <string>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
extern "C" {
#include <lardon3d/app_state.h>
#include <lardon3d/feature_store.h>
#include <lardon3d/geometric_verifier.h>
#include <lardon3d/match_file.h>
}
static int failures;
struct Fixture {
char root[PATH_MAX];
char database_path[PATH_MAX];
Lardon3DAppState state;
Lardon3DProjectDbFeatureSet set_a;
Lardon3DProjectDbFeatureSet set_b;
Lardon3DProjectDbCandidatePair pair;
uint32_t parent_sequence;
};
static void to_hex(const unsigned char *bytes, size_t size, char *output) {
static const char digits[] = "0123456789abcdef";
for (size_t index = 0; index < size; ++index) {
output[2 * index] = digits[bytes[index] >> 4U];
output[2 * index + 1] = digits[bytes[index] & 15U];
}
output[2 * size] = '\0';
}
#define CHECK(expression) \
do { \
if (!(expression)) { \
std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, \
#expression); \
++failures; \
} \
} while (0)
static bool join_path(char output[PATH_MAX], const char *left,
const char *right) {
int length = std::snprintf(output, PATH_MAX, "%s/%s", left, right);
return length > 0 && length < PATH_MAX;
}
static bool remove_tree(const char *path) {
struct stat information;
if (lstat(path, &information) != 0)
return errno == ENOENT;
if (!S_ISDIR(information.st_mode))
return unlink(path) == 0;
DIR *directory = opendir(path);
if (!directory)
return false;
bool ok = true;
for (dirent *entry = readdir(directory); entry; entry = readdir(directory)) {
if (std::strcmp(entry->d_name, ".") == 0 ||
std::strcmp(entry->d_name, "..") == 0)
continue;
char child[PATH_MAX];
if (!join_path(child, path, entry->d_name) || !remove_tree(child))
ok = false;
}
if (closedir(directory) != 0 || rmdir(path) != 0)
ok = false;
return ok;
}
static bool execute_sql(const char *path, const char *sql) {
sqlite3 *connection = nullptr;
if (sqlite3_open(path, &connection) != SQLITE_OK)
return false;
bool ok =
sqlite3_exec(connection, sql, nullptr, nullptr, nullptr) == SQLITE_OK;
return sqlite3_close(connection) == SQLITE_OK && ok;
}
static void image_asset_path(const unsigned char hash[32], char path[4096]) {
static const char digits[] = "0123456789abcdef";
char hex[65];
for (size_t index = 0; index < 32; ++index) {
hex[2 * index] = digits[hash[index] >> 4U];
hex[2 * index + 1] = digits[hash[index] & 15U];
}
hex[64] = '\0';
(void)std::snprintf(path, 4096, "assets/images/%c%c/%s", hex[0], hex[1], hex);
}
static bool register_image(Fixture *fixture, unsigned char seed,
Lardon3DProjectDbImage *image) {
unsigned char hash[32];
std::memset(hash, seed, sizeof(hash));
char path[4096];
image_asset_path(hash, path);
Lardon3DProjectDbImageRegisterStatus status;
return lardon3d_project_db_register_image(
fixture->state.project_db, 1, hash, path, 1, "fixture.bin",
"/fixture.bin", 0, seed, &status, image) == LARDON3D_PROJECT_DB_OK;
}
static bool publish_features(Fixture *fixture,
const Lardon3DProjectDbImage *image,
unsigned char salt,
Lardon3DProjectDbFeatureSet *set) {
constexpr uint32_t feature_count = LARDON3D_MATCH_FILE_MAX_MATCHES;
std::vector<Lardon3DFeatureKeypoint> keypoints(feature_count);
std::vector<unsigned char> descriptors(
static_cast<size_t>(feature_count) * 32, salt);
for (uint32_t index = 0; index < feature_count; ++index) {
uint32_t source = salt == 4 ? (index * 3511U) % feature_count : index;
keypoints[index].x = 20.0F + static_cast<float>(source % 128U) * 7.0F;
keypoints[index].y = 20.0F + static_cast<float>(source / 128U) * 7.0F;
if (salt == 4)
keypoints[index].x -= 5.0F;
keypoints[index].size = 1.0F;
}
Lardon3DExtractedFeatures features = {};
features.image_width = 1024;
features.image_height = 768;
features.feature_count = feature_count;
features.keypoints = keypoints.data();
features.descriptors = descriptors.data();
features.descriptor_bytes = descriptors.size();
unsigned char fingerprint[32];
std::memset(fingerprint, salt, sizeof(fingerprint));
return lardon3d_feature_store_publish_v2(
&fixture->state, image->image_id, 0, "orb", 1, fingerprint,
LARDON3D_FEATURE_DESCRIPTOR_U8, 32, 0, &features,
set) == LARDON3D_FEATURE_STORE_OK;
}
static bool fixture_create(Fixture *fixture) {
std::memset(fixture, 0, sizeof(*fixture));
char root[] = "/tmp/lardon3d-geometric-core-XXXXXX";
char *created = mkdtemp(root);
if (!created ||
std::snprintf(fixture->root, sizeof(fixture->root), "%s", created) <= 0 ||
!join_path(fixture->database_path, fixture->root, "project.db"))
return false;
lardon3d_app_state_init(&fixture->state);
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
if (lardon3d_project_db_open(fixture->database_path,
&fixture->state.project_db,
error) != LARDON3D_PROJECT_DB_OK)
return false;
fixture->state.project_loaded = true;
(void)std::snprintf(fixture->state.project_path,
sizeof(fixture->state.project_path), "%s", fixture->root);
Lardon3DProjectDbScanSet scanset;
Lardon3DProjectDbImage image_a, image_b;
return lardon3d_project_db_create_scanset(fixture->state.project_db,
"geometry", &scanset) ==
LARDON3D_PROJECT_DB_OK &&
scanset.scanset_id == 1 && register_image(fixture, 1, &image_a) &&
register_image(fixture, 2, &image_b) &&
publish_features(fixture, &image_a, 3, &fixture->set_a) &&
publish_features(fixture, &image_b, 4, &fixture->set_b) &&
lardon3d_project_db_create_candidate_pair(
fixture->state.project_db, image_a.image_id, image_b.image_id, 1,
&fixture->pair) == LARDON3D_PROJECT_DB_OK;
}
static void fixture_destroy(Fixture *fixture) {
if (fixture->state.project_db)
lardon3d_project_db_close(fixture->state.project_db);
fixture->state.project_db = nullptr;
CHECK(remove_tree(fixture->root));
}
static bool sha256_file(const char *path, unsigned char output[32],
uint64_t *size) {
int descriptor = open(path, O_RDONLY | O_CLOEXEC);
if (descriptor < 0)
return false;
EVP_MD_CTX *context = EVP_MD_CTX_new();
bool ok = context && EVP_DigestInit_ex(context, EVP_sha256(), nullptr) == 1;
unsigned char bytes[4096];
uint64_t total = 0;
for (;;) {
ssize_t count = read(descriptor, bytes, sizeof(bytes));
if (count < 0) {
ok = false;
break;
}
if (count == 0)
break;
total += static_cast<uint64_t>(count);
if (EVP_DigestUpdate(context, bytes, static_cast<size_t>(count)) != 1)
ok = false;
}
unsigned int digest_size = 0;
ok = ok && EVP_DigestFinal_ex(context, output, &digest_size) == 1 &&
digest_size == 32;
EVP_MD_CTX_free(context);
if (close(descriptor) != 0)
ok = false;
*size = total;
return ok;
}
static void put_u32(unsigned char bytes[4], uint32_t value) {
for (unsigned int index = 0; index < 4; ++index)
bytes[index] = static_cast<unsigned char>(value >> (8U * index));
}
static bool create_parent(Fixture *fixture, uint32_t count,
uint64_t *parent_id) {
++fixture->parent_sequence;
char relative[64];
char full[PATH_MAX];
int length = std::snprintf(relative, sizeof(relative), "matches-%u.bin",
fixture->parent_sequence);
if (length <= 0 || length >= static_cast<int>(sizeof(relative)) ||
!join_path(full, fixture->root, relative))
return false;
std::vector<Lardon3DMatchFileEntry> entries(count);
for (uint32_t index = 0; index < count; ++index) {
entries[index].feature_index_a = index;
entries[index].feature_index_b = count == 0 ? 0 : (index * 7U) % count;
entries[index].distance = static_cast<float>(index + 1U);
}
int descriptor = open(full, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
if (descriptor < 0)
return false;
bool ok = lardon3d_match_file_write(
descriptor, LARDON3D_FEATURE_DESCRIPTOR_U8, 32,
fixture->set_a.feature_set_id, fixture->set_b.feature_set_id,
entries.data(), count) == LARDON3D_MATCH_FILE_OK;
if (close(descriptor) != 0)
ok = false;
unsigned char sha256[32];
uint64_t size = 0;
if (!ok || !sha256_file(full, sha256, &size))
return false;
unsigned char fingerprint[32];
std::memset(fingerprint, static_cast<int>(fixture->parent_sequence),
sizeof(fingerprint));
Lardon3DProjectDbMatchResult parent;
if (lardon3d_project_db_create_match_result(
fixture->state.project_db, fixture->pair.candidate_pair_id,
fixture->set_a.feature_set_id, fixture->set_b.feature_set_id,
"fixture", 1, fingerprint, LARDON3D_MATCH_RESULT_STATUS_MATCHED,
count, sha256, relative, size, fixture->parent_sequence,
&parent) != LARDON3D_PROJECT_DB_OK)
return false;
*parent_id = parent.match_result_id;
return true;
}
static std::string mask_bits(uint32_t count, uint32_t inliers,
uint32_t stride = 1) {
std::string bits(count, '0');
uint32_t position = 0;
for (uint32_t selected = 0; selected < inliers; ++selected) {
while (bits[position] == '1')
position = (position + 1U) % count;
bits[position] = '1';
position = (position + stride) % count;
}
return bits;
}
static void test_parameters_and_fingerprint() {
Lardon3DGeometricVerifierParameters parameters =
lardon3d_geometric_verifier_default_parameters();
CHECK(lardon3d_geometric_verifier_parameters_valid(&parameters));
unsigned char first[32], second[32], changed[32];
lardon3d_geometric_verifier_fingerprint(&parameters, first);
lardon3d_geometric_verifier_fingerprint(&parameters, second);
CHECK(std::memcmp(first, second, sizeof(first)) == 0);
auto differs = [&](Lardon3DGeometricVerifierParameters changed_parameters) {
lardon3d_geometric_verifier_fingerprint(&changed_parameters, changed);
return std::memcmp(first, changed, sizeof(first)) != 0;
};
parameters.threshold_pixels = 2.0;
CHECK(differs(parameters));
parameters = lardon3d_geometric_verifier_default_parameters();
parameters.confidence = 0.99;
CHECK(differs(parameters));
parameters = lardon3d_geometric_verifier_default_parameters();
parameters.max_iterations = 4000;
CHECK(differs(parameters));
parameters = lardon3d_geometric_verifier_default_parameters();
parameters.min_inlier_count = 17;
CHECK(differs(parameters));
parameters = lardon3d_geometric_verifier_default_parameters();
parameters.min_inlier_ratio = 0.21;
CHECK(differs(parameters));
parameters = lardon3d_geometric_verifier_default_parameters();
parameters.seed_policy_version = 2;
CHECK(differs(parameters));
parameters = lardon3d_geometric_verifier_default_parameters();
parameters.canonicalization_version = 2;
CHECK(differs(parameters));
parameters = lardon3d_geometric_verifier_default_parameters();
unsigned char default_bytes[LARDON3D_GEOMETRIC_VERIFIER_FINGERPRINT_SIZE];
unsigned char changed_bytes[LARDON3D_GEOMETRIC_VERIFIER_FINGERPRINT_SIZE];
CHECK(lardon3d_geometric_verifier_fingerprint_bytes(
&parameters, LARDON3D_GEOMETRIC_ALGORITHM_USAC_MAGSAC, 1, default_bytes));
CHECK(lardon3d_geometric_verifier_fingerprint_bytes(
&parameters, LARDON3D_GEOMETRIC_ALGORITHM_USAC_DEFAULT, 1,
changed_bytes));
CHECK(std::memcmp(default_bytes, changed_bytes, sizeof(default_bytes)) != 0);
CHECK(lardon3d_geometric_verifier_fingerprint_bytes(
&parameters, LARDON3D_GEOMETRIC_ALGORITHM_USAC_MAGSAC, 2, changed_bytes));
CHECK(std::memcmp(default_bytes, changed_bytes, sizeof(default_bytes)) != 0);
CHECK(default_bytes[83] == 0);
parameters.min_inlier_ratio = 0.0;
CHECK(lardon3d_geometric_verifier_parameters_valid(&parameters));
CHECK(lardon3d_geometric_verifier_fingerprint_bytes(
&parameters, LARDON3D_GEOMETRIC_ALGORITHM_USAC_MAGSAC, 1, default_bytes));
parameters.min_inlier_ratio = -0.0;
CHECK(lardon3d_geometric_verifier_fingerprint_bytes(
&parameters, LARDON3D_GEOMETRIC_ALGORITHM_USAC_MAGSAC, 1, changed_bytes));
CHECK(std::memcmp(default_bytes, changed_bytes, sizeof(default_bytes)) == 0);
parameters = lardon3d_geometric_verifier_default_parameters();
parameters.confidence = 1.0;
CHECK(!lardon3d_geometric_verifier_parameters_valid(&parameters));
parameters = lardon3d_geometric_verifier_default_parameters();
parameters.threshold_pixels = std::numeric_limits<double>::quiet_NaN();
CHECK(!lardon3d_geometric_verifier_parameters_valid(&parameters));
parameters = lardon3d_geometric_verifier_default_parameters();
parameters.max_iterations = std::numeric_limits<uint32_t>::max();
CHECK(!lardon3d_geometric_verifier_parameters_valid(&parameters));
parameters = lardon3d_geometric_verifier_default_parameters();
parameters.min_inlier_count = 8193;
CHECK(!lardon3d_geometric_verifier_parameters_valid(&parameters));
parameters = lardon3d_geometric_verifier_default_parameters();
CHECK(lardon3d_geometric_verifier_fingerprint_bytes(
&parameters, LARDON3D_GEOMETRIC_ALGORITHM_USAC_MAGSAC, 1, default_bytes));
char encoded_hex[2 * LARDON3D_GEOMETRIC_VERIFIER_FINGERPRINT_SIZE + 1];
char fingerprint_hex[65];
to_hex(default_bytes, sizeof(default_bytes), encoded_hex);
to_hex(first, sizeof(first), fingerprint_hex);
CHECK(std::strcmp(
encoded_hex,
"4c3344475646503101000000010000000100000001000000000000000000f83f"
"2b8716d9cef7ef3f88130000100000009a9999999999c93f0100000001000000"
"0200020001050000000e00000001030300000000") == 0);
CHECK(
std::strcmp(
fingerprint_hex,
"ddb44bb070c62be66c405946e89cbb49c084f8f30a21d6f408dc239225b7bbd0") ==
0);
}
static void test_seed() {
unsigned char match[32] = {};
unsigned char fingerprint[32] = {};
uint32_t first = lardon3d_geometric_verifier_seed(match, fingerprint);
CHECK(first == lardon3d_geometric_verifier_seed(match, fingerprint));
match[31] = 1;
CHECK(first != lardon3d_geometric_verifier_seed(match, fingerprint));
CHECK(lardon3d_geometric_verifier_seed(nullptr, fingerprint) == 0);
CHECK(first == 1803944746U);
Lardon3DGeometricVerifierParameters parameters =
lardon3d_geometric_verifier_default_parameters();
unsigned char production_fingerprint[32];
lardon3d_geometric_verifier_fingerprint(&parameters, production_fingerprint);
CHECK(lardon3d_geometric_verifier_seed(match, production_fingerprint) ==
1910542150U);
}
static void test_canonicalization() {
const double source[9] = {-2.0, 1.0, 0.0, 0.5, -0.25, 0.0, 0.0, 0.0, 0.0};
double positive[9], negative[9], scaled[9], half[9];
for (size_t index = 0; index < 9; ++index) {
positive[index] = source[index];
negative[index] = -source[index];
scaled[index] = 2.0 * source[index];
half[index] = 0.5 * source[index];
}
CHECK(lardon3d_geometric_verifier_canonicalize(positive));
CHECK(lardon3d_geometric_verifier_canonicalize(negative));
CHECK(lardon3d_geometric_verifier_canonicalize(scaled));
CHECK(lardon3d_geometric_verifier_canonicalize(half));
CHECK(std::memcmp(positive, negative, sizeof(positive)) == 0);
for (size_t index = 0; index < 9; ++index)
CHECK(positive[index] == scaled[index]);
for (size_t index = 0; index < 9; ++index)
CHECK(positive[index] == half[index]);
CHECK(positive[0] > 0.0);
double zero[9] = {};
CHECK(!lardon3d_geometric_verifier_canonicalize(zero));
double invalid[9] = {std::numeric_limits<double>::infinity()};
CHECK(!lardon3d_geometric_verifier_canonicalize(invalid));
invalid[0] = -std::numeric_limits<double>::infinity();
CHECK(!lardon3d_geometric_verifier_canonicalize(invalid));
invalid[0] = std::numeric_limits<double>::quiet_NaN();
CHECK(!lardon3d_geometric_verifier_canonicalize(invalid));
double tie[9] = {-1.0, 1.0};
CHECK(lardon3d_geometric_verifier_canonicalize(tie));
CHECK(tie[0] > 0.0 && tie[1] < 0.0);
double tiny[9] = {std::numeric_limits<double>::denorm_min()};
CHECK(lardon3d_geometric_verifier_canonicalize(tiny));
CHECK(tiny[0] == 1.0);
}
static bool
mask_matches(const Lardon3DProjectDbGeometricVerificationResult &result,
const std::string &bits) {
if (result.inlier_mask_size != (bits.size() + 7U) / 8U)
return false;
for (size_t index = 0; index < bits.size(); ++index) {
bool stored = (result.inlier_mask[index / 8U] & (1U << (index % 8U))) != 0;
if (stored != (bits[index] == '1'))
return false;
}
return true;
}
static bool
run_controlled(Fixture *fixture, uint64_t parent_id, const std::string &bits,
const Lardon3DGeometricVerifierParameters *parameters,
Lardon3DProjectDbGeometricVerificationResult *result,
bool *reused) {
if (setenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR", "controlled", 1) != 0 ||
setenv("LARDON3D_TEST_GEOMETRIC_MASK", bits.c_str(), 1) != 0)
return false;
Lardon3DGeometricVerifierResult status =
lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, parent_id, parameters,
result, reused);
(void)unsetenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR");
(void)unsetenv("LARDON3D_TEST_GEOMETRIC_MASK");
return status == LARDON3D_GEOMETRIC_VERIFIER_OK;
}
static void test_e2e_and_reuse(Fixture *fixture) {
Lardon3DGeometricVerifierParameters parameters =
lardon3d_geometric_verifier_default_parameters();
uint64_t verified_parent = 0;
CHECK(create_parent(fixture, 32, &verified_parent));
std::string verified_bits = mask_bits(32, 20, 7);
Lardon3DProjectDbGeometricVerificationResult verified;
bool reused = false;
lardon3d_geometric_verifier_test_reset_estimator_calls();
CHECK(run_controlled(fixture, verified_parent, verified_bits, &parameters,
&verified, &reused));
CHECK(!reused && lardon3d_geometric_verifier_test_estimator_calls() == 1);
CHECK(verified.status == LARDON3D_GEOMETRIC_VERIFIED && verified.has_model &&
verified.inlier_count == 20 && mask_matches(verified, verified_bits));
double norm = 0.0;
for (double value : verified.model) {
CHECK(std::isfinite(value));
norm = std::hypot(norm, value);
}
CHECK(std::fabs(norm - 1.0) < 1e-15);
Lardon3DProjectDbGeometricVerificationResult reused_verified;
CHECK(run_controlled(fixture, verified_parent, verified_bits, &parameters,
&reused_verified, &reused));
CHECK(reused && lardon3d_geometric_verifier_test_estimator_calls() == 1 &&
reused_verified.geometric_verification_result_id ==
verified.geometric_verification_result_id);
uint64_t rejected_parent = 0;
CHECK(create_parent(fixture, 32, &rejected_parent));
std::string rejected_bits = mask_bits(32, 15, 5);
Lardon3DProjectDbGeometricVerificationResult rejected;
CHECK(run_controlled(fixture, rejected_parent, rejected_bits, &parameters,
&rejected, &reused));
CHECK(!reused && rejected.status == LARDON3D_GEOMETRIC_REJECTED &&
!rejected.has_model && rejected.inlier_count == 15 &&
mask_matches(rejected, rejected_bits));
uint32_t calls_after_rejected =
lardon3d_geometric_verifier_test_estimator_calls();
CHECK(run_controlled(fixture, rejected_parent, rejected_bits, &parameters,
&rejected, &reused));
CHECK(reused && lardon3d_geometric_verifier_test_estimator_calls() ==
calls_after_rejected);
parameters.threshold_pixels = 2.0;
Lardon3DProjectDbGeometricVerificationResult different;
CHECK(run_controlled(fixture, verified_parent, verified_bits, &parameters,
&different, &reused));
CHECK(!reused && different.geometric_verification_result_id !=
verified.geometric_verification_result_id);
}
static void test_scientific_boundaries(Fixture *fixture) {
Lardon3DGeometricVerifierParameters parameters =
lardon3d_geometric_verifier_default_parameters();
uint64_t insufficient_parent = 0;
CHECK(create_parent(fixture, 6, &insufficient_parent));
Lardon3DProjectDbGeometricVerificationResult insufficient;
bool reused = false;
lardon3d_geometric_verifier_test_reset_estimator_calls();
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, insufficient_parent,
&parameters, &insufficient,
&reused) == LARDON3D_GEOMETRIC_VERIFIER_OK);
CHECK(!reused && insufficient.status == LARDON3D_GEOMETRIC_REJECTED &&
insufficient.inlier_count == 0 && !insufficient.has_model &&
insufficient.inlier_mask_size == 1 &&
insufficient.inlier_mask[0] == 0 &&
lardon3d_geometric_verifier_test_estimator_calls() == 0);
uint64_t empty_parent = 0;
CHECK(create_parent(fixture, 16, &empty_parent));
CHECK(setenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR", "empty", 1) == 0);
Lardon3DProjectDbGeometricVerificationResult empty;
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, empty_parent, &parameters,
&empty, &reused) == LARDON3D_GEOMETRIC_VERIFIER_OK);
CHECK(unsetenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR") == 0);
CHECK(empty.status == LARDON3D_GEOMETRIC_REJECTED &&
empty.inlier_count == 0 && !empty.has_model);
for (uint32_t inliers : {15U, 16U, 17U}) {
uint64_t parent = 0;
CHECK(create_parent(fixture, 80, &parent));
std::string bits = mask_bits(80, inliers, 9);
Lardon3DProjectDbGeometricVerificationResult result;
CHECK(run_controlled(fixture, parent, bits, &parameters, &result, &reused));
CHECK(result.inlier_count == inliers && mask_matches(result, bits));
CHECK(result.status == (inliers >= 16 ? LARDON3D_GEOMETRIC_VERIFIED
: LARDON3D_GEOMETRIC_REJECTED));
}
}
static void test_mask_boundaries(Fixture *fixture) {
Lardon3DGeometricVerifierParameters parameters =
lardon3d_geometric_verifier_default_parameters();
parameters.min_inlier_count = 1;
parameters.min_inlier_ratio = 0.0;
for (uint32_t count : {1U, 2U}) {
uint64_t parent = 0;
CHECK(create_parent(fixture, count, &parent));
Lardon3DProjectDbGeometricVerificationResult result;
bool reused = false;
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, parent, &parameters,
&result, &reused) == LARDON3D_GEOMETRIC_VERIFIER_OK);
CHECK(result.status == LARDON3D_GEOMETRIC_REJECTED &&
result.inlier_count == 0 && result.inlier_mask_size == 1 &&
result.inlier_mask[0] == 0);
}
for (uint32_t count : {7U, 8U, 9U, 63U, 64U, 65U, 8191U, 8192U}) {
uint64_t parent = 0;
CHECK(create_parent(fixture, count, &parent));
std::string bits = mask_bits(count, (count + 1U) / 2U, 11);
Lardon3DProjectDbGeometricVerificationResult result;
bool reused = false;
CHECK(run_controlled(fixture, parent, bits, &parameters, &result, &reused));
CHECK(!reused && result.status == LARDON3D_GEOMETRIC_VERIFIED &&
result.inlier_count == (count + 1U) / 2U &&
mask_matches(result, bits));
unsigned int remainder = count % 8U;
if (remainder != 0)
CHECK((result.inlier_mask[result.inlier_mask_size - 1] &
static_cast<unsigned char>(0xffU << remainder)) == 0);
}
}
static void test_failures(Fixture *fixture) {
Lardon3DGeometricVerifierParameters parameters =
lardon3d_geometric_verifier_default_parameters();
for (const char *behavior :
{"error", "malformed-mask", "malformed-model", "nan-model"}) {
uint64_t parent = 0;
CHECK(create_parent(fixture, 32, &parent));
CHECK(setenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR", behavior, 1) == 0);
Lardon3DProjectDbGeometricVerificationResult result;
bool reused = false;
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, parent, &parameters,
&result, &reused) == LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR);
CHECK(unsetenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR") == 0);
unsigned char fingerprint[32];
lardon3d_geometric_verifier_fingerprint(&parameters, fingerprint);
CHECK(lardon3d_project_db_find_geometric_verification_result(
fixture->state.project_db, parent,
LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, fingerprint,
&result) == LARDON3D_PROJECT_DB_NOT_FOUND);
}
uint64_t publication_parent = 0;
CHECK(create_parent(fixture, 32, &publication_parent));
std::string bits = mask_bits(32, 20, 3);
CHECK(setenv("LARDON3D_TEST_GEOMETRIC_PUBLICATION_FAILURE", "1", 1) == 0);
CHECK(setenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR", "controlled", 1) == 0);
CHECK(setenv("LARDON3D_TEST_GEOMETRIC_MASK", bits.c_str(), 1) == 0);
Lardon3DProjectDbGeometricVerificationResult result;
bool reused = false;
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, publication_parent,
&parameters, &result,
&reused) == LARDON3D_GEOMETRIC_VERIFIER_DATABASE_ERROR);
CHECK(unsetenv("LARDON3D_TEST_GEOMETRIC_PUBLICATION_FAILURE") == 0);
CHECK(unsetenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR") == 0);
CHECK(unsetenv("LARDON3D_TEST_GEOMETRIC_MASK") == 0);
unsigned char fingerprint[32];
lardon3d_geometric_verifier_fingerprint(&parameters, fingerprint);
CHECK(lardon3d_project_db_find_geometric_verification_result(
fixture->state.project_db, publication_parent,
LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, fingerprint,
&result) == LARDON3D_PROJECT_DB_NOT_FOUND);
CHECK(run_controlled(fixture, publication_parent, bits, &parameters, &result,
&reused));
CHECK(!reused && result.status == LARDON3D_GEOMETRIC_VERIFIED);
uint64_t allocation_parent = 0;
CHECK(create_parent(fixture, 32, &allocation_parent));
CHECK(setenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR", "bad-alloc", 1) == 0);
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, allocation_parent,
&parameters, &result,
&reused) == LARDON3D_GEOMETRIC_VERIFIER_OUT_OF_MEMORY);
CHECK(unsetenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR") == 0);
}
static void test_parent_and_asset_failures(Fixture *fixture) {
Lardon3DGeometricVerifierParameters parameters =
lardon3d_geometric_verifier_default_parameters();
Lardon3DProjectDbGeometricVerificationResult result;
bool reused = false;
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, UINT64_C(999999),
&parameters, &result,
&reused) == LARDON3D_GEOMETRIC_VERIFIER_NOT_FOUND);
unsigned char fingerprint[32] = {};
Lardon3DProjectDbMatchResult no_match;
CHECK(lardon3d_project_db_create_match_result(
fixture->state.project_db, fixture->pair.candidate_pair_id,
fixture->set_a.feature_set_id, fixture->set_b.feature_set_id,
"no-match", 1, fingerprint, LARDON3D_MATCH_RESULT_STATUS_NO_MATCH,
0, nullptr, nullptr, 0, 1, &no_match) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, no_match.match_result_id,
&parameters, &result,
&reused) == LARDON3D_GEOMETRIC_VERIFIER_NOT_FOUND);
uint64_t missing_asset_parent = 0;
CHECK(create_parent(fixture, 32, &missing_asset_parent));
Lardon3DProjectDbMatchResult parent;
CHECK(lardon3d_project_db_load_match_result(fixture->state.project_db,
missing_asset_parent, &parent) ==
LARDON3D_PROJECT_DB_OK);
char full[PATH_MAX];
CHECK(join_path(full, fixture->root, parent.match_asset_path));
CHECK(unlink(full) == 0);
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, missing_asset_parent,
&parameters, &result,
&reused) == LARDON3D_GEOMETRIC_VERIFIER_CORRUPT);
uint64_t truncated_parent = 0;
CHECK(create_parent(fixture, 32, &truncated_parent));
CHECK(lardon3d_project_db_load_match_result(fixture->state.project_db,
truncated_parent, &parent) ==
LARDON3D_PROJECT_DB_OK);
CHECK(join_path(full, fixture->root, parent.match_asset_path));
CHECK(truncate(full, LARDON3D_MATCH_FILE_HEADER_SIZE) == 0);
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, truncated_parent,
&parameters, &result,
&reused) == LARDON3D_GEOMETRIC_VERIFIER_CORRUPT);
uint64_t index_parent = 0;
CHECK(create_parent(fixture, 32, &index_parent));
CHECK(lardon3d_project_db_load_match_result(fixture->state.project_db,
index_parent, &parent) ==
LARDON3D_PROJECT_DB_OK);
CHECK(join_path(full, fixture->root, parent.match_asset_path));
int descriptor = open(full, O_WRONLY | O_CLOEXEC);
CHECK(descriptor >= 0);
unsigned char invalid_index[4];
put_u32(invalid_index, LARDON3D_MATCH_FILE_MAX_MATCHES);
CHECK(pwrite(descriptor, invalid_index, sizeof(invalid_index),
LARDON3D_MATCH_FILE_HEADER_SIZE) ==
static_cast<ssize_t>(sizeof(invalid_index)));
CHECK(close(descriptor) == 0);
unsigned char corrupt_sha[32];
uint64_t corrupt_size = 0;
CHECK(sha256_file(full, corrupt_sha, &corrupt_size));
char corrupt_relative[LARDON3D_PROJECT_DB_PATH_CAPACITY];
(void)std::snprintf(corrupt_relative, sizeof(corrupt_relative), "%s",
parent.match_asset_path);
uint32_t corrupt_count = parent.match_count;
Lardon3DProjectDbMatchResult repaired;
CHECK(lardon3d_project_db_repair_match_result(
fixture->state.project_db, index_parent,
LARDON3D_MATCH_RESULT_STATUS_MATCHED, corrupt_count, corrupt_sha,
corrupt_relative, corrupt_size,
&repaired) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, index_parent, &parameters,
&result, &reused) == LARDON3D_GEOMETRIC_VERIFIER_CORRUPT);
}
static void test_real_estimator_and_reopen(Fixture *fixture) {
uint64_t parent = 0;
CHECK(create_parent(fixture, 8192, &parent));
Lardon3DGeometricVerifierParameters parameters =
lardon3d_geometric_verifier_default_parameters();
Lardon3DProjectDbGeometricVerificationResult result;
bool reused = false;
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, parent, &parameters,
&result, &reused) == LARDON3D_GEOMETRIC_VERIFIER_OK);
CHECK(!reused && result.status == LARDON3D_GEOMETRIC_VERIFIED &&
result.has_model &&
result.inlier_count >= parameters.min_inlier_count &&
result.inlier_mask_size == 1024);
Lardon3DProjectDbGeometricVerificationResult before_reopen = result;
uint64_t result_id = result.geometric_verification_result_id;
lardon3d_project_db_close(fixture->state.project_db);
fixture->state.project_db = nullptr;
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
CHECK(lardon3d_project_db_open(fixture->database_path,
&fixture->state.project_db,
error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, parent, &parameters,
&result, &reused) == LARDON3D_GEOMETRIC_VERIFIER_OK);
CHECK(reused && result.geometric_verification_result_id == result_id &&
result.status == before_reopen.status &&
result.inlier_count == before_reopen.inlier_count &&
result.inlier_mask_size == before_reopen.inlier_mask_size &&
std::memcmp(result.inlier_mask, before_reopen.inlier_mask,
before_reopen.inlier_mask_size) == 0 &&
std::memcmp(result.model, before_reopen.model, sizeof(result.model)) ==
0);
}
static void test_feature_failures(Fixture *fixture) {
Lardon3DGeometricVerifierParameters parameters =
lardon3d_geometric_verifier_default_parameters();
Lardon3DProjectDbGeometricVerificationResult result;
bool reused = false;
uint64_t ownership_parent = 0;
CHECK(create_parent(fixture, 32, &ownership_parent));
char sql[256];
int length = std::snprintf(
sql, sizeof(sql),
"UPDATE match_results SET feature_set_id_a=%llu WHERE "
"match_result_id=%llu",
static_cast<unsigned long long>(fixture->set_b.feature_set_id),
static_cast<unsigned long long>(ownership_parent));
CHECK(length > 0 && length < static_cast<int>(sizeof(sql)) &&
execute_sql(fixture->database_path, sql));
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, ownership_parent,
&parameters, &result,
&reused) == LARDON3D_GEOMETRIC_VERIFIER_CORRUPT);
uint64_t missing_asset_parent = 0;
CHECK(create_parent(fixture, 32, &missing_asset_parent));
char feature_path[PATH_MAX];
CHECK(join_path(feature_path, fixture->root, fixture->set_a.asset.path));
CHECK(unlink(feature_path) == 0);
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, missing_asset_parent,
&parameters, &result,
&reused) == LARDON3D_GEOMETRIC_VERIFIER_CORRUPT);
uint64_t missing_set_parent = 0;
CHECK(create_parent(fixture, 32, &missing_set_parent));
length = std::snprintf(
sql, sizeof(sql),
"PRAGMA foreign_keys=OFF;DELETE FROM feature_sets "
"WHERE feature_set_id=%llu;PRAGMA foreign_keys=ON",
static_cast<unsigned long long>(fixture->set_a.feature_set_id));
CHECK(length > 0 && length < static_cast<int>(sizeof(sql)) &&
execute_sql(fixture->database_path, sql));
CHECK(lardon3d_geometric_verifier_verify_and_publish(
fixture->root, fixture->state.project_db, missing_set_parent,
&parameters, &result,
&reused) == LARDON3D_GEOMETRIC_VERIFIER_NOT_FOUND);
}
int main() {
test_parameters_and_fingerprint();
test_seed();
test_canonicalization();
Fixture fixture;
CHECK(fixture_create(&fixture));
if (fixture.state.project_db) {
test_e2e_and_reuse(&fixture);
test_scientific_boundaries(&fixture);
test_mask_boundaries(&fixture);
test_failures(&fixture);
test_parent_and_asset_failures(&fixture);
test_real_estimator_and_reopen(&fixture);
test_feature_failures(&fixture);
}
fixture_destroy(&fixture);
if (failures == 0)
std::puts("geometric verifier core unit tests: PASS");
return failures == 0 ? 0 : 1;
}

View file

@ -0,0 +1,358 @@
#include <dirent.h>
#include <errno.h>
#include <sched.h>
#include <sqlite3.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <lardon3d/geometric_verifier_task.h>
#include <lardon3d/project.h>
#include <lardon3d/task_queue.h>
#define CHECK(condition) \
do { \
if (!(condition)) { \
(void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \
return false; \
} \
} while (0)
static int fixture_parent_count(void) {
const char *value = getenv("LARDON3D_TEST_GEOMETRIC_PARENT_COUNT");
if (!value || !value[0]) {
return 12;
}
char *end = NULL;
long parsed = strtol(value, &end, 10);
return end && *end == '\0' && parsed >= 12 && parsed <= 10000 ? (int)parsed
: 12;
}
static bool remove_tree(const char *path) {
struct stat information;
if (lstat(path, &information) != 0) {
return errno == ENOENT;
}
if (!S_ISDIR(information.st_mode)) {
return unlink(path) == 0;
}
DIR *directory = opendir(path);
if (!directory) {
return false;
}
bool ok = true;
for (struct dirent *entry = readdir(directory); entry;
entry = readdir(directory)) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
char child[4096];
int written = snprintf(child, sizeof(child), "%s/%s", path, entry->d_name);
if (written <= 0 || (size_t)written >= sizeof(child) ||
!remove_tree(child)) {
ok = false;
}
}
return closedir(directory) == 0 && rmdir(path) == 0 && ok;
}
static Lardon3DResourcePolicy policy(void) {
return (Lardon3DResourcePolicy){
.system_memory_reserve_bytes = 4ULL * 1024 * 1024 * 1024,
.emergency_memory_floor_bytes = 2ULL * 1024 * 1024 * 1024,
.system_cpu_reserve = 4,
.maximum_cpu_load_ratio = 1.0,
.maximum_cpu_pressure_avg10 = 100.0,
.maximum_memory_pressure_avg10 = 100.0,
.maximum_io_pressure_avg10 = 100.0,
.io_slot_capacity = 1,
.gpu_slot_capacity = 1,
};
}
static bool wait_state(Lardon3DTaskQueue *queue, uint64_t id,
Lardon3DTaskState wanted,
Lardon3DTaskSnapshot *snapshot) {
for (size_t attempt = 0; attempt < 2000000; ++attempt) {
if (lardon3d_task_queue_get(queue, id, snapshot) &&
snapshot->state == wanted) {
return true;
}
sched_yield();
}
return false;
}
static bool wait_durable(Lardon3DProjectDb *database, uint64_t id,
Lardon3DTaskState wanted) {
for (size_t attempt = 0; attempt < 2000000; ++attempt) {
Lardon3DProjectDbTask task;
if (lardon3d_project_db_load_task(database, id, &task) ==
LARDON3D_PROJECT_DB_OK &&
task.saved_state == wanted) {
return true;
}
sched_yield();
}
return false;
}
static bool exec_sql(const char *path, const char *sql) {
sqlite3 *db = NULL;
bool ok =
sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE, NULL) == SQLITE_OK &&
sqlite3_exec(db, sql, NULL, NULL, NULL) == SQLITE_OK;
return sqlite3_close(db) == SQLITE_OK && ok;
}
static bool query_integer(const char *path, const char *sql,
sqlite3_int64 expected) {
sqlite3 *db = NULL;
sqlite3_stmt *statement = NULL;
bool ok =
sqlite3_open_v2(path, &db, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK &&
sqlite3_prepare_v2(db, sql, -1, &statement, NULL) == SQLITE_OK &&
sqlite3_step(statement) == SQLITE_ROW &&
sqlite3_column_int64(statement, 0) == expected;
if (statement) {
(void)sqlite3_finalize(statement);
}
return sqlite3_close(db) == SQLITE_OK && ok;
}
static bool seed_reusable_parents(const char *path,
const unsigned char fingerprint[32]) {
sqlite3 *db = NULL;
CHECK(sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE, NULL) == SQLITE_OK);
static const char setup[] =
"PRAGMA foreign_keys=ON;BEGIN IMMEDIATE;"
"INSERT INTO scansets(name,created_at,updated_at) VALUES('task',1,1);"
"INSERT INTO image_assets(sha256,path,size_bytes,state,created_at) "
"VALUES(zeroblob(32),'assets/images/00/a',1,1,1),"
"(randomblob(32),'assets/images/00/b',1,1,1);"
"INSERT INTO "
"images(scanset_id,asset_id,original_name,source_path,imported_at) "
"VALUES(1,1,'a','a',1),(1,2,'b','b',1);"
"INSERT INTO "
"feature_assets(sha256,path,size_bytes,durability,created_at) "
"VALUES(randomblob(32),'assets/features/a',1,0,1),"
"(randomblob(32),'assets/features/b',1,0,1);"
"INSERT INTO "
"feature_sets(image_id,feature_asset_id,extractor_kind,extractor_version,"
"parameter_fingerprint,source_image_sha256,feature_count,descriptor_type,"
"descriptor_dimension,created_at) VALUES"
"(1,1,'orb',1,zeroblob(32),zeroblob(32),16,1,32,1),"
"(2,2,'orb',1,zeroblob(32),zeroblob(32),16,1,32,1);"
"INSERT INTO candidate_pairs(image_id_a,image_id_b,created_at) "
"VALUES(1,2,1);"
"COMMIT;";
CHECK(sqlite3_exec(db, setup, NULL, NULL, NULL) == SQLITE_OK);
sqlite3_stmt *parent = NULL;
sqlite3_stmt *result = NULL;
CHECK(sqlite3_prepare_v2(
db,
"INSERT INTO "
"match_results(candidate_pair_id,feature_set_id_a,feature_set_id_b,"
"matcher_kind,matcher_version,parameter_fingerprint,result_status,"
"match_count,"
"match_asset_sha256,match_asset_path,match_asset_size_bytes,"
"created_at) "
"VALUES(1,1,2,?1,1,?2,1,16,?3,?4,128,1)",
-1, &parent, NULL) == SQLITE_OK);
CHECK(sqlite3_prepare_v2(
db,
"INSERT INTO "
"geometric_verification_results(match_result_id,verifier_kind,"
"verifier_version,parameter_fingerprint,status,inlier_count,inlier_"
"mask,created_at) "
"VALUES(?1,1,1,?2,1,0,zeroblob(2),1)",
-1, &result, NULL) == SQLITE_OK);
for (int index = 0; index < fixture_parent_count(); ++index) {
char kind[32];
char asset[64];
unsigned char identity[32];
unsigned char match_hash[32];
memset(identity, index + 1, sizeof(identity));
memset(match_hash, index + 33, sizeof(match_hash));
(void)snprintf(kind, sizeof(kind), "fixture-%d", index);
(void)snprintf(asset, sizeof(asset), "assets/matches/%d", index);
sqlite3_bind_text(parent, 1, kind, -1, SQLITE_TRANSIENT);
sqlite3_bind_blob(parent, 2, identity, 32, SQLITE_TRANSIENT);
sqlite3_bind_blob(parent, 3, match_hash, 32, SQLITE_TRANSIENT);
sqlite3_bind_text(parent, 4, asset, -1, SQLITE_TRANSIENT);
CHECK(sqlite3_step(parent) == SQLITE_DONE);
CHECK(sqlite3_reset(parent) == SQLITE_OK);
sqlite3_bind_int64(result, 1, sqlite3_last_insert_rowid(db));
sqlite3_bind_blob(result, 2, fingerprint, 32, SQLITE_TRANSIENT);
CHECK(sqlite3_step(result) == SQLITE_DONE);
CHECK(sqlite3_reset(result) == SQLITE_OK);
}
CHECK(sqlite3_finalize(parent) == SQLITE_OK);
CHECK(sqlite3_finalize(result) == SQLITE_OK);
return sqlite3_close(db) == SQLITE_OK;
}
static bool add_reusable_results(const char *path,
const unsigned char fingerprint[32]) {
sqlite3 *db = NULL;
sqlite3_stmt *statement = NULL;
CHECK(sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE, NULL) == SQLITE_OK);
CHECK(
sqlite3_prepare_v2(
db,
"INSERT INTO "
"geometric_verification_results(match_result_id,verifier_kind,"
"verifier_version,parameter_fingerprint,status,inlier_count,inlier_"
"mask,created_at) "
"SELECT match_result_id,1,1,?1,1,0,zeroblob(2),2 FROM match_results",
-1, &statement, NULL) == SQLITE_OK);
sqlite3_bind_blob(statement, 1, fingerprint, 32, SQLITE_TRANSIENT);
int code = sqlite3_step(statement);
if (code != SQLITE_DONE) {
(void)fprintf(stderr, "Insertion reuse: %s\n", sqlite3_errmsg(db));
}
CHECK(code == SQLITE_DONE);
CHECK(sqlite3_finalize(statement) == SQLITE_OK);
return sqlite3_close(db) == SQLITE_OK;
}
static bool run_task_test(void) {
char root[] = "/tmp/lardon3d-geometric-task-XXXXXX";
CHECK(mkdtemp(root) != NULL);
char internal[4096];
char checkpoints[4096];
char database_path[4096];
(void)snprintf(internal, sizeof(internal), "%s/.lardon3d", root);
(void)snprintf(checkpoints, sizeof(checkpoints), "%s/checkpoints", internal);
(void)snprintf(database_path, sizeof(database_path), "%s/project.sqlite3",
internal);
CHECK(mkdir(internal, 0700) == 0 && mkdir(checkpoints, 0700) == 0);
Lardon3DAppState state;
lardon3d_app_state_init(&state);
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
CHECK(lardon3d_project_db_open(database_path, &state.project_db, error) ==
LARDON3D_PROJECT_DB_OK);
state.project_loaded = true;
(void)snprintf(state.project_path, sizeof(state.project_path), "%s", root);
state.hardware_profile = (Lardon3DHardwareProfile){
.logical_cpu_count = 16,
.page_size_bytes = 4096,
.memory_total_bytes = 16ULL * 1024 * 1024 * 1024,
.cpu_architecture = "test",
};
Lardon3DResourcePolicy resource_policy = policy();
state.resource_governor = lardon3d_resource_governor_create(
&state.hardware_profile, &resource_policy);
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 16);
CHECK(state.resource_governor && state.task_queue);
Lardon3DGeometricVerifierTaskConfiguration configuration = {
.verifier = lardon3d_geometric_verifier_default_parameters(),
};
unsigned char fingerprint[32];
lardon3d_geometric_verifier_fingerprint(&configuration.verifier, fingerprint);
CHECK(seed_reusable_parents(database_path, fingerprint));
const Lardon3DTaskKindRegistry *registry =
lardon3d_task_kind_registry_production();
const Lardon3DTaskKindDescriptor *descriptor = NULL;
CHECK(lardon3d_task_kind_registry_lookup(
registry, LARDON3D_GEOMETRIC_VERIFIER_TASK_KIND, 1, &descriptor) ==
LARDON3D_TASK_KIND_OK);
uint64_t task_id = 0;
CHECK(lardon3d_project_enqueue_geometric_verifier_task(&state, &configuration,
&task_id));
Lardon3DTaskSnapshot snapshot;
CHECK(wait_durable(state.project_db, task_id, TASK_COMPLETED));
Lardon3DProjectDbGeometricVerifierTask durable;
CHECK(lardon3d_project_db_load_geometric_verifier_task(
state.project_db, task_id, &durable) == LARDON3D_PROJECT_DB_OK &&
durable.after_match_result_id == (uint64_t)fixture_parent_count() &&
memcmp(durable.parameter_fingerprint, fingerprint, 32) == 0);
configuration.verifier.threshold_pixels = 1.6;
lardon3d_geometric_verifier_fingerprint(&configuration.verifier, fingerprint);
CHECK(add_reusable_results(database_path, fingerprint));
CHECK(setenv("LARDON3D_TEST_GEOMETRIC_PAUSE_AFTER_PUBLICATION", "1", 1) == 0);
CHECK(lardon3d_project_enqueue_geometric_verifier_task(&state, &configuration,
&task_id));
CHECK(wait_state(state.task_queue, task_id, TASK_PAUSED, &snapshot));
CHECK(lardon3d_project_db_load_geometric_verifier_task(
state.project_db, task_id, &durable) == LARDON3D_PROJECT_DB_OK &&
durable.after_match_result_id == 0);
CHECK(setenv("LARDON3D_TEST_GEOMETRIC_SKIP_FINISHED_CHECKPOINT", "1", 1) ==
0);
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = NULL;
lardon3d_project_db_close(state.project_db);
state.project_db = NULL;
CHECK(lardon3d_project_db_open(database_path, &state.project_db, error) ==
LARDON3D_PROJECT_DB_OK);
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 16);
CHECK(state.task_queue != NULL);
CHECK(unsetenv("LARDON3D_TEST_GEOMETRIC_PAUSE_AFTER_PUBLICATION") == 0);
CHECK(unsetenv("LARDON3D_TEST_GEOMETRIC_SKIP_FINISHED_CHECKPOINT") == 0);
Lardon3DProjectRecoverySummary recovery;
Lardon3DProjectDbResult recovery_result =
lardon3d_project_resume_recoverable_tasks(
&state, lardon3d_task_kind_registry_production(), &recovery);
if (recovery_result != LARDON3D_PROJECT_DB_OK || recovery.resumed != 1) {
(void)fprintf(
stderr,
"Recovery result=%d inspected=%zu resumed=%zu skipped=%zu failed=%zu\n",
recovery_result, recovery.inspected, recovery.resumed, recovery.skipped,
recovery.failed);
}
CHECK(recovery_result == LARDON3D_PROJECT_DB_OK && recovery.resumed == 1);
CHECK(wait_durable(state.project_db, task_id, TASK_COMPLETED));
CHECK(lardon3d_project_db_load_geometric_verifier_task(
state.project_db, task_id, &durable) == LARDON3D_PROJECT_DB_OK &&
durable.after_match_result_id == (uint64_t)fixture_parent_count());
configuration.verifier.threshold_pixels = 1.7;
lardon3d_geometric_verifier_fingerprint(&configuration.verifier, fingerprint);
CHECK(add_reusable_results(database_path, fingerprint));
CHECK(setenv("LARDON3D_TEST_GEOMETRIC_PAUSE_AFTER_PUBLICATION", "1", 1) == 0);
CHECK(lardon3d_project_enqueue_geometric_verifier_task(&state, &configuration,
&task_id));
CHECK(wait_state(state.task_queue, task_id, TASK_PAUSED, &snapshot));
CHECK(lardon3d_task_queue_cancel(state.task_queue, task_id));
CHECK(wait_state(state.task_queue, task_id, TASK_CANCELLED, &snapshot));
CHECK(unsetenv("LARDON3D_TEST_GEOMETRIC_PAUSE_AFTER_PUBLICATION") == 0);
lardon3d_task_queue_destroy(state.task_queue);
lardon3d_resource_governor_destroy(state.resource_governor);
lardon3d_project_db_close(state.project_db);
CHECK(exec_sql(
database_path,
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE geometric_verifier_tasks;"
"UPDATE metadata SET value=12 WHERE key='schema_version';COMMIT;"));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V13", "1", 1) == 0);
Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_IO_ERROR);
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V13") == 0);
CHECK(query_integer(database_path,
"SELECT value FROM metadata WHERE key='schema_version'",
12));
CHECK(
query_integer(database_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='geometric_verifier_tasks'",
0));
CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 13);
lardon3d_project_db_close(database);
CHECK(remove_tree(root));
return true;
}
int main(void) { return run_task_test() ? EXIT_SUCCESS : EXIT_FAILURE; }

View file

@ -48,6 +48,7 @@ static bool create_v9_database(const char *path) {
if (sqlite3_open(path, &connection) != SQLITE_OK) return false; if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
static const char sql[] = static const char sql[] =
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE geometric_verifier_tasks;"
"DROP TABLE geometric_verification_results;" "DROP TABLE geometric_verification_results;"
"DROP TABLE matcher_tasks;" "DROP TABLE matcher_tasks;"
"DROP TABLE match_results;" "DROP TABLE match_results;"
@ -94,7 +95,7 @@ static bool run_test(void) {
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDb *database = NULL; Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(database && lardon3d_project_db_schema_version(database) == 12); CHECK(database && lardon3d_project_db_schema_version(database) == 13);
Lardon3DProjectDbScanSet scanset; Lardon3DProjectDbScanSet scanset;
CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) == CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) ==
@ -453,7 +454,7 @@ static bool run_test(void) {
database = NULL; database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 12); CHECK(lardon3d_project_db_schema_version(database) == 13);
/* Verify persistence: load previously created results */ /* Verify persistence: load previously created results */
CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) == CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) ==
@ -493,10 +494,10 @@ static bool run_test(void) {
CHECK(create_v9_database(v9_path)); CHECK(create_v9_database(v9_path));
CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 9)); 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_open(v9_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 12); CHECK(lardon3d_project_db_schema_version(database) == 13);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
CHECK(query_integer(v9_path, CHECK(query_integer(v9_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='match_results'", 1)); "name='match_results'", 1));
@ -515,7 +516,7 @@ static bool run_test(void) {
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='matcher_tasks'", 0)); "name='matcher_tasks'", 0));
CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) == LARDON3D_PROJECT_DB_OK && CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) == LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 12); lardon3d_project_db_schema_version(database) == 13);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;

View file

@ -89,6 +89,7 @@ static bool downgrade_project_to_historical_v10(const char *database_path) {
} }
static const char sql[] = static const char sql[] =
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE geometric_verifier_tasks;"
"DROP TABLE geometric_verification_results;" "DROP TABLE geometric_verification_results;"
"DROP TABLE matcher_tasks;" "DROP TABLE matcher_tasks;"
"UPDATE metadata SET value=10 WHERE key='schema_version';" "UPDATE metadata SET value=10 WHERE key='schema_version';"
@ -351,7 +352,7 @@ static bool run_test(void) {
"name='matcher_tasks'", "name='matcher_tasks'",
0)); 0));
CHECK(reopen_runtime(&fixture)); CHECK(reopen_runtime(&fixture));
CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 12); CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 13);
CHECK(query_integer(database_path, CHECK(query_integer(database_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='matcher_tasks'", "name='matcher_tasks'",

View file

@ -89,7 +89,7 @@ static bool create_future_database(const char *path) {
} }
bool ok = sqlite3_exec(connection, bool ok = sqlite3_exec(connection,
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
"INSERT INTO metadata VALUES('schema_version',13);", "INSERT INTO metadata VALUES('schema_version',14);",
NULL, NULL, NULL) == SQLITE_OK; NULL, NULL, NULL) == SQLITE_OK;
return sqlite3_close(connection) == SQLITE_OK && 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; if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
static const char sql[] = static const char sql[] =
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE geometric_verifier_tasks;"
"DROP TABLE geometric_verification_results;" "DROP TABLE geometric_verification_results;"
"DROP TABLE matcher_tasks;" "DROP TABLE matcher_tasks;"
"DROP TABLE match_results;" "DROP TABLE match_results;"
@ -122,6 +123,7 @@ static bool create_v6_database(const char *path) {
if (sqlite3_open(path, &connection) != SQLITE_OK) return false; if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
static const char sql[] = static const char sql[] =
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE geometric_verifier_tasks;"
"DROP TABLE geometric_verification_results;" "DROP TABLE geometric_verification_results;"
"DROP TABLE matcher_tasks;" "DROP TABLE matcher_tasks;"
"DROP TABLE match_results;" "DROP TABLE match_results;"
@ -151,6 +153,7 @@ static bool create_v10_database(const char *path) {
} }
static const char sql[] = static const char sql[] =
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE geometric_verifier_tasks;"
"DROP TABLE geometric_verification_results;" "DROP TABLE geometric_verification_results;"
"DROP TABLE matcher_tasks;" "DROP TABLE matcher_tasks;"
"UPDATE metadata SET value=10 WHERE key='schema_version';" "UPDATE metadata SET value=10 WHERE key='schema_version';"
@ -338,7 +341,7 @@ static bool run_test(void) {
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDb *database = NULL; Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(database && lardon3d_project_db_schema_version(database) == 12); CHECK(database && lardon3d_project_db_schema_version(database) == 13);
bool legacy_pending = true; bool legacy_pending = true;
CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) == CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
@ -605,7 +608,7 @@ static bool run_test(void) {
LARDON3D_PROJECT_DB_INVALID_ARGUMENT); LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
lardon3d_project_db_close(contexts[0].database); lardon3d_project_db_close(contexts[0].database);
database = NULL; database = NULL;
CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
CHECK(query_integer(database_path, "SELECT count(*) FROM tasks WHERE task_id=1", 1)); 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_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
@ -627,7 +630,7 @@ static bool run_test(void) {
CHECK(create_v1_database(legacy_path)); CHECK(create_v1_database(legacy_path));
CHECK(lardon3d_project_db_open(legacy_path, &database, error) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_open(legacy_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 12); CHECK(lardon3d_project_db_schema_version(database) == 13);
CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK && CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK &&
strcmp(loaded_project.stable_id, "legacy-project") == 0); strcmp(loaded_project.stable_id, "legacy-project") == 0);
CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK);
@ -637,7 +640,7 @@ static bool run_test(void) {
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
CHECK(create_v1_database(failed_migration_path)); CHECK(create_v1_database(failed_migration_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0);
@ -662,7 +665,7 @@ static bool run_test(void) {
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer(v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(query_integer(v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
CHECK(create_v2_database(failed_v3_migration_path)); CHECK(create_v2_database(failed_v3_migration_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0);
@ -690,7 +693,7 @@ static bool run_test(void) {
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer(v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(query_integer(v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
CHECK(create_v3_database(failed_v4_path)); CHECK(create_v3_database(failed_v4_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0);
@ -707,7 +710,7 @@ static bool run_test(void) {
fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error); fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error);
} }
CHECK(v4_result == LARDON3D_PROJECT_DB_OK); CHECK(v4_result == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 12); CHECK(lardon3d_project_db_schema_version(database) == 13);
CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) == CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
@ -746,24 +749,24 @@ static bool run_test(void) {
fprintf(stderr, "Nouvelle tentative migration v7 (%d): %s\n", (int)retry_v7, error); fprintf(stderr, "Nouvelle tentative migration v7 (%d): %s\n", (int)retry_v7, error);
} }
CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK && CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 12); lardon3d_project_db_schema_version(database) == 13);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(create_v5_database(direct_v5_path)); CHECK(create_v5_database(direct_v5_path));
CHECK(query_integer(direct_v5_path, "SELECT value FROM metadata WHERE key='schema_version'", 5)); 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 && CHECK(lardon3d_project_db_open(direct_v5_path, &database, error) == LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 12); lardon3d_project_db_schema_version(database) == 13);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(create_v7_database(v8_path)); CHECK(create_v7_database(v8_path));
CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7)); 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_open(v8_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 12); CHECK(lardon3d_project_db_schema_version(database) == 13);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
CHECK(create_v7_database(failed_v8_path)); CHECK(create_v7_database(failed_v8_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0);
@ -774,7 +777,7 @@ static bool run_test(void) {
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='candidate_pairs'", 0)); "name='candidate_pairs'", 0));
CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == LARDON3D_PROJECT_DB_OK && CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 12); lardon3d_project_db_schema_version(database) == 13);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -789,10 +792,10 @@ static bool run_test(void) {
"name='matcher_tasks'", "name='matcher_tasks'",
0)); 0));
CHECK(lardon3d_project_db_open(v10_path, &database, error) == LARDON3D_PROJECT_DB_OK && CHECK(lardon3d_project_db_open(v10_path, &database, error) == LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 12); lardon3d_project_db_schema_version(database) == 13);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer(v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 12)); CHECK(query_integer(v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 13));
CHECK(query_integer(v10_path, CHECK(query_integer(v10_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='matcher_tasks'", "name='matcher_tasks'",
@ -815,7 +818,7 @@ static bool run_test(void) {
1)); 1));
CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) == CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 12); CHECK(lardon3d_project_db_schema_version(database) == 13);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;