feat(reconstruction): add geometric verification model

This commit is contained in:
fy59 2026-08-09 14:01:04 +02:00
parent d97291ad7b
commit 558f00cd6f
15 changed files with 1227 additions and 38 deletions

View file

@ -27,7 +27,8 @@ Lardon3D ne vise pas simplement "dossier de photos → objet 3D", mais "ensemble
- **Image View** : vues triées et filtrées pour la TUI
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
- **Project Database v11** : tâches, catalogue, Feature Store, matching et tâches Matcher durables
- **Project Database v12** : tâches, catalogue, matching et résultats géométriques durables
- **Geometric Verification Model v1** : identité, masque d'inliers et modèle 3×3 persistants
- **Task Kind Registry** : identité métier durable et reconstruction runtime explicite
- **Recovery projet** : reprise automatique sélective et bornée des imports récupérables
- **Task Queue** : file FIFO avec sélection adaptative et backpressure
@ -111,6 +112,7 @@ Acquisitions
- [Candidate Pair](docs/architecture/candidate_pair.md)
- [Match Result](docs/architecture/match_result.md)
- [Matcher](docs/architecture/matcher.md)
- [Geometric Verification](docs/architecture/geometric_verification.md)
- [Backend Vulkan ORB](docs/architecture/vulkan_matcher.md)
- [Viewer](docs/architecture/viewer.md)
- [Revue des fondations](docs/architecture/foundation_review.md)
@ -152,11 +154,12 @@ Pour les changements sensibles à la mémoire ou à la concurrence, ajouter ASan
Lardon3D est en développement actif. La persistance des tâches, le catalogue,
le Feature Store multipasse, le Visual Index ORB, Candidate Pair Generator
et Matcher v1 sont implémentés. Le runtime Feature + Matcher emploie des tâches
et Matcher v1 sont implémentés. Geometric Verification Model v1 est implémenté ;
le Geometric Verifier reste planifié. Le runtime Feature + Matcher emploie des tâches
durables, de petits lots, le Resource Governor interactif et un hot path Vulkan
ORB exact avec fallback CPU. La feasibility Vulkan SIFT/RootSIFT a été rejetée ;
ces deux matchers restent sur OpenCV L2. DAG générique,
vérification géométrique, SfM et viewer restent des tickets séparés planifiés.
calcul de vérification géométrique, SfM et viewer restent des tickets séparés planifiés.
## Licence

View file

@ -0,0 +1,175 @@
# Geometric Verification
## Scope
Geometric Verification Model v1 est le contrat scientifique persistant placé après le Matcher.
Il stocke un résultat terminé, compact et immutable. Il n'est ni un moteur de calcul ni une tâche.
Aucun RANSAC, USAC, MAGSAC, calcul d'inliers ou backend géométrique n'appartient à ce ticket.
## Position in reconstruction pipeline
La chaîne d'ownership est :
```text
Feature Set → Candidate Pair → Match Result → Geometric Verification Result
```
Le masque indexe exclusivement l'ordre des entrées du Match File canonique du Match Result. Il
n'indexe directement ni les features, ni la Candidate Pair, ni un ordre temporaire de backend.
## Scientific ownership
Le parent scientifique est `match_result_id`. L'API accepte uniquement un Match Result existant,
`MATCHED`, avec `match_count` strictement positif. `NO_MATCH` et les erreurs runtime ne peuvent pas
produire de résultat géométrique.
## Parent Match Result
Le Match Store reste propriétaire de la validation du Match File. La création consulte le parent
et son `match_count` en DB ; elle ne relit pas l'asset. Un load valide aussi l'existence et l'état du
parent afin qu'une ligne corrompue ne soit jamais rendue comme résultat valide.
## Persistent identity
L'identité demandée et unique est :
```text
(match_result_id, verifier_kind, verifier_version, parameter_fingerprint)
```
Le fingerprint est le SHA-256 opaque de 32 octets déjà standard dans le projet. Il représentera
un encodage de paramètres versionné, stable, à ordre de champs explicite et, pour les nombres
binaires, little-endian. Aucun timestamp, résultat, PID, durée ou identifiant matériel n'y entre.
## Verifier kind
v1 supporte uniquement `FUNDAMENTAL`, valeur persistante stable 1. Aucun comportement fictif
`ESSENTIAL` ou `HOMOGRAPHY` n'est réservé dans l'API publique.
## Persistent states
- `GEOMETRIC_REJECTED=1` : calcul scientifique terminé, critère non satisfait ;
- `GEOMETRIC_VERIFIED=2` : calcul scientifique terminé, critère satisfait.
`FAILED`, `RUNNING`, `PAUSED` et `CANCELLED` appartiennent au Task Runtime. REJECTED peut conserver
un nombre d'inliers non nul.
## Model representation
FUNDAMENTAL utilise neuf colonnes SQLite `REAL`, en ordre ligne-major `m00` à `m22`. SQLite
convertit les valeurs numériques en binary64 sans exposer une ABI C. VERIFIED exige les neuf
valeurs présentes et finies. REJECTED exige les neuf valeurs NULL. Le modèle n'impose ni rang 2,
ni déterminant, ni normalisation ou échelle canonique ; ces règles relèvent du futur verifier.
## Inlier representation
Le masque est un BLOB SQLite obligatoire de taille exacte `ceil(match_count / 8)`. Pour l'entrée
`i`, `byte_index=i/8`, `bit_index=i%8` et le masque vaut `1u << bit_index`. Le bit 0 est donc le bit
de poids faible de l'octet 0. Cette convention est indépendante de l'endianness CPU et de l'ABI.
Les bits de padding du dernier octet valent zéro et le popcount est exactement `inlier_count`.
Le masque existe pour REJECTED comme pour VERIFIED. Avec 8192 matches, il mesure au maximum
1024 octets. Un BLOB SQLite évite les milliers de lignes secondaires et la publication, le hash,
le nettoyage et la récupération d'un asset externe d'environ 1 Kio. Une liste `uint32_t` serait
jusqu'à 32 fois plus grande au cas dense et aurait un encodage supplémentaire à versionner.
## Invariants
- `0 <= inlier_count <= parent.match_count <= 8192` ;
- longueur, padding et popcount du masque sont canoniques ;
- REJECTED possède un masque cohérent et aucun modèle ;
- VERIFIED possède un masque cohérent et exactement neuf valeurs finies ;
- kind, version et fingerprint ont une sérialisation stable ;
- une ligne publiée est complète et immutable.
Exemple : pour 100 matches, FUNDAMENTAL v1/fingerprint X peut publier REJECTED avec 23 inliers,
un masque de 13 octets et aucun modèle. Une autre identité peut publier VERIFIED avec 67 inliers,
le même format de masque et une matrice 3×3 finie.
## Persistence semantics
Une création valide puis insère identité, état, masque et modèle dans une transaction courte. Le
calcul futur se fera entièrement avant cette transaction. SQLite fournit l'atomicité ; aucun asset
ou journal secondaire n'est créé.
## Reuse
Le reuse cherche uniquement l'identité exacte, jamais le résultat le plus récent. Une identité
existante retourne une erreur de contrainte à `create`; le runtime fera `find`, validera puis
réutilisera. `INSERT OR REPLACE` est interdit, même si le nouveau contenu semble identique.
## Invalidations
Un nouveau Match Result possède un nouvel ID et ne réutilise donc aucun ancien résultat
géométrique. La FK emploie `ON DELETE CASCADE` : supprimer explicitement le parent supprime ses
enfants et ne crée pas d'orphelin. Aucun moteur d'invalidation parallèle n'est nécessaire.
## Project DB schema
Project DB v12 ajoute `geometric_verification_results`, une contrainte UNIQUE sur l'identité et un
index de pagination `(match_result_id, geometric_verification_result_id)`. Les CHECK SQL portent
les bornes scalaires, tailles locales et nullabilité modèle/état. La cohérence avec le parent, le
padding, le popcount et la finitude restent validés en C.
## API
L'API publique implémente :
- `lardon3d_project_db_create_geometric_verification_result()` ;
- `lardon3d_project_db_load_geometric_verification_result()` ;
- `lardon3d_project_db_find_geometric_verification_result()` ;
- `lardon3d_project_db_list_geometric_verification_results()`.
La liste est bornée à 256 entrées, filtrée par parent puis ordonnée par ID croissant avec curseur.
Le résultat en mémoire contient son `created_at` et son masque dans une capacité fixe de 1024
octets : aucun ownership dynamique ni fonction de destruction. Les fonctions copient fingerprint,
masque et neuf coefficients ; l'appelant conserve ses entrées.
Parent absent retourne `NOT_FOUND`; parent NO_MATCH ou parent incohérent retourne `CONSTRAINT` à
la création. Masque, modèle ou arguments locaux invalides retournent `INVALID_ARGUMENT`; duplicate
identity retourne `CONSTRAINT`. Un loader qui rencontre une ligne ou un parent incohérent retourne
`CORRUPT`, sans résultat partiel.
## Resource bounds
Un résultat contient au plus 1024 octets de masque et 72 octets de valeurs numériques, plus de
petites métadonnées. Une page est bornée. Le loader vérifie les entiers et tailles SQLite avant
tout cast ou copie. Il n'existe ni cache global, ni lecture non bornée, ni Content Store associé.
Le Match File parent mesure au plus 98 336 octets ; le futur job peut donc rester une petite unité.
## Error ownership
Seuls les résultats scientifiques terminés sont persistés. OOM, exception, annulation, timeout,
device lost, I/O transitoire ou panne de thread appartiennent à l'exécution de tâche. État du modèle
et état d'exécution sont deux contrats distincts.
## Recovery semantics
Après commit, le résultat est complet et réutilisable après réouverture. Avant commit, le rollback
ne laisse aucune ligne partielle. Un loader rejette toute ligne incohérente comme corruption au
lieu de réparer ou d'interpréter au mieux.
## Future verifier execution contract
Le prochain ticket prendra un Match Result et son Match File borné. L'accès nécessaire existe via
`lardon3d_feature_reader_keypoints()`, borné à 256 keypoints par appel ; l'intégration devra relier
les deux Feature Sets et les indices du Match File sans modifier le Feature Store. Le verifier
estimera hors transaction, dérivera état/masque/modèle, publiera en une courte transaction,
checkpoint puis libérera les buffers. Une paire est l'unité atomique. Task Runtime et Resource
Governor décideront admission, threads et lots ; zram/swap ne sont jamais un budget.
Un backend reste hors identité seulement s'il est scientifiquement transparent. Sinon son
algorithme ou contrat doit apparaître dans kind/version/fingerprint avant publication. Toute seed
influençant le résultat doit avoir une politique déterministe versionnée ou être couverte par le
fingerprint. Aucun nombre de threads ou hardware ID n'est un paramètre scientifique par défaut.
## Explicitly out of scope
Le calcul géométrique, le choix RANSAC/USAC/MAGSAC, OpenCV geometry, GPU, Vulkan, OpenCL, shader,
task kind, worker, checkpoint et nouvelle orchestration sont explicitement hors périmètre.
## Versioning
Project DB schema version 12 décrit le stockage. `verifier_version` décrit indépendamment le
contrat scientifique. Changer un algorithme n'impose une migration DB que si la représentation
persistante change.

View file

@ -61,4 +61,7 @@ contrainte `UNIQUE`.
Le prochain modèle possède ses propres statistiques géométriques, dont un
éventuel `inlier_count`.
NEXT: GEOMETRIC VERIFICATION MODEL
Geometric Verification Model v1 est désormais l'enfant scientifique persistant
de ce résultat dans Project DB v12.
NEXT: GEOMETRIC VERIFIER v1

View file

@ -92,6 +92,10 @@ estimation couvrant ce working set ; aucune seconde logique de budget n'est
introduite. Sa ligne durable `matcher_tasks` appartient au schéma Project DB
v11 ; le Match Result reste le contrat publié en v10.
Project DB v12 ajoute un enfant `Geometric Verification Result` référencé par
`match_result_id`. Le Matcher ne calcule, ne stocke et ne valide aucun inlier
géométrique ; seul son Match File canonique définit l'ordre indexé par le masque.
### Backend Vulkan ORB
La frontière évaluée remplace uniquement KNN Hamming par un compute top-2 : un
@ -113,4 +117,4 @@ mêmes paires attendues, l'ordre et la sérialisation stables. Aucune promesse
bit-à-bit cross-platform n'est faite pour les distances L2 flottantes.
ORB/Hamming bénéficie d'une garantie plus forte grâce à sa distance entière.
NEXT: GEOMETRIC VERIFICATION MODEL
NEXT: GEOMETRIC VERIFIER v1

View file

@ -1,6 +1,7 @@
# Base de données projet Lardon3D
> Version courante : **v11**. La migration transactionnelle v10→v11 ajoute
> Version courante : **v12**. La migration transactionnelle v11→v12 ajoute le
> modèle immutable `geometric_verification_results`. La migration v10→v11 ajoute
> `matcher_tasks` pour la tâche Matcher durable. La version v10 publiée ajoute
> uniquement `match_results` pour le Match Result Model. La migration v8→v9
> ajoute la table `candidate_pair_generate_tasks` pour la tâche durable
@ -396,19 +397,65 @@ CREATE INDEX match_results_feature_set_b_idx
- `lardon3d_project_db_record_matcher_task()` — UPSERT configuration/curseur
- `lardon3d_project_db_load_matcher_task()` — SELECT par task_id
## Schéma v12 implémenté
La migration v11→v12 ajoute uniquement `geometric_verification_results`. Le
parent est un Match Result `MATCHED`; sa validation interligne reste dans l'API.
Schéma abrégé (la chaîne SQL exécutable canonique reste dans `src/project_db.c`) :
```sql
CREATE TABLE geometric_verification_results(
geometric_verification_result_id INTEGER PRIMARY KEY AUTOINCREMENT
CHECK(geometric_verification_result_id>0),
match_result_id INTEGER NOT NULL
REFERENCES match_results(match_result_id) ON DELETE CASCADE,
verifier_kind INTEGER NOT NULL CHECK(verifier_kind=1),
verifier_version INTEGER NOT NULL
CHECK(verifier_version>0 AND verifier_version<=4294967295),
parameter_fingerprint BLOB NOT NULL
CHECK(length(parameter_fingerprint)=32),
status INTEGER NOT NULL CHECK(status IN (1,2)),
inlier_count INTEGER NOT NULL
CHECK(inlier_count>=0 AND inlier_count<=8192),
inlier_mask BLOB NOT NULL
CHECK(length(inlier_mask)>=1 AND length(inlier_mask)<=1024),
model_m00 REAL, model_m01 REAL, model_m02 REAL,
model_m10 REAL, model_m11 REAL, model_m12 REAL,
model_m20 REAL, model_m21 REAL, model_m22 REAL,
created_at INTEGER NOT NULL CHECK(created_at>=0),
CHECK(/* REJECTED: neuf NULL ; VERIFIED: neuf non-NULL */),
UNIQUE(match_result_id, verifier_kind, verifier_version,
parameter_fingerprint)
);
CREATE INDEX geometric_verification_results_parent_idx
ON geometric_verification_results(
match_result_id, geometric_verification_result_id
);
```
Les valeurs stables sont FUNDAMENTAL=1, GEOMETRIC_REJECTED=1 et
GEOMETRIC_VERIFIED=2. REJECTED interdit le modèle ; VERIFIED exige neuf valeurs
finies. Pour les deux états, l'API impose taille canonique, padding nul et
popcount exact du masque, ainsi que `inlier_count <= parent.match_count`.
L'index parent sert la liste paginée ; la contrainte UNIQUE sert le find exact.
Le contrat complet, dont l'ordre des bits, est dans
`geometric_verification.md`.
## Ouverture et migrations
Une DB vide reçoit la chaîne de schémas jusqu'à v11 dans une transaction
Une DB vide reçoit la chaîne de schémas jusqu'à v12 dans une transaction
`BEGIN IMMEDIATE`. Une DB v1 reçoit transactionnellement les colonnes nullable
`task_kind` et `task_kind_version`, puis les migrations v2→v3. Les anciennes lignes restent
`NULL/NULL`, sans type inventé et sans perte des projets, tâches, checkpoints ou
artefacts. Une interruption ou erreur provoque un rollback complet. Les DB v1,
v2, v3, v4, v5, v6, v7, v8, v9 et v10 sont migrées séquentiellement vers v11.
v2, v3, v4, v5, v6, v7, v8, v9, v10 et v11 sont migrées séquentiellement vers v12.
Une v10 publiée est validée comme telle avant que v10→v11 crée
`matcher_tasks` ; son absence n'est donc pas une corruption. Une version future est refusée et une DB contenant
des tables sans métadonnée de version est considérée corrompue. La fonction
interne de migration applique uniquement la chaîne séquentielle connue jusqu'à
v11 ; une valeur hors de 1..11 est refusée.
v12 ; une valeur hors de 1..12 est refusée. La failure injectée v12 rollbacke
la table, l'index et le changement de version, laissant une vraie v11 utilisable.
Migration v1→v2 exacte, exécutée entre `BEGIN IMMEDIATE` et `COMMIT` :
@ -545,8 +592,8 @@ ouvert.
## Statut
**IMPLEMENTED** — SQLite système, schéma v11 et migrations séquentielles
v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11, identité projet, transactions
**IMPLEMENTED** — SQLite système, schéma v12 et migrations séquentielles
v1→v2→v3→v4→v5→v6→v7→v8→v9→v10→v11→v12, identité projet, transactions
tâche+checkpoint, pagination de reprise et artefacts génériques.
**IMPLEMENTED** — ouverture/fermeture avec le projet, identité INI/DB cohérente,

View file

@ -97,10 +97,20 @@ Index, persistance, canonicalisation, idempotence et tâche durable.
| **Orchestration** | `matcher.run` traite les Candidate Pairs par pages et lots durables de 1/2/4/8. |
**Statut :** IMPLEMENTED v1 — Matcher, Match Store, reprise idempotente et Task
durable. La vérification géométrique reste l'étape suivante, non commencée.
durable. Le calcul Geometric Verifier reste l'étape suivante, non commencée.
Le Match Result appartient à Project DB v10 et la tâche durable `matcher.run`
à Project DB v11.
### F2. Geometric Verification
Project DB v12 implémente uniquement le modèle scientifique persistant. Chaque
résultat appartient à un Match Result `MATCHED`, possède une identité exacte,
un masque compact borné et, si VERIFIED, une matrice fondamentale 3×3 finie.
La publication est atomique et immutable. Le calcul, son task kind et le choix
d'algorithme restent planifiés dans Geometric Verifier v1.
**Statut :** MODEL IMPLEMENTED v1 — VERIFIER PLANNED.
---
### G. Tracks
@ -229,7 +239,8 @@ Image Catalog (B) ──► Feature Store (C)
## Statut du pipeline
Import, Image Catalog, Feature Extraction, Feature Store, Visual Index,
Candidate Pair et Matching v1 sont **IMPLEMENTED**. Geometric Verification,
Candidate Pair, Matching v1 et Geometric Verification Model v1 sont
**IMPLEMENTED**. Geometric Verifier,
Tracks et SfM sont **PLANNED**.
Ce document décrit la vision architecturale cible du pipeline de

View file

@ -55,6 +55,14 @@ chaque paire et entre les lots. Un crash après publication mais avant le
checkpoint revoit la paire : le Matcher réutilise alors le Match Result et ne
recalcule pas les descripteurs.
## Geometric Verification Model
Project DB v12 stocke un résultat borné à 1024 octets de masque et neuf
binary64. Le modèle ne lit aucun signal matériel et ne modifie pas le Governor.
Le futur verifier calculera une paire hors transaction, publiera par une courte
transaction, checkpoint puis libérera ses buffers avant réadmission. Admission,
threads et lots resteront exclusivement décidés par Runtime et Governor.
## GPU et files
La Radeon 780M est UMA : toute mémoire GPU compte aussi comme pression RAM. Un

View file

@ -111,6 +111,11 @@ par lots adaptatifs de 1, 2, 4 ou 8. Il persiste le curseur
effectue une rupture de séquence avant le suivant. Une paire repassée après un
crash est réutilisée par son Match Result.
Geometric Verification Model v1 n'ajoute aucun task kind ni état runtime.
Project DB v12 ne contient que les deux états scientifiques terminaux. Le futur
Geometric Verifier v1 devra obtenir réservation, pause/cancel, lot et checkpoint
dans un ticket séparé avant toute exécution.
Le chemin de production de l'import ne possède plus de thread ni de drapeau
d'annulation privés. Son wrapper TUI ne fait qu'enqueue/cancel/observer la
tâche générique. Chaque callback traite un lot borné, checkpoint hors mutex de

View file

@ -9,7 +9,7 @@
#include <lardon3d/task.h>
enum {
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 11,
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 12,
LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
@ -18,6 +18,9 @@ enum {
LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX = 256,
LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX = 256,
LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX = 256,
LARDON3D_PROJECT_DB_GEOMETRIC_RESULT_PAGE_MAX = 256,
LARDON3D_PROJECT_DB_INLIER_MASK_MAX = 1024,
LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS = 9,
LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY = 256,
LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY = 256,
LARDON3D_PROJECT_DB_SHA256_SIZE = 32,
@ -158,6 +161,30 @@ typedef struct {
int64_t created_at;
} Lardon3DProjectDbMatchResult;
typedef enum {
LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL = 1,
} Lardon3DGeometricVerifierKind;
typedef enum {
LARDON3D_GEOMETRIC_REJECTED = 1,
LARDON3D_GEOMETRIC_VERIFIED = 2,
} Lardon3DGeometricVerificationStatus;
typedef struct {
uint64_t geometric_verification_result_id;
uint64_t match_result_id;
Lardon3DGeometricVerifierKind verifier_kind;
uint32_t verifier_version;
unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE];
Lardon3DGeometricVerificationStatus status;
uint32_t inlier_count;
size_t inlier_mask_size;
unsigned char inlier_mask[LARDON3D_PROJECT_DB_INLIER_MASK_MAX];
bool has_model;
double model[LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS];
int64_t created_at;
} Lardon3DProjectDbGeometricVerificationResult;
typedef enum {
LARDON3D_PROJECT_DB_IMAGE_REGISTERED = 0,
LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT
@ -507,4 +534,24 @@ Lardon3DProjectDbResult lardon3d_project_db_list_match_results(
Lardon3DProjectDb *database, uint64_t after_match_result_id,
Lardon3DProjectDbMatchResult *results, size_t capacity, size_t *count);
Lardon3DProjectDbResult lardon3d_project_db_create_geometric_verification_result(
Lardon3DProjectDb *database, uint64_t match_result_id,
Lardon3DGeometricVerifierKind verifier_kind, uint32_t verifier_version,
const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE],
Lardon3DGeometricVerificationStatus status, uint32_t inlier_count,
const unsigned char *inlier_mask, size_t inlier_mask_size, const double *model,
int64_t created_at, Lardon3DProjectDbGeometricVerificationResult *result);
Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verification_result(
Lardon3DProjectDb *database, uint64_t geometric_verification_result_id,
Lardon3DProjectDbGeometricVerificationResult *result);
Lardon3DProjectDbResult lardon3d_project_db_find_geometric_verification_result(
Lardon3DProjectDb *database, uint64_t match_result_id,
Lardon3DGeometricVerifierKind verifier_kind, uint32_t verifier_version,
const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE],
Lardon3DProjectDbGeometricVerificationResult *result);
Lardon3DProjectDbResult lardon3d_project_db_list_geometric_verification_results(
Lardon3DProjectDb *database, uint64_t match_result_id,
uint64_t after_geometric_verification_result_id,
Lardon3DProjectDbGeometricVerificationResult *results, size_t capacity, size_t *count);
#endif

View file

@ -507,6 +507,22 @@ match_result_test = executable(
test('match-result', match_result_test, timeout: 30)
geometric_verification_test = executable(
'test-geometric-verification',
sources: [
'tests/test_geometric_verification.c',
'src/project_db.c',
'src/task.c',
'src/resource_governor.c',
'src/resource_snapshot.c',
],
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl],
)
test('geometric-verification', geometric_verification_test, timeout: 30)
project_test = executable(
'test-project',
sources: [

View file

@ -238,6 +238,31 @@ static const char schema_matcher_task_v11[] =
"matcher_kind INTEGER NOT NULL CHECK(matcher_kind BETWEEN 0 AND 2),"
"ratio_threshold REAL NOT NULL CHECK(ratio_threshold>0.0 AND ratio_threshold<1.0));";
static const char schema_geometric_verification_v12[] =
"CREATE TABLE geometric_verification_results("
"geometric_verification_result_id INTEGER PRIMARY KEY AUTOINCREMENT "
"CHECK(geometric_verification_result_id>0),"
"match_result_id INTEGER NOT NULL REFERENCES match_results(match_result_id) ON DELETE CASCADE,"
"verifier_kind INTEGER NOT NULL CHECK(verifier_kind=1),"
"verifier_version INTEGER NOT NULL CHECK(verifier_version>0 AND verifier_version<=4294967295),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32),"
"status INTEGER NOT NULL CHECK(status IN (1,2)),"
"inlier_count INTEGER NOT NULL CHECK(inlier_count>=0 AND inlier_count<=8192),"
"inlier_mask BLOB NOT NULL CHECK(length(inlier_mask)>=1 AND length(inlier_mask)<=1024),"
"model_m00 REAL,model_m01 REAL,model_m02 REAL,"
"model_m10 REAL,model_m11 REAL,model_m12 REAL,"
"model_m20 REAL,model_m21 REAL,model_m22 REAL,"
"created_at INTEGER NOT NULL CHECK(created_at>=0),"
"CHECK((status=1 AND model_m00 IS NULL AND model_m01 IS NULL AND model_m02 IS NULL AND "
"model_m10 IS NULL AND model_m11 IS NULL AND model_m12 IS NULL AND model_m20 IS NULL AND "
"model_m21 IS NULL AND model_m22 IS NULL) OR "
"(status=2 AND model_m00 IS NOT NULL AND model_m01 IS NOT NULL AND model_m02 IS NOT NULL AND "
"model_m10 IS NOT NULL AND model_m11 IS NOT NULL AND model_m12 IS NOT NULL AND "
"model_m20 IS NOT NULL AND model_m21 IS NOT NULL AND model_m22 IS NOT NULL)),"
"UNIQUE(match_result_id,verifier_kind,verifier_version,parameter_fingerprint));"
"CREATE INDEX geometric_verification_results_parent_idx ON "
"geometric_verification_results(match_result_id,geometric_verification_result_id);";
static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) {
if (destination) {
(void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : "");
@ -327,7 +352,7 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
}
if (from_version != 0 && from_version != 1 && from_version != 2 && from_version != 3 &&
from_version != 4 && from_version != 5 && from_version != 6 && from_version != 7 &&
from_version != 8 && from_version != 9 && from_version != 10) {
from_version != 8 && from_version != 9 && from_version != 10 && from_version != 11) {
return LARDON3D_PROJECT_DB_CORRUPT;
}
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
@ -568,6 +593,22 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
"finish schema v11 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version < 12) {
result = execute(database, schema_geometric_verification_v12,
"migrate schema v11 to v12");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V12");
if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) {
result = execute(database, "INSERT INTO missing_test_table VALUES(1)",
"forced migration v12 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=12 WHERE key='schema_version' AND value=11",
"finish schema v12 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit migration");
}
@ -682,7 +723,8 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje
"feature_support_members",
"candidate_pairs",
"matcher_tasks",
"match_results"};
"match_results",
"geometric_verification_results"};
for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) &&
result == LARDON3D_PROJECT_DB_OK;
++index) {
@ -3751,6 +3793,366 @@ Lardon3DProjectDbResult lardon3d_project_db_list_match_results(
return db_result;
}
static size_t geometric_mask_size(uint32_t match_count) {
return ((size_t)match_count + 7U) / 8U;
}
static uint32_t geometric_mask_popcount(const unsigned char *mask, size_t size) {
uint32_t count = 0;
for (size_t byte_index = 0; byte_index < size; ++byte_index) {
unsigned int value = mask[byte_index];
while (value != 0U) {
value &= value - 1U;
++count;
}
}
return count;
}
static bool valid_geometric_mask(const unsigned char *mask, size_t size, uint32_t match_count,
uint32_t inlier_count) {
size_t expected_size = geometric_mask_size(match_count);
if (!mask || size != expected_size || expected_size == 0 ||
expected_size > LARDON3D_PROJECT_DB_INLIER_MASK_MAX || inlier_count > match_count) {
return false;
}
unsigned int used_bits = match_count % 8U;
if (used_bits != 0U) {
unsigned int padding_mask = 0xffU << used_bits;
if (((unsigned int)mask[size - 1] & padding_mask) != 0U) {
return false;
}
}
return geometric_mask_popcount(mask, size) == inlier_count;
}
static bool valid_geometric_model(Lardon3DGeometricVerificationStatus status,
const double *model) {
if (status == LARDON3D_GEOMETRIC_REJECTED) {
return model == NULL;
}
if (status != LARDON3D_GEOMETRIC_VERIFIED || !model) {
return false;
}
for (size_t index = 0; index < LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS; ++index) {
if (!isfinite(model[index])) {
return false;
}
}
return true;
}
static const char geometric_result_columns[] =
"g.geometric_verification_result_id,g.match_result_id,g.verifier_kind,"
"g.verifier_version,g.parameter_fingerprint,g.status,g.inlier_count,g.inlier_mask,"
"g.model_m00,g.model_m01,g.model_m02,g.model_m10,g.model_m11,g.model_m12,"
"g.model_m20,g.model_m21,g.model_m22,g.created_at,m.result_status,m.match_count ";
static bool read_geometric_result(sqlite3_stmt *statement,
Lardon3DProjectDbGeometricVerificationResult *result) {
if (!statement || !result) {
return false;
}
for (int column = 0; column <= 7; ++column) {
int expected_type = column == 4 || column == 7 ? SQLITE_BLOB : SQLITE_INTEGER;
if (sqlite3_column_type(statement, column) != expected_type) {
return false;
}
}
if (sqlite3_column_type(statement, 17) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 18) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 19) != SQLITE_INTEGER) {
return false;
}
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
sqlite3_int64 parent_id = sqlite3_column_int64(statement, 1);
sqlite3_int64 kind = sqlite3_column_int64(statement, 2);
sqlite3_int64 version = sqlite3_column_int64(statement, 3);
sqlite3_int64 status = sqlite3_column_int64(statement, 5);
sqlite3_int64 inlier_count = sqlite3_column_int64(statement, 6);
sqlite3_int64 created_at = sqlite3_column_int64(statement, 17);
sqlite3_int64 parent_status = sqlite3_column_int64(statement, 18);
sqlite3_int64 match_count = sqlite3_column_int64(statement, 19);
int fingerprint_size = sqlite3_column_bytes(statement, 4);
int mask_size = sqlite3_column_bytes(statement, 7);
const void *fingerprint = sqlite3_column_blob(statement, 4);
const void *mask = sqlite3_column_blob(statement, 7);
if (id <= 0 || parent_id <= 0 || kind != LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL ||
version <= 0 || version > UINT32_MAX ||
(status != LARDON3D_GEOMETRIC_REJECTED && status != LARDON3D_GEOMETRIC_VERIFIED) ||
inlier_count < 0 || inlier_count > 8192 || created_at < 0 ||
parent_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED || match_count <= 0 ||
match_count > 8192 || fingerprint_size != LARDON3D_PROJECT_DB_SHA256_SIZE ||
!fingerprint || mask_size <= 0 || mask_size > LARDON3D_PROJECT_DB_INLIER_MASK_MAX || !mask ||
!valid_geometric_mask(mask, (size_t)mask_size, (uint32_t)match_count,
(uint32_t)inlier_count)) {
return false;
}
double model[LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS] = {0};
bool has_model = status == LARDON3D_GEOMETRIC_VERIFIED;
for (int index = 0; index < LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS; ++index) {
int type = sqlite3_column_type(statement, 8 + index);
if ((!has_model && type != SQLITE_NULL) ||
(has_model && type != SQLITE_FLOAT && type != SQLITE_INTEGER)) {
return false;
}
if (has_model) {
model[index] = sqlite3_column_double(statement, 8 + index);
if (!isfinite(model[index])) {
return false;
}
}
}
memset(result, 0, sizeof(*result));
result->geometric_verification_result_id = (uint64_t)id;
result->match_result_id = (uint64_t)parent_id;
result->verifier_kind = (Lardon3DGeometricVerifierKind)kind;
result->verifier_version = (uint32_t)version;
memcpy(result->parameter_fingerprint, fingerprint, LARDON3D_PROJECT_DB_SHA256_SIZE);
result->status = (Lardon3DGeometricVerificationStatus)status;
result->inlier_count = (uint32_t)inlier_count;
result->inlier_mask_size = (size_t)mask_size;
memcpy(result->inlier_mask, mask, (size_t)mask_size);
result->has_model = has_model;
memcpy(result->model, model, sizeof(model));
result->created_at = created_at;
return true;
}
static Lardon3DProjectDbResult read_single_geometric_result(
Lardon3DProjectDb *database, sqlite3_stmt *statement,
Lardon3DProjectDbGeometricVerificationResult *result) {
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
(void)sqlite3_finalize(statement);
return LARDON3D_PROJECT_DB_NOT_FOUND;
}
if (code != SQLITE_ROW) {
Lardon3DProjectDbResult db_result = sqlite_result(database, code, "read geometric result");
(void)sqlite3_finalize(statement);
return db_result;
}
bool valid = read_geometric_result(statement, result);
code = sqlite3_step(statement);
(void)sqlite3_finalize(statement);
return valid && code == SQLITE_DONE ? LARDON3D_PROJECT_DB_OK : LARDON3D_PROJECT_DB_CORRUPT;
}
Lardon3DProjectDbResult lardon3d_project_db_create_geometric_verification_result(
Lardon3DProjectDb *database, uint64_t match_result_id,
Lardon3DGeometricVerifierKind verifier_kind, uint32_t verifier_version,
const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE],
Lardon3DGeometricVerificationStatus status, uint32_t inlier_count,
const unsigned char *inlier_mask, size_t inlier_mask_size, const double *model,
int64_t created_at, Lardon3DProjectDbGeometricVerificationResult *result) {
if (!database || !valid_catalog_id(match_result_id) ||
verifier_kind != LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL || verifier_version == 0 ||
!parameter_fingerprint || inlier_count > 8192 || !inlier_mask || inlier_mask_size == 0 ||
inlier_mask_size > LARDON3D_PROJECT_DB_INLIER_MASK_MAX ||
!valid_geometric_model(status, model) || created_at < 0 || !result) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(result, 0, sizeof(*result));
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult db_result =
execute(database, "BEGIN IMMEDIATE", "begin geometric result creation");
sqlite3_stmt *statement = NULL;
uint32_t match_count = 0;
if (db_result == LARDON3D_PROJECT_DB_OK) {
db_result = prepare(database,
"SELECT result_status,match_count FROM match_results "
"WHERE match_result_id=?1",
&statement);
}
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
db_result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || sqlite3_column_type(statement, 0) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 1) != SQLITE_INTEGER) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
sqlite3_int64 parent_status = sqlite3_column_int64(statement, 0);
sqlite3_int64 parent_count = sqlite3_column_int64(statement, 1);
if (parent_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED || parent_count <= 0 ||
parent_count > 8192) {
db_result = LARDON3D_PROJECT_DB_CONSTRAINT;
} else {
match_count = (uint32_t)parent_count;
}
}
(void)sqlite3_finalize(statement);
statement = NULL;
}
if (db_result == LARDON3D_PROJECT_DB_OK &&
!valid_geometric_mask(inlier_mask, inlier_mask_size, match_count, inlier_count)) {
db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
if (db_result == LARDON3D_PROJECT_DB_OK) {
db_result = prepare(
database,
"INSERT INTO geometric_verification_results("
"match_result_id,verifier_kind,verifier_version,parameter_fingerprint,status,"
"inlier_count,inlier_mask,model_m00,model_m01,model_m02,model_m10,model_m11,model_m12,"
"model_m20,model_m21,model_m22,created_at) "
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17)",
&statement);
}
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id);
(void)sqlite3_bind_int(statement, 2, (int)verifier_kind);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)verifier_version);
(void)sqlite3_bind_blob(statement, 4, parameter_fingerprint,
LARDON3D_PROJECT_DB_SHA256_SIZE, SQLITE_TRANSIENT);
(void)sqlite3_bind_int(statement, 5, (int)status);
(void)sqlite3_bind_int64(statement, 6, (sqlite3_int64)inlier_count);
(void)sqlite3_bind_blob(statement, 7, inlier_mask, (int)inlier_mask_size, SQLITE_TRANSIENT);
for (int index = 0; index < LARDON3D_PROJECT_DB_FUNDAMENTAL_COEFFICIENTS; ++index) {
if (model) {
(void)sqlite3_bind_double(statement, 8 + index, model[index]);
} else {
(void)sqlite3_bind_null(statement, 8 + index);
}
}
(void)sqlite3_bind_int64(statement, 17, created_at);
db_result = step_done(database, statement, "create geometric result");
statement = NULL;
}
uint64_t result_id = 0;
if (db_result == LARDON3D_PROJECT_DB_OK) {
sqlite3_int64 inserted_id = sqlite3_last_insert_rowid(database->connection);
if (inserted_id <= 0) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
result_id = (uint64_t)inserted_id;
db_result = execute(database, "COMMIT", "commit geometric result creation");
}
}
if (db_result != LARDON3D_PROJECT_DB_OK) {
(void)execute(database, "ROLLBACK", "rollback geometric result creation");
}
(void)pthread_mutex_unlock(&database->mutex);
if (db_result != LARDON3D_PROJECT_DB_OK) {
return db_result;
}
result->geometric_verification_result_id = result_id;
result->match_result_id = match_result_id;
result->verifier_kind = verifier_kind;
result->verifier_version = verifier_version;
memcpy(result->parameter_fingerprint, parameter_fingerprint,
LARDON3D_PROJECT_DB_SHA256_SIZE);
result->status = status;
result->inlier_count = inlier_count;
result->inlier_mask_size = inlier_mask_size;
memcpy(result->inlier_mask, inlier_mask, inlier_mask_size);
result->has_model = model != NULL;
if (model) {
memcpy(result->model, model, sizeof(result->model));
}
result->created_at = created_at;
return LARDON3D_PROJECT_DB_OK;
}
Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verification_result(
Lardon3DProjectDb *database, uint64_t geometric_verification_result_id,
Lardon3DProjectDbGeometricVerificationResult *result) {
if (!database || !valid_catalog_id(geometric_verification_result_id) || !result) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(result, 0, sizeof(*result));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
char sql[1024];
(void)snprintf(sql, sizeof(sql), "SELECT %sFROM geometric_verification_results g "
"JOIN match_results m ON m.match_result_id=g.match_result_id "
"WHERE g.geometric_verification_result_id=?1",
geometric_result_columns);
Lardon3DProjectDbResult db_result = prepare(database, sql, &statement);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)geometric_verification_result_id);
db_result = read_single_geometric_result(database, statement, result);
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
Lardon3DProjectDbResult lardon3d_project_db_find_geometric_verification_result(
Lardon3DProjectDb *database, uint64_t match_result_id,
Lardon3DGeometricVerifierKind verifier_kind, uint32_t verifier_version,
const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE],
Lardon3DProjectDbGeometricVerificationResult *result) {
if (!database || !valid_catalog_id(match_result_id) ||
verifier_kind != LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL || verifier_version == 0 ||
!parameter_fingerprint || !result) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(result, 0, sizeof(*result));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
char sql[1280];
(void)snprintf(sql, sizeof(sql), "SELECT %sFROM geometric_verification_results g "
"JOIN match_results m ON m.match_result_id=g.match_result_id "
"WHERE g.match_result_id=?1 AND g.verifier_kind=?2 AND "
"g.verifier_version=?3 AND g.parameter_fingerprint=?4",
geometric_result_columns);
Lardon3DProjectDbResult db_result = prepare(database, sql, &statement);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id);
(void)sqlite3_bind_int(statement, 2, (int)verifier_kind);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)verifier_version);
(void)sqlite3_bind_blob(statement, 4, parameter_fingerprint,
LARDON3D_PROJECT_DB_SHA256_SIZE, SQLITE_TRANSIENT);
db_result = read_single_geometric_result(database, statement, result);
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_geometric_verification_results(
Lardon3DProjectDb *database, uint64_t match_result_id,
uint64_t after_geometric_verification_result_id,
Lardon3DProjectDbGeometricVerificationResult *results, size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!database || !valid_catalog_id(match_result_id) ||
after_geometric_verification_result_id > INT64_MAX || !results || !count || capacity == 0 ||
capacity > LARDON3D_PROJECT_DB_GEOMETRIC_RESULT_PAGE_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
char sql[1280];
(void)snprintf(sql, sizeof(sql), "SELECT %sFROM geometric_verification_results g "
"JOIN match_results m ON m.match_result_id=g.match_result_id "
"WHERE g.match_result_id=?1 AND "
"g.geometric_verification_result_id>?2 "
"ORDER BY g.geometric_verification_result_id LIMIT ?3",
geometric_result_columns);
Lardon3DProjectDbResult db_result = prepare(database, sql, &statement);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id);
(void)sqlite3_bind_int64(statement, 2,
(sqlite3_int64)after_geometric_verification_result_id);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
if (!read_geometric_result(statement, &results[*count])) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
break;
}
++*count;
}
if (db_result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) {
db_result = sqlite_result(database, code, "list geometric results");
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
#ifdef LARDON3D_PROJECT_DB_TESTING
Lardon3DProjectDbResult lardon3d_project_db_test_delete_feature_identity(
Lardon3DProjectDb *database, uint64_t feature_set_id, uint64_t feature_asset_id) {

View file

@ -0,0 +1,463 @@
#include <math.h>
#include <sqlite3.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <lardon3d/project_db.h>
#define CHECK(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \
return false; \
} \
} while (0)
typedef struct {
uint64_t matched_id;
uint64_t no_match_id;
uint64_t second_matched_id;
uint64_t candidate_pair_id;
uint64_t feature_set_id_a;
uint64_t feature_set_id_b;
} Parents;
static bool execute_sql(const char *path, const char *sql) {
sqlite3 *connection = NULL;
if (sqlite3_open(path, &connection) != SQLITE_OK) {
return false;
}
bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
return sqlite3_close(connection) == SQLITE_OK && ok;
}
static bool query_integer(const char *path, const char *sql, sqlite3_int64 expected) {
sqlite3 *connection = NULL;
sqlite3_stmt *statement = NULL;
if (sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK ||
sqlite3_prepare_v2(connection, sql, -1, &statement, NULL) != SQLITE_OK) {
if (statement) (void)sqlite3_finalize(statement);
if (connection) (void)sqlite3_close(connection);
return false;
}
bool matches = sqlite3_step(statement) == SQLITE_ROW &&
sqlite3_column_int64(statement, 0) == expected &&
sqlite3_step(statement) == SQLITE_DONE;
return sqlite3_finalize(statement) == SQLITE_OK && sqlite3_close(connection) == SQLITE_OK &&
matches;
}
static void asset_path(const unsigned char hash[32], const char *kind,
char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]) {
static const char digits[] = "0123456789abcdef";
char hex[65];
for (size_t index = 0; index < 32; ++index) {
hex[index * 2] = digits[hash[index] >> 4];
hex[index * 2 + 1] = digits[hash[index] & 15U];
}
hex[64] = '\0';
(void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, "assets/%s/%c%c/%s", kind, hex[0],
hex[1], hex);
}
static bool create_match_result(Lardon3DProjectDb *database, uint64_t pair_id, uint64_t fs_a,
uint64_t fs_b, uint32_t version, unsigned char fingerprint_byte,
int status, uint32_t match_count, uint64_t *id) {
unsigned char fingerprint[32] = {0};
unsigned char hash[32] = {0};
char path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
memset(fingerprint, fingerprint_byte, sizeof(fingerprint));
memset(hash, (unsigned char)(fingerprint_byte + 64U), sizeof(hash));
asset_path(hash, "matches", path);
Lardon3DProjectDbMatchResult result;
Lardon3DProjectDbResult db_result = lardon3d_project_db_create_match_result(
database, pair_id, fs_a, fs_b, "model-test", version, fingerprint, status, match_count,
status == LARDON3D_MATCH_RESULT_STATUS_MATCHED ? hash : NULL,
status == LARDON3D_MATCH_RESULT_STATUS_MATCHED ? path : NULL,
status == LARDON3D_MATCH_RESULT_STATUS_MATCHED ? 32U + (uint64_t)match_count * 12U : 0,
version, &result);
if (db_result != LARDON3D_PROJECT_DB_OK) {
return false;
}
*id = result.match_result_id;
return true;
}
static bool create_parents(Lardon3DProjectDb *database, Parents *parents) {
Lardon3DProjectDbScanSet scanset;
if (lardon3d_project_db_create_scanset(database, "Geometry-model", &scanset) !=
LARDON3D_PROJECT_DB_OK) {
return false;
}
unsigned char image_hash_a[32] = {0x11};
unsigned char image_hash_b[32] = {0x22};
char image_path_a[LARDON3D_PROJECT_DB_PATH_CAPACITY];
char image_path_b[LARDON3D_PROJECT_DB_PATH_CAPACITY];
asset_path(image_hash_a, "images", image_path_a);
asset_path(image_hash_b, "images", image_path_b);
Lardon3DProjectDbImageRegisterStatus register_status;
Lardon3DProjectDbImage image_a;
Lardon3DProjectDbImage image_b;
if (lardon3d_project_db_register_image(database, scanset.scanset_id, image_hash_a, image_path_a,
10, "a.jpg", "/a.jpg", 0, 1, &register_status,
&image_a) != LARDON3D_PROJECT_DB_OK ||
lardon3d_project_db_register_image(database, scanset.scanset_id, image_hash_b, image_path_b,
10, "b.jpg", "/b.jpg", 0, 2, &register_status,
&image_b) != LARDON3D_PROJECT_DB_OK) {
return false;
}
Lardon3DProjectDbCandidatePair pair;
if (lardon3d_project_db_create_candidate_pair(database, image_a.image_id, image_b.image_id, 3,
&pair) != LARDON3D_PROJECT_DB_OK) {
return false;
}
unsigned char feature_hash_a[32] = {0x33};
unsigned char feature_hash_b[32] = {0x44};
char feature_path_a[LARDON3D_PROJECT_DB_PATH_CAPACITY];
char feature_path_b[LARDON3D_PROJECT_DB_PATH_CAPACITY];
asset_path(feature_hash_a, "features", feature_path_a);
asset_path(feature_hash_b, "features", feature_path_b);
Lardon3DProjectDbFeatureSet feature_a;
Lardon3DProjectDbFeatureSet feature_b;
if (lardon3d_project_db_register_feature_set(
database, image_a.image_id, "orb", 1, feature_hash_a, image_hash_a, 8192, 1, 32,
feature_hash_a, feature_path_a, 100, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 4,
&feature_a) != LARDON3D_PROJECT_DB_OK ||
lardon3d_project_db_register_feature_set(
database, image_b.image_id, "orb", 1, feature_hash_b, image_hash_b, 8192, 1, 32,
feature_hash_b, feature_path_b, 100, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 5,
&feature_b) != LARDON3D_PROJECT_DB_OK) {
return false;
}
parents->candidate_pair_id = pair.candidate_pair_id;
parents->feature_set_id_a = feature_a.feature_set_id;
parents->feature_set_id_b = feature_b.feature_set_id;
return create_match_result(database, pair.candidate_pair_id, feature_a.feature_set_id,
feature_b.feature_set_id, 1, 0x51,
LARDON3D_MATCH_RESULT_STATUS_MATCHED, 9, &parents->matched_id) &&
create_match_result(database, pair.candidate_pair_id, feature_a.feature_set_id,
feature_b.feature_set_id, 2, 0x52,
LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, &parents->no_match_id) &&
create_match_result(database, pair.candidate_pair_id, feature_a.feature_set_id,
feature_b.feature_set_id, 3, 0x53,
LARDON3D_MATCH_RESULT_STATUS_MATCHED, 8192,
&parents->second_matched_id);
}
static bool create_v11_database(const char *path) {
Lardon3DProjectDb *database = NULL;
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
if (lardon3d_project_db_open(path, &database, error) != LARDON3D_PROJECT_DB_OK) {
return false;
}
lardon3d_project_db_close(database);
return execute_sql(path,
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE geometric_verification_results;"
"UPDATE metadata SET value=11 WHERE key='schema_version';"
"COMMIT;PRAGMA foreign_keys=ON;");
}
static bool test_model_api(const char *path) {
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 12);
Parents parents;
CHECK(create_parents(database, &parents));
unsigned char fingerprint_a[32];
unsigned char fingerprint_b[32];
memset(fingerprint_a, 0xa1, sizeof(fingerprint_a));
memset(fingerprint_b, 0xb2, sizeof(fingerprint_b));
unsigned char mask_a[2] = {0x01, 0x00};
double model[9] = {0.0, -1.0, 1e-300, 1e300, 4.0, 5.0, 6.0, 7.0, 8.0};
Lardon3DProjectDbGeometricVerificationResult verified;
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1,
fingerprint_a, LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), model, 10,
&verified) == LARDON3D_PROJECT_DB_OK);
CHECK(verified.has_model && verified.inlier_mask_size == 2 &&
memcmp(verified.model, model, sizeof(model)) == 0);
unsigned char rejected_mask[2] = {0x03, 0x00};
Lardon3DProjectDbGeometricVerificationResult rejected;
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 2,
fingerprint_b, LARDON3D_GEOMETRIC_REJECTED, 2, rejected_mask,
sizeof(rejected_mask), NULL, 11, &rejected) == LARDON3D_PROJECT_DB_OK);
CHECK(!rejected.has_model && rejected.inlier_count == 2);
unsigned char zero_mask[2] = {0};
Lardon3DProjectDbGeometricVerificationResult zero_inliers;
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3,
fingerprint_b, LARDON3D_GEOMETRIC_REJECTED, 0, zero_mask, sizeof(zero_mask), NULL, 11,
&zero_inliers) == LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbGeometricVerificationResult loaded;
CHECK(lardon3d_project_db_load_geometric_verification_result(
database, verified.geometric_verification_result_id, &loaded) ==
LARDON3D_PROJECT_DB_OK);
CHECK(loaded.match_result_id == parents.matched_id && loaded.has_model &&
memcmp(loaded.model, model, sizeof(model)) == 0 &&
memcmp(loaded.inlier_mask, mask_a, sizeof(mask_a)) == 0);
CHECK(lardon3d_project_db_find_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1,
fingerprint_a, &loaded) == LARDON3D_PROJECT_DB_OK &&
loaded.geometric_verification_result_id == verified.geometric_verification_result_id);
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1,
fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 2, rejected_mask,
sizeof(rejected_mask), NULL, 12, &loaded) == LARDON3D_PROJECT_DB_CONSTRAINT);
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.no_match_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1,
fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 0, mask_a, 1, NULL, 12,
&loaded) == LARDON3D_PROJECT_DB_CONSTRAINT);
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, UINT64_C(9223372036854775807),
LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1, fingerprint_a,
LARDON3D_GEOMETRIC_REJECTED, 0, mask_a, 1, NULL, 12,
&loaded) == LARDON3D_PROJECT_DB_NOT_FOUND);
unsigned char bad_padding[2] = {0x01, 0x80};
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3,
fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 1, bad_padding, sizeof(bad_padding),
NULL, 12, &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3,
fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 2, mask_a, sizeof(mask_a), NULL, 12,
&loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
unsigned char all_inliers[2] = {0xff, 0x01};
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 4,
fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 9, all_inliers, sizeof(all_inliers), NULL,
12, &loaded) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 5,
fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 10, all_inliers, sizeof(all_inliers), NULL,
12, &loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3,
fingerprint_a, LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), NULL, 12,
&loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3,
fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 1, mask_a, sizeof(mask_a), model, 12,
&loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
double bad_model[9] = {0};
bad_model[4] = NAN;
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3,
fingerprint_a, LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), bad_model, 12,
&loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
bad_model[4] = INFINITY;
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 3,
fingerprint_a, LARDON3D_GEOMETRIC_VERIFIED, 1, mask_a, sizeof(mask_a), bad_model, 12,
&loaded) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
unsigned char max_mask[LARDON3D_PROJECT_DB_INLIER_MASK_MAX] = {0};
max_mask[0] = 0x01;
max_mask[1023] = 0x80;
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.second_matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1,
fingerprint_a, LARDON3D_GEOMETRIC_REJECTED, 2, max_mask, sizeof(max_mask), NULL, 13,
&loaded) == LARDON3D_PROJECT_DB_OK);
CHECK(loaded.inlier_mask_size == 1024 && loaded.inlier_count == 2);
static const uint32_t boundary_counts[] = {1, 2, 7, 8, 9, 63, 64, 65, 8191};
for (size_t boundary_index = 0;
boundary_index < sizeof(boundary_counts) / sizeof(boundary_counts[0]); ++boundary_index) {
uint32_t match_count = boundary_counts[boundary_index];
uint64_t parent_id = 0;
CHECK(create_match_result(database, parents.candidate_pair_id, parents.feature_set_id_a,
parents.feature_set_id_b, (uint32_t)(10U + boundary_index),
(unsigned char)(0x60U + boundary_index),
LARDON3D_MATCH_RESULT_STATUS_MATCHED, match_count, &parent_id));
unsigned char boundary_mask[LARDON3D_PROJECT_DB_INLIER_MASK_MAX] = {0};
size_t boundary_size = ((size_t)match_count + 7U) / 8U;
uint32_t last_index = match_count - 1U;
boundary_mask[last_index / 8U] = (unsigned char)(1U << (last_index % 8U));
unsigned char boundary_fingerprint[32];
memset(boundary_fingerprint, (int)(0xc0U + boundary_index),
sizeof(boundary_fingerprint));
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parent_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1,
boundary_fingerprint, LARDON3D_GEOMETRIC_REJECTED, 1, boundary_mask,
boundary_size, NULL, 20 + (int64_t)boundary_index, &loaded) ==
LARDON3D_PROJECT_DB_OK);
CHECK(loaded.inlier_mask_size == boundary_size &&
loaded.inlier_mask[last_index / 8U] == (unsigned char)(1U << (last_index % 8U)));
}
Lardon3DProjectDbGeometricVerificationResult page[2];
size_t count = 0;
CHECK(lardon3d_project_db_list_geometric_verification_results(
database, parents.matched_id, 0, page, 1, &count) == LARDON3D_PROJECT_DB_OK &&
count == 1);
uint64_t cursor = page[0].geometric_verification_result_id;
CHECK(lardon3d_project_db_list_geometric_verification_results(
database, parents.matched_id, cursor, page, 2, &count) == LARDON3D_PROJECT_DB_OK &&
count == 2 && page[0].geometric_verification_result_id > cursor);
CHECK(lardon3d_project_db_list_geometric_verification_results(
database, parents.matched_id, 0, page,
LARDON3D_PROJECT_DB_GEOMETRIC_RESULT_PAGE_MAX + 1U, &count) ==
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
uint64_t verified_id = verified.geometric_verification_result_id;
lardon3d_project_db_close(database);
database = NULL;
CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_geometric_verification_result(database, verified_id, &loaded) ==
LARDON3D_PROJECT_DB_OK && memcmp(loaded.model, model, sizeof(model)) == 0);
lardon3d_project_db_close(database);
return true;
}
static bool loader_rejects_after_sql(const char *path, const char *sql) {
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDb *database = NULL;
if (!execute_sql(path, sql) ||
lardon3d_project_db_open(path, &database, error) != LARDON3D_PROJECT_DB_OK) {
return false;
}
Lardon3DProjectDbGeometricVerificationResult result;
bool rejected = lardon3d_project_db_load_geometric_verification_result(database, 1, &result) ==
LARDON3D_PROJECT_DB_CORRUPT;
lardon3d_project_db_close(database);
return rejected;
}
static bool test_corruption(const char *path) {
static const char reset[] =
"PRAGMA ignore_check_constraints=ON;"
"UPDATE geometric_verification_results SET verifier_kind=1,verifier_version=1,"
"parameter_fingerprint=zeroblob(32),status=2,inlier_count=1,inlier_mask=x'0100',"
"model_m00=0,model_m01=0,model_m02=0,model_m10=0,model_m11=0,model_m12=0,"
"model_m20=0,model_m21=0,model_m22=0 WHERE geometric_verification_result_id=1;";
CHECK(execute_sql(path, reset));
static const char *corruptions[] = {
"PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results "
"SET verifier_kind=9 WHERE geometric_verification_result_id=1;",
"PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results "
"SET verifier_kind=1,verifier_version=0 WHERE geometric_verification_result_id=1;",
"PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results "
"SET verifier_version=1,parameter_fingerprint=x'01' "
"WHERE geometric_verification_result_id=1;",
"PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results "
"SET parameter_fingerprint=zeroblob(32),status=9 "
"WHERE geometric_verification_result_id=1;",
"PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results "
"SET status=2,inlier_count=9000 WHERE geometric_verification_result_id=1;",
"PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results "
"SET inlier_count=1,inlier_mask=x'03' WHERE geometric_verification_result_id=1;",
"PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results "
"SET inlier_mask=x'0180' WHERE geometric_verification_result_id=1;",
"PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results "
"SET inlier_mask=x'0300' WHERE geometric_verification_result_id=1;",
"PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results "
"SET inlier_mask=x'0100',model_m00=NULL WHERE geometric_verification_result_id=1;",
"PRAGMA ignore_check_constraints=ON;UPDATE geometric_verification_results "
"SET model_m00=1e999 WHERE geometric_verification_result_id=1;",
};
for (size_t index = 0; index < sizeof(corruptions) / sizeof(corruptions[0]); ++index) {
CHECK(execute_sql(path, reset));
CHECK(loader_rejects_after_sql(path, corruptions[index]));
}
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDb *database = NULL;
CHECK(execute_sql(path, reset));
CHECK(lardon3d_project_db_open(path, &database, error) == LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbGeometricVerificationResult result;
CHECK(lardon3d_project_db_load_geometric_verification_result(database, 1, &result) ==
LARDON3D_PROJECT_DB_OK);
uint64_t parent_id = result.match_result_id;
lardon3d_project_db_close(database);
char parent_corruption[512];
CHECK(snprintf(parent_corruption, sizeof(parent_corruption),
"PRAGMA ignore_check_constraints=ON;UPDATE match_results SET result_status=0 "
"WHERE match_result_id=%llu;",
(unsigned long long)parent_id) > 0);
CHECK(loader_rejects_after_sql(path, parent_corruption));
return true;
}
static bool test_migration(const char *v11_path, const char *failed_path) {
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDb *database = NULL;
CHECK(create_v11_database(v11_path));
CHECK(query_integer(v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 11));
CHECK(query_integer(v11_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='geometric_verification_results'",
0));
CHECK(lardon3d_project_db_open(v11_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 12);
Parents parents;
CHECK(create_parents(database, &parents));
unsigned char fingerprint[32] = {0x91};
unsigned char mask[2] = {0x01, 0x00};
Lardon3DProjectDbGeometricVerificationResult migrated_result;
CHECK(lardon3d_project_db_create_geometric_verification_result(
database, parents.matched_id, LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, 1,
fingerprint, LARDON3D_GEOMETRIC_REJECTED, 1, mask, sizeof(mask), NULL, 30,
&migrated_result) == LARDON3D_PROJECT_DB_OK);
uint64_t migrated_result_id = migrated_result.geometric_verification_result_id;
lardon3d_project_db_close(database);
CHECK(query_integer(v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 12));
CHECK(query_integer(v11_path,
"SELECT count(*) FROM sqlite_master WHERE type='index' AND "
"name='geometric_verification_results_parent_idx'",
1));
CHECK(lardon3d_project_db_open(v11_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_geometric_verification_result(
database, migrated_result_id, &migrated_result) == LARDON3D_PROJECT_DB_OK &&
migrated_result.match_result_id == parents.matched_id);
lardon3d_project_db_close(database);
CHECK(create_v11_database(failed_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V12", "1", 1) == 0);
CHECK(lardon3d_project_db_open(failed_path, &database, error) == LARDON3D_PROJECT_DB_IO_ERROR);
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V12") == 0);
CHECK(query_integer(failed_path, "SELECT value FROM metadata WHERE key='schema_version'", 11));
CHECK(query_integer(failed_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='geometric_verification_results'",
0));
CHECK(lardon3d_project_db_open(failed_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 12);
lardon3d_project_db_close(database);
return true;
}
static bool run_test(void) {
char directory[] = "/tmp/lardon3d-geometric-model-XXXXXX";
CHECK(mkdtemp(directory));
char database_path[512];
char v11_path[512];
char failed_path[512];
CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0);
CHECK(snprintf(v11_path, sizeof(v11_path), "%s/v11.db", directory) > 0);
CHECK(snprintf(failed_path, sizeof(failed_path), "%s/failed-v12.db", directory) > 0);
CHECK(test_model_api(database_path));
CHECK(test_corruption(database_path));
CHECK(test_migration(v11_path, failed_path));
CHECK(unlink(database_path) == 0);
CHECK(unlink(v11_path) == 0);
CHECK(unlink(failed_path) == 0);
CHECK(rmdir(directory) == 0);
return true;
}
int main(void) {
return run_test() ? EXIT_SUCCESS : EXIT_FAILURE;
}

View file

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

View file

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

View file

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