feat(reconstruction): add durable geometric verifier
This commit is contained in:
parent
558f00cd6f
commit
70a5a18ad6
25 changed files with 3166 additions and 81 deletions
20
README.md
20
README.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
301
docs/architecture/geometric_verifier.md
Normal file
301
docs/architecture/geometric_verifier.md
Normal 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.
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
3–7 % de latence isolée pour réserver quatre threads logiques au desktop.
|
3–7 % 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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
65
include/lardon3d/geometric_verifier.h
Normal file
65
include/lardon3d/geometric_verifier.h
Normal 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
|
||||||
35
include/lardon3d/geometric_verifier_task.h
Normal file
35
include/lardon3d/geometric_verifier_task.h
Normal 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
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
72
meson.build
72
meson.build
|
|
@ -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
411
src/geometric_verifier.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
321
src/geometric_verifier_task.c
Normal file
321
src/geometric_verifier_task.c
Normal 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,
|
||||||
|
¶meters, &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(¶meters, fingerprint);
|
||||||
|
if (!lardon3d_geometric_verifier_parameters_valid(¶meters) ||
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
171
src/project_db.c
171
src/project_db.c
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
455
tests/benchmark_geometric_verifier.cpp
Normal file
455
tests/benchmark_geometric_verifier.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
807
tests/test_geometric_verifier_core.cpp
Normal file
807
tests/test_geometric_verifier_core.cpp
Normal 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(¶meters));
|
||||||
|
unsigned char first[32], second[32], changed[32];
|
||||||
|
lardon3d_geometric_verifier_fingerprint(¶meters, first);
|
||||||
|
lardon3d_geometric_verifier_fingerprint(¶meters, 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(
|
||||||
|
¶meters, LARDON3D_GEOMETRIC_ALGORITHM_USAC_MAGSAC, 1, default_bytes));
|
||||||
|
CHECK(lardon3d_geometric_verifier_fingerprint_bytes(
|
||||||
|
¶meters, 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(
|
||||||
|
¶meters, 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(¶meters));
|
||||||
|
CHECK(lardon3d_geometric_verifier_fingerprint_bytes(
|
||||||
|
¶meters, LARDON3D_GEOMETRIC_ALGORITHM_USAC_MAGSAC, 1, default_bytes));
|
||||||
|
parameters.min_inlier_ratio = -0.0;
|
||||||
|
CHECK(lardon3d_geometric_verifier_fingerprint_bytes(
|
||||||
|
¶meters, 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(¶meters));
|
||||||
|
parameters = lardon3d_geometric_verifier_default_parameters();
|
||||||
|
parameters.threshold_pixels = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
CHECK(!lardon3d_geometric_verifier_parameters_valid(¶meters));
|
||||||
|
parameters = lardon3d_geometric_verifier_default_parameters();
|
||||||
|
parameters.max_iterations = std::numeric_limits<uint32_t>::max();
|
||||||
|
CHECK(!lardon3d_geometric_verifier_parameters_valid(¶meters));
|
||||||
|
parameters = lardon3d_geometric_verifier_default_parameters();
|
||||||
|
parameters.min_inlier_count = 8193;
|
||||||
|
CHECK(!lardon3d_geometric_verifier_parameters_valid(¶meters));
|
||||||
|
|
||||||
|
parameters = lardon3d_geometric_verifier_default_parameters();
|
||||||
|
CHECK(lardon3d_geometric_verifier_fingerprint_bytes(
|
||||||
|
¶meters, 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(¶meters, 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, ¶meters,
|
||||||
|
&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, ¶meters,
|
||||||
|
&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, ¶meters,
|
||||||
|
&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, ¶meters,
|
||||||
|
&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, ¶meters,
|
||||||
|
&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,
|
||||||
|
¶meters, &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, ¶meters,
|
||||||
|
&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, ¶meters, &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, ¶meters,
|
||||||
|
&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, ¶meters, &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, ¶meters,
|
||||||
|
&result, &reused) == LARDON3D_GEOMETRIC_VERIFIER_ESTIMATOR_ERROR);
|
||||||
|
CHECK(unsetenv("LARDON3D_TEST_GEOMETRIC_ESTIMATOR") == 0);
|
||||||
|
unsigned char fingerprint[32];
|
||||||
|
lardon3d_geometric_verifier_fingerprint(¶meters, 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,
|
||||||
|
¶meters, &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(¶meters, 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, ¶meters, &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,
|
||||||
|
¶meters, &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),
|
||||||
|
¶meters, &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,
|
||||||
|
¶meters, &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,
|
||||||
|
¶meters, &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,
|
||||||
|
¶meters, &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, ¶meters,
|
||||||
|
&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, ¶meters,
|
||||||
|
&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, ¶meters,
|
||||||
|
&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,
|
||||||
|
¶meters, &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,
|
||||||
|
¶meters, &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,
|
||||||
|
¶meters, &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;
|
||||||
|
}
|
||||||
358
tests/test_geometric_verifier_task.c
Normal file
358
tests/test_geometric_verifier_task.c
Normal 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; }
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue