feat(reconstruction): add precision features, candidate pairs and matcher pipeline

This commit is contained in:
fy59 2026-08-09 09:59:07 +02:00
parent e08e609fc4
commit 02bf2c6d07
33 changed files with 4003 additions and 27 deletions

View file

@ -0,0 +1,27 @@
---
name: lardon-architect
description: Décisions architecturales persistantes et difficiles de Lardon3D.
model: gemini-3.6-flash
---
Tu interviens uniquement lorsqu'une vraie décision architecturale persistante
est ambiguë.
Étudie:
- invariants;
- compatibilité;
- persistance/migrations;
- fingerprints;
- invalidation;
- reprise;
- coût mémoire/CPU;
- évolution future.
Préfère la solution minimale compatible avec l'architecture existante.
Retour:
- décision;
- alternatives rejetées;
- conséquences;
- invariants à documenter.
Ne code pas.

View file

@ -0,0 +1,20 @@
---
name: lardon-build
description: Implémente une tranche Lardon3D déjà cadrée.
model: gemini-3.6-flash
---
Tu es l'agent d'implémentation Lardon3D.
Tu reçois un contrat, une tranche ou un diagnostic déjà cadré.
Responsabilités:
- implémentation minimale et cohérente;
- respecter les invariants existants;
- C17/C++ selon les fichiers existants;
- lisibilité cible ~100 colonnes, maximum 120;
- pas de refactor hors périmètre;
- documentation mise à jour lorsque le contrat ou comportement change;
- uniquement les tests ciblés nécessaires pendant l'implémentation.
Tu ne fais pas la validation indépendante finale.
Tu ne commit, push ou stage jamais.

View file

@ -0,0 +1,20 @@
---
name: lardon-concurrency
description: Analyse spécialisée des problèmes réels de concurrence Lardon3D.
model: gemini-3.6-flash
---
Tu interviens uniquement lorsqu'une vraie question de concurrence existe.
Analyse:
- ownership;
- mutex/atomics;
- transactions;
- worker races;
- publication atomique;
- pause/cancel/recovery;
- deadlocks;
- duplicate creation;
- visibility entre threads.
Ne fais pas d'audit général.
Ne modifie rien sauf demande explicite de l'orchestrateur après diagnostic.

View file

@ -0,0 +1,24 @@
---
name: lardon-diagnose
description: Diagnostic borné des échecs dont la cause exacte est inconnue.
model: gemini-3.6-flash
---
Tu diagnostiques un problème précis dans Lardon3D.
Tu ne corriges rien.
Exigence principale:
établir une chaîne causale concrète à partir du symptôme observé.
Évite les hypothèses générales.
Inspecte uniquement le chemin nécessaire.
Retour obligatoire:
- symptôme confirmé;
- cause CONFIRMÉE / PROBABLE / NON ÉTABLIE;
- chaîne causale;
- fichiers et lignes concernés;
- correction minimale recommandée;
- routage suivant.
Arrête-toi dès qu'une cause concrète suffisante est établie.

View file

@ -0,0 +1,18 @@
---
name: lardon-docs
description: Maintient la documentation canonique de Lardon3D.
model: gemini-3.6-flash
---
Tu es responsable de la documentation canonique de Lardon3D.
La documentation est une partie du produit, pas une étape cosmétique finale.
Tu dois:
- préserver la terminologie existante;
- documenter les contrats, invariants, persistance, fingerprints,
invalidation, recovery, bornes et concurrence lorsqu'ils changent;
- supprimer les promesses devenues obsolètes;
- vérifier que la documentation décrit le comportement réellement implémenté;
- éviter toute documentation artificielle pour un microfix sans changement de comportement.
Une décision importante ne doit pas exister uniquement dans un rapport d'agent.

View file

@ -0,0 +1,33 @@
---
name: lardon-orchestrator
description: Pilote les tickets Lardon3D et délègue le travail aux spécialistes.
model: gemini-3.6-flash
---
Tu es l'orchestrateur principal de Lardon3D.
Ta responsabilité est de piloter, pas de tout faire toi-même.
Règles:
- comprendre le ticket et l'état réel du dépôt avant d'agir;
- considérer la documentation canonique comme la mémoire durable du projet;
- maintenir .goose/work/current_ticket.md et handoff.md lorsqu'un run devient long;
- déléguer aux agents spécialisés quand leur rôle correspond au travail;
- ne pas refaire toi-même un travail déjà délégué;
- ne pas lancer deux agents d'écriture sur les mêmes fichiers en parallèle;
- éviter les appels d'agents redondants;
- utiliser des tests ciblés pendant le développement et une validation globale seulement à la fin;
- continuer automatiquement tant qu'une action sûre et utile existe.
Tu peux faire directement uniquement une correction locale, mécanique, évidente et petite.
Une modification substantielle doit aller à lardon-build.
Ordre normal:
documentation/contrat
→ implémentation
→ tests ciblés
→ documentation alignée
→ tranche suivante
→ validation finale
→ review finale.
Ne commit, ne push et ne stage jamais.

View file

@ -0,0 +1,20 @@
---
name: lardon-read
description: Audit ciblé en lecture seule du code et de la documentation Lardon3D.
model: gemini-3.6-flash
---
Tu es l'agent d'audit ciblé de Lardon3D.
LECTURE SEULE.
Ta mission:
- lire uniquement les fichiers nécessaires à la question;
- retrouver les contrats et invariants existants;
- identifier précisément les dépendances utiles;
- signaler les divergences documentation/code;
- rendre un résumé compact avec fichiers et preuves.
Ne modifie aucun fichier.
Ne compile pas sauf si explicitement demandé.
Ne propose pas de refactor opportuniste.
Arrête l'audit lorsque les informations nécessaires sont établies.

View file

@ -0,0 +1,29 @@
---
name: lardon-review
description: Review finale indépendante des tickets Lardon3D.
model: gemini-3.6-flash
---
Tu es le reviewer final indépendant.
Ne modifie rien.
Review prioritaire:
- correctness;
- persistance et migrations;
- recovery;
- identité/fingerprints;
- invalidation;
- bornes;
- concurrence;
- déterminisme;
- tests;
- cohérence documentation/code;
- complexité inutile.
Classe les findings par sévérité.
Ne transforme pas la review en nouvel audit général.
Verdict final:
APPROVED
ou
CHANGES_REQUIRED avec findings précis.

View file

@ -0,0 +1,32 @@
---
name: lardon-tests
description: Validation indépendante des changements Lardon3D sans correction.
model: gemini-3.6-flash
---
Tu es le validateur indépendant de Lardon3D.
VALIDATION UNIQUEMENT.
Tu peux:
- compiler;
- lancer les tests demandés;
- lancer sanitizers quand demandé;
- exécuter git diff --check;
- localiser exactement une assertion, cible ou commande en échec.
Tu ne dois PAS:
- modifier le code;
- corriger les tests;
- diagnostiquer largement;
- implémenter une solution.
En cas d'échec, rends:
- commande;
- test/cible;
- résultat attendu;
- résultat observé;
- localisation;
- reproductibilité;
- routage recommandé.
Verdict: PASS ou FAIL.

View file

@ -0,0 +1,25 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "developer__shell|developer__text_editor",
"hooks": [
{
"type": "command",
"command": "python3 ${PLUGIN_ROOT}/scripts/policy.py"
}
]
}
],
"BeforeReadFile": [
{
"hooks": [
{
"type": "command",
"command": "python3 ${PLUGIN_ROOT}/scripts/policy.py"
}
]
}
]
}
}

View file

@ -0,0 +1,5 @@
{
"name": "lardon-safety",
"version": "1.0.0",
"description": "Hard safety policy for Goose on Lardon3D"
}

View file

@ -0,0 +1,62 @@
#!/usr/bin/env python3
import json
import re
import sys
try:
payload = json.load(sys.stdin)
except Exception:
sys.exit(0)
event = str(payload.get("event", ""))
tool = str(payload.get("tool_name", ""))
tool_input = payload.get("tool_input") or {}
blob = json.dumps(tool_input, ensure_ascii=False)
def deny(reason):
print(json.dumps({
"decision": "block",
"reason": reason,
}))
sys.exit(0)
# Explicitly forbidden areas.
if re.search(r'(^|[/\s"\'"])scan3d(?:/|["\'\s]|$)', blob):
deny("scan3d/** is outside the Lardon3D agent scope")
if re.search(r'(^|[/\s"\'"])\.git(?:/|["\'\s]|$)', blob):
deny(".git/** must not be accessed directly")
if event == "PreToolUse" and "shell" in tool:
command = str(tool_input.get("command", ""))
forbidden = [
r'(^|[;&|]\s*|\s)sudo(\s|$)',
r'(^|[;&|]\s*|\s)doas(\s|$)',
r'(^|[;&|]\s*|\s)(pacman|yay|paru)(\s|$)',
r'\bgit\s+add\b',
r'\bgit\s+commit\b',
r'\bgit\s+push\b',
r'\bgit\s+reset\b',
r'\bgit\s+clean\b',
r'\bgit\s+checkout\b',
r'\bgit\s+restore\b',
r'\bgit\s+rebase\b',
r'\bgit\s+merge\b',
r'\bgit\s+switch\b',
r'(^|[;&|]\s*|\s)rm(\s|$)',
r'(^|[;&|]\s*|\s)chmod(\s|$)',
r'(^|[;&|]\s*|\s)chown(\s|$)',
r'(^|[;&|]\s*|\s)(kill|pkill|killall)(\s|$)',
r'(^|[;&|]\s*|\s)(poweroff|reboot|shutdown)(\s|$)',
]
for pattern in forbidden:
if re.search(pattern, command):
deny(f"blocked by Lardon3D safety policy: {command}")
sys.exit(0)

53
.goosehints Normal file
View file

@ -0,0 +1,53 @@
# Lardon3D — règles Goose
Lardon3D est un moteur persistant et incrémental de reconstruction 3D.
## Principe prioritaire
La documentation canonique est la mémoire durable du projet et est plus
importante que les rapports temporaires d'agents.
Pour toute tranche substantielle:
audit docs
→ contrat/invariants
→ documentation
→ implémentation
→ tests ciblés
→ vérification doc/code
→ tranche suivante
Une modification de comportement, architecture, persistance, migration,
fingerprint, invalidation, recovery, concurrence ou bornes sans documentation
alignée rend la tranche PARTIAL.
## Discipline
- Lire AGENTS.md avant tout travail substantiel.
- Ne jamais commit, push ou stage.
- Ne jamais utiliser git add -A.
- Ne jamais annuler ou écraser des modifications existantes de l'utilisateur.
- scan3d/** est hors périmètre.
- .git/** est hors périmètre.
- Pas de sudo ni gestionnaire de paquets.
- Pas de commandes destructives.
- Pas de refactor opportuniste.
- Code lisible: cible ~100 colonnes, maximum 120.
- Tests ciblés pendant le développement.
- Suite complète uniquement au gate final sauf nécessité démontrée.
## Multi-agent
Utiliser les rôles:
- lardon-read: audit ciblé
- lardon-diagnose: cause inconnue
- lardon-build: implémentation
- lardon-tests: validation indépendante
- lardon-docs: documentation
- lardon-architect: décision architecturale réelle
- lardon-concurrency: vraie question de concurrence
- lardon-review: review finale unique
Ne pas déléguer par réflexe.
Ne pas faire refaire par le parent le travail d'un subagent.
Ne pas lancer plusieurs écrivains concurrents sur les mêmes fichiers.

View file

@ -107,6 +107,9 @@ Acquisitions
- [Feature Store](docs/architecture/feature_store.md) - [Feature Store](docs/architecture/feature_store.md)
- [Precision Feature Pipeline v1A](docs/architecture/precision_feature_pipeline.md) - [Precision Feature Pipeline v1A](docs/architecture/precision_feature_pipeline.md)
- [Visual Index](docs/architecture/visual_index.md) - [Visual Index](docs/architecture/visual_index.md)
- [Candidate Pair](docs/architecture/candidate_pair.md)
- [Match Result](docs/architecture/match_result.md)
- [Matcher](docs/architecture/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)
@ -145,9 +148,9 @@ Pour les changements sensibles à la mémoire ou à la concurrence, ajouter ASan
## Statut ## Statut
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 et le Visual Index ORB sont implémentés. Candidate le Feature Store multipasse, le Visual Index ORB, Candidate Pair Generator
Pair Generator, matching, DAG générique, SfM et viewer restent des tickets et Matcher v1 sont implémentés. DAG générique, vérification géométrique,
séparés planifiés. SfM et viewer restent des tickets séparés planifiés.
## Licence ## Licence

View file

@ -115,7 +115,10 @@ l'extraction après import et planification multi-image/DAG.
**IMPLEMENTED** — Visual Index v1 consomme ce reader sans changer le format. **IMPLEMENTED** — Visual Index v1 consomme ce reader sans changer le format.
**PLANNED** — paires candidates, matching, vérification géométrique, tracks et SfM. **IMPLEMENTED** — paires candidates, Matcher v1 et Match Store v1 consomment
les Feature Sets persistés.
**PLANNED** — vérification géométrique, tracks et SfM.
## Extension v2 multi-descriptor ## Extension v2 multi-descriptor

View file

@ -0,0 +1,64 @@
# Match Result Model v1
## Rôle et frontière
Un Match Result décrit un calcul descriptor-level réussi entre deux Feature
Sets d'une Candidate Pair. Les correspondances sont dans le Match File; aucune
vérification géométrique, aucun inlier géométrique et aucun masque d'inliers ne
font partie de ce modèle.
Les erreurs d'exécution appartiennent au Task Runtime et aux valeurs de retour.
Elles ne créent pas de Match Result. Le modèle persistant possède deux états :
- `NO_MATCH` (0) : `match_count == 0`, aucun asset;
- `MATCHED` (1) : `match_count > 0`, asset obligatoire.
`match_count` est exactement le nombre de correspondances descriptor-level
persistées après Lowe ratio et canonicalisation. Il n'existe ni `raw_match_count`
ambigu ni `inlier_count` dans Match Result v1.
## Identité et ownership
L'identité immuable est :
```
(candidate_pair_id, feature_set_id_a, feature_set_id_b,
matcher_kind, matcher_version, parameter_fingerprint)
```
Elle porte une contrainte `UNIQUE`. `feature_set_id_a` appartient à
`candidate_pair.image_id_a` et `feature_set_id_b` à `image_id_b`. Les Feature
Set IDs ne sont jamais triés numériquement.
## Schéma Project DB v10
`match_results` impose en SQL : statut dans `(0,1)`, `0 <= match_count <= 8192`,
SHA de 32 octets, chemins non vides et tailles strictement positives lorsqu'ils
sont présents. Un `CHECK` conjoint impose :
```
NO_MATCH => match_count = 0 et SHA/path/size NULL
MATCHED => match_count > 0 et SHA/path/size tous présents
```
La création vérifie aussi atomiquement l'ownership A/B avant l'INSERT. Les API
load/find/list conservent l'ordre stable par `match_result_id`; close/reopen ne
change aucune identité ni métadonnée.
## Reuse
`NO_MATCH` est directement réutilisable sans asset. `MATCHED` ne l'est qu'après
validation du chemin, de la taille, du SHA-256 réel, du format complet, du type
et de la dimension des descripteurs, des Feature Set IDs A/B et du compte.
Une ligne existante dont l'asset est absent ou corrompu déclenche un recalcul.
Le fichier est republié atomiquement et les champs résultat/asset de cette même
ligne sont réparés transactionnellement; son ID et son identité scientifique à
six parties restent inchangés. Aucune identité artificielle ne contourne la
contrainte `UNIQUE`.
## Suite
Le prochain modèle possède ses propres statistiques géométriques, dont un
éventuel `inlier_count`.
NEXT: GEOMETRIC VERIFICATION MODEL

View file

@ -0,0 +1,96 @@
# Matcher v1 et Match Store v1
## Contrat
Le pipeline v1 est `ORB → BFMatcher Hamming` ou `SIFT/RootSIFT → BFMatcher L2`,
puis KNN `k=2`, Lowe ratio, tri canonique, Match File content-addressed et Match
Result. Il s'arrête avant toute vérification géométrique.
Pour chaque descripteur A, OpenCV fournit zéro, un ou deux voisins. Zéro ne
produit rien. Un seul voisin valide est accepté. Avec deux voisins, le premier
est accepté exactement si `d1 < threshold * d2`; une seconde distance nulle,
une égalité, NaN, Inf ou une distance négative est rejetée. `-0.0` est accepté
comme zéro conformément à IEEE-754. Indices invalides et
distances du premier voisin non finies ou négatives sont rejetés.
Un `feature_index_a` produit donc au maximum une correspondance acceptée.
Sans cross-check, plusieurs indices A peuvent viser le même index B. L'ordre
final est `(feature_index_a, distance, feature_index_b)` croissant. BFMatcher
fournit une liste KNN par query et le filtre ne conserve que son premier voisin :
la déduplication est donc mathématiquement inutile. Le Matcher rejette toute
sortie anormale contenant deux fois le même index A.
## Match File v1
Header fixe de 32 octets, tous les entiers en little-endian :
```
0..3 octets ASCII exacts "L3DM"
4 format_version = 1
5 descriptor_type (1=U8, 2=F32)
6..7 reserved = 0
8..11 match_count uint32
12..15 descriptor_dimension uint32 (32 ou 128)
16..23 feature_set_id_a uint64
24..31 feature_set_id_b uint64
```
Chaque entrée fait 12 octets : index A uint32, index B uint32, distance float32.
Le format accepte zéro entrée pour tester reader/writer, mais le Matcher ne
publie pas d'asset vide. La borne est 8192 entrées et la taille maximale exacte
est `32 + 8192 * 12 = 98336` octets, environ 96 Kio.
Le reader valide avant toute allocation : magic physique, version, reserved,
type/dimension, borne du compte, calcul de taille, taille physique exacte,
Feature Set IDs A/B sans permutation, indices contre les comptes attendus et
distance finie non négative. Fichiers tronqués, trailing bytes et versions
futures sont rejetés.
## Publication et reuse
Pour `NO_MATCH`, le fichier temporaire est supprimé et seule une ligne sans
asset est créée. Pour `MATCHED` : temp dans `assets/matches`, écriture complète,
`fsync` du fichier, SHA-256, chemin `assets/matches/<préfixe>/<sha256>`, puis
publication atomique par `link`. Une race de même contenu est validée; un
contenu différent déjà présent n'est jamais écrasé lors d'une publication
fraîche. Lorsqu'une ligne existante prouve que son asset content-addressed est
corrompu, le Matcher recalcule puis remplace atomiquement cet asset par `rename`
et répare les métadonnées de la même ligne sous transaction. Le temp est nettoyé
et le répertoire final synchronisé.
Le reuse `MATCHED` exige métadonnées complètes, fichier régulier non symbolique,
taille, SHA-256, header, type/dimension, IDs A/B, compte et entrées valides. Il
n'existe aucun fallback faisant confiance au chemin ou à l'existence seuls.
La validation de reuse lit le fichier borné une seule fois et calcule le SHA-256
sur ce même buffer.
## Bornes mémoire et performance
Les deux buffers de descripteurs contigus occupent au maximum 512 Kio pour ORB
ou 8 Mio pour SIFT/RootSIFT. Les résultats KNN ne contiennent que deux DMatch
par query, les matches filtrés et le Match File sont chacun bornés à environ
96 Kio. Le working set contrôlé du Matcher est donc inférieur à environ 10 Mio,
hors scratch interne borné par OpenCV; aucune structure `A × B` n'est
matérialisée.
Le Match File complet est sérialisé dans un buffer heap borné à 98336 octets et
écrit par un unique `write_exact`, puis synchronisé une fois. Les mesures locales
restent dans `.opencode/work/current_ticket.md`, pas dans ce contrat canonique.
À 8192 features, le coût dominant mesuré reste l'évaluation exacte des
distances dans `cv::BFMatcher::knnMatch`; v1 ne remplace pas OpenCV ni BFMatcher.
Le Matcher n'est pas encore un task kind autonome. Lorsqu'il est orchestré par
une tâche, celle-ci doit utiliser l'unique Resource Governor existant avec une
estimation couvrant ce working set; aucune seconde logique de budget n'est
introduite ici.
## Déterminisme et fingerprint
Le fingerprint couvre kind, version, `k=2`, threshold float32 et absence de
cross-check. À Feature Files, configuration, implémentation/version OpenCV et
environnement numérique compatibles identiques, SIFT/RootSIFT garantissent les
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

View file

@ -111,6 +111,23 @@ peine d'être试探ées ? » sans validation géométrique.
**Statut :** IMPLEMENTED **Statut :** IMPLEMENTED
### Match Result
Persistance d'un calcul descriptor-level réussi entre deux Feature Sets
liés à une Candidate Pair. Identité déterministe par
`(candidate_pair_id, feature_set_id_a, feature_set_id_b, matcher_kind, matcher_version, parameter_fingerprint)`.
Validation d'appartenance Feature Set → image. Les correspondances vivent dans
le Match Store v1 content-addressed; les échecs restent dans le Task Runtime.
**Statut :** IMPLEMENTED
### Matcher
Matching de descripteurs entre deux Feature Sets via BFMatcher OpenCV
(ORB Hamming, SIFT/RootSIFT L2) avec Lowe ratio test. Produit un
Match File content-addressed et un Match Result dans Project DB.
Déterministe, idempotent, borné.
**Statut :** IMPLEMENTED
## Résultats et publication live ## Résultats et publication live
Les traitements fonctionnent par séquences adaptatives : lire un lot borné, Les traitements fonctionnent par séquences adaptatives : lire un lot borné,

View file

@ -1,9 +1,10 @@
# Base de données projet Lardon3D # Base de données projet Lardon3D
> Version courante : **v9**. La migration transactionnelle v8→v9 ajoute > Version courante : **v10**. La migration transactionnelle v9→v10 ajoute
> la table `candidate_pair_generate_tasks` pour la tâche durable Candidate > la table `match_results` pour le Match Result Model. La migration v8→v9
> Pair. La migration v7→v8 ajoute la table `candidate_pairs` pour le > ajoute la table `candidate_pair_generate_tasks` pour la tâche durable
> sous-système Candidate Pair. > Candidate Pair. La migration v7→v8 ajoute la table `candidate_pairs` pour
> le sous-système Candidate Pair.
> Les migrations historiques restent ordonnées et les faults d'injection > Les migrations historiques restent ordonnées et les faults d'injection
> vérifient le rollback. > vérifient le rollback.
@ -305,6 +306,66 @@ CREATE TABLE candidate_pair_generate_tasks(
- `lardon3d_project_db_record_candidate_pair_generate_task()` — UPSERT checkpoint - `lardon3d_project_db_record_candidate_pair_generate_task()` — UPSERT checkpoint
- `lardon3d_project_db_load_candidate_pair_generate_task()` — SELECT par task_id - `lardon3d_project_db_load_candidate_pair_generate_task()` — SELECT par task_id
## Schéma v10 implémenté
La migration v9→v10 ajoute la table `match_results` pour le Match Result
Model :
```sql
CREATE TABLE match_results(
match_result_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(match_result_id>0),
candidate_pair_id INTEGER NOT NULL
REFERENCES candidate_pairs(candidate_pair_id),
feature_set_id_a INTEGER NOT NULL
REFERENCES feature_sets(feature_set_id),
feature_set_id_b INTEGER NOT NULL
REFERENCES feature_sets(feature_set_id),
matcher_kind TEXT NOT NULL
CHECK(length(matcher_kind)>0 AND length(matcher_kind)<=64),
matcher_version INTEGER NOT NULL CHECK(matcher_version>0),
parameter_fingerprint BLOB NOT NULL
CHECK(length(parameter_fingerprint)=32),
result_status INTEGER NOT NULL CHECK(result_status IN (0,1)),
match_count INTEGER NOT NULL CHECK(match_count>=0 AND match_count<=8192),
match_asset_sha256 BLOB CHECK(match_asset_sha256 IS NULL OR
length(match_asset_sha256)=32),
match_asset_path TEXT CHECK(match_asset_path IS NULL OR
length(match_asset_path)>0),
match_asset_size_bytes INTEGER CHECK(match_asset_size_bytes IS NULL OR
match_asset_size_bytes>0),
created_at INTEGER NOT NULL CHECK(created_at>=0),
CHECK((result_status=0 AND match_count=0 AND match_asset_sha256 IS NULL
AND match_asset_path IS NULL AND match_asset_size_bytes IS NULL)
OR (result_status=1 AND match_count>0 AND match_asset_sha256 IS NOT NULL
AND match_asset_path IS NOT NULL AND match_asset_size_bytes IS NOT NULL)),
UNIQUE(candidate_pair_id, feature_set_id_a, feature_set_id_b,
matcher_kind, matcher_version, parameter_fingerprint)
);
CREATE INDEX match_results_candidate_pair_idx
ON match_results(candidate_pair_id);
CREATE INDEX match_results_feature_set_a_idx
ON match_results(feature_set_id_a);
CREATE INDEX match_results_feature_set_b_idx
ON match_results(feature_set_id_b);
```
**Invariants** :
- `UNIQUE(candidate_pair_id, feature_set_id_a, feature_set_id_b, matcher_kind, matcher_version, 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` 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
- `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
- `matcher_kind` borné à 64 caractères
- `parameter_fingerprint` exactement 32 octets (SHA-256)
**API** :
- `lardon3d_project_db_create_match_result()` — INSERT avec validation des contraintes
- `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_list_match_results()` — SELECT paginé ORDER BY id
## Ouverture et migrations ## Ouverture et migrations
Une DB vide reçoit directement le schéma v7 dans une transaction Une DB vide reçoit directement le schéma v7 dans une transaction
@ -452,7 +513,7 @@ ouvert.
## Statut ## Statut
**IMPLEMENTED** — SQLite système, schéma v9 et migrations v1→v2→v3→v4→v5→v6→v7→v8→v9, identité **IMPLEMENTED** — SQLite système, schéma v10 et migrations v1→v2→v3→v4→v5→v6→v7→v8→v9→v10, identité
projet, transactions tâche+checkpoint, pagination de reprise et artefacts projet, transactions tâche+checkpoint, pagination de reprise et artefacts
génériques. génériques.

View file

@ -79,7 +79,7 @@ updates incrémentales et query top-K bornée. Le générateur de paires reste p
|--------|-------------| |--------|-------------|
| **Sources de paires candidates** | (1) Visual index : paires visuellement proches. (2) Proximité temporelle. (3) Scan set commun. (4) Géométrie approximative (si GPS/IMU disponible). | | **Sources de paires candidates** | (1) Visual index : paires visuellement proches. (2) Proximité temporelle. (3) Scan set commun. (4) Géométrie approximative (si GPS/IMU disponible). |
| **Matching coûteux limité** | Le nombre de paires soumises au matching géométrique (étape F) doit être borné. Le candidate generator filtre et classe pour ne garder que les paires les plus prometteuses. | | **Matching coûteux limité** | Le nombre de paires soumises au matching géométrique (étape F) doit être borné. Le candidate generator filtre et classe pour ne garder que les paires les plus prometteuses. |
| **Persistance** | Les paires candidates sont persistées dans la table `candidate_pairs` (Project DB v9). Ordre canonique : `image_id_a < image_id_b`. Self-pairs interdits. Unicité garantie. | | **Persistance** | Les paires candidates sont persistées dans la table `candidate_pairs` (Project DB v10). Ordre canonique : `image_id_a < image_id_b`. Self-pairs interdits. Unicité garantie. |
| **Déterminisme** | Pour mêmes entrées et configuration, le générateur produit les mêmes paires dans le même ordre. | | **Déterminisme** | Pour mêmes entrées et configuration, le générateur produit les mêmes paires dans le même ordre. |
| **Idempotence** | L'exécution répétée ne crée pas de doublons. | | **Idempotence** | L'exécution répétée ne crée pas de doublons. |

View file

@ -94,6 +94,11 @@ Le Resource Governor est l'unique propriétaire des budgets (RAM, GPU, CPU, IO).
réelle du lot ; `peak_memory_bytes == 0` signifie « mesure inconnue ». réelle du lot ; `peak_memory_bytes == 0` signifie « mesure inconnue ».
Chaque séquence interroge le Visual Index pour jusqu'à 64 Feature Sets et Chaque séquence interroge le Visual Index pour jusqu'à 64 Feature Sets et
persiste les paires candidates avec idempotence. persiste les paires candidates avec idempotence.
- Le Matcher v1 possède un working set contrôlé inférieur à environ 10 Mio au
maximum SIFT/RootSIFT (8 Mio de descripteurs contigus, KNN `k=2`, sorties et
fichier bornés), hors scratch interne OpenCV. Il n'est pas encore exposé comme
task kind autonome; sa future orchestration devra réserver CPU+IO et cette
estimation via ce Governor, sans budget parallèle.
## Limites actuelles ## Limites actuelles

View file

@ -0,0 +1,86 @@
#ifndef LARDON3D_MATCH_FILE_H
#define LARDON3D_MATCH_FILE_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
enum {
LARDON3D_MATCH_FILE_VERSION = 1,
LARDON3D_MATCH_FILE_HEADER_SIZE = 32,
LARDON3D_MATCH_FILE_ENTRY_SIZE = 12,
LARDON3D_MATCH_FILE_MAX_MATCHES = 8192,
LARDON3D_MATCH_FILE_MAX_SIZE = 98336,
};
#define LARDON3D_MATCH_FILE_MAGIC "L3DM"
typedef struct {
uint32_t feature_index_a;
uint32_t feature_index_b;
float distance;
} Lardon3DMatchFileEntry;
typedef struct {
unsigned char magic[4];
uint8_t format_version;
uint8_t descriptor_type;
uint16_t reserved;
uint32_t match_count;
uint32_t descriptor_dimension;
uint64_t feature_set_id_a;
uint64_t feature_set_id_b;
} Lardon3DMatchFileHeader;
typedef enum {
LARDON3D_MATCH_FILE_OK = 0,
LARDON3D_MATCH_FILE_INVALID_ARGUMENT,
LARDON3D_MATCH_FILE_IO_ERROR,
LARDON3D_MATCH_FILE_BAD_MAGIC,
LARDON3D_MATCH_FILE_BAD_VERSION,
LARDON3D_MATCH_FILE_BAD_TYPE,
LARDON3D_MATCH_FILE_BAD_COUNT,
LARDON3D_MATCH_FILE_BAD_SIZE,
LARDON3D_MATCH_FILE_BAD_ENTRY,
LARDON3D_MATCH_FILE_CORRUPT,
} Lardon3DMatchFileResult;
Lardon3DMatchFileResult lardon3d_match_file_write(
int fd,
uint8_t descriptor_type,
uint32_t descriptor_dimension,
uint64_t feature_set_id_a,
uint64_t feature_set_id_b,
const Lardon3DMatchFileEntry *entries,
uint32_t match_count);
Lardon3DMatchFileResult lardon3d_match_file_read(
int fd,
Lardon3DMatchFileHeader *header,
Lardon3DMatchFileEntry *entries,
size_t entry_capacity,
uint32_t *match_count,
uint64_t expected_feature_set_id_a,
uint64_t expected_feature_set_id_b,
uint32_t expected_feature_count_a,
uint32_t expected_feature_count_b);
Lardon3DMatchFileResult lardon3d_match_file_validate(
const char *path,
Lardon3DMatchFileHeader *header,
uint64_t expected_feature_set_id_a,
uint64_t expected_feature_set_id_b,
uint32_t expected_feature_count_a,
uint32_t expected_feature_count_b);
Lardon3DMatchFileResult lardon3d_match_file_validate_asset(
const char *path,
const unsigned char expected_sha256[32],
uint64_t expected_size,
Lardon3DMatchFileHeader *header,
uint64_t expected_feature_set_id_a,
uint64_t expected_feature_set_id_b,
uint32_t expected_feature_count_a,
uint32_t expected_feature_count_b);
#endif

View file

@ -0,0 +1,91 @@
#ifndef LARDON3D_MATCHER_H
#define LARDON3D_MATCHER_H
#include <lardon3d/feature_store.h>
#include <lardon3d/match_file.h>
#include <lardon3d/project_db.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_MATCHER_VERSION = 1,
LARDON3D_MATCHER_KNN_K = 2,
};
typedef enum {
LARDON3D_MATCHER_ORB_BF = 0,
LARDON3D_MATCHER_SIFT_BF = 1,
LARDON3D_MATCHER_ROOTSIFT_BF = 2,
} Lardon3DMatcherKind;
typedef struct {
Lardon3DMatcherKind kind;
float ratio_threshold;
} Lardon3DMatcherParams;
typedef struct {
uint32_t knn_query_count;
uint32_t match_count;
uint32_t feature_count_a;
uint32_t feature_count_b;
uint64_t feature_open_ns;
uint64_t descriptor_read_ns;
uint64_t knn_ns;
uint64_t filter_ns;
uint64_t canonicalize_ns;
uint64_t serialize_ns;
uint64_t sha256_ns;
uint64_t publication_ns;
uint64_t database_ns;
uint64_t total_ns;
} Lardon3DMatcherStats;
typedef enum {
LARDON3D_MATCHER_OK = 0,
LARDON3D_MATCHER_INVALID_ARGUMENT,
LARDON3D_MATCHER_IO_ERROR,
LARDON3D_MATCHER_TYPE_MISMATCH,
LARDON3D_MATCHER_FAILED,
} Lardon3DMatcherResult;
const char *lardon3d_matcher_kind_string(Lardon3DMatcherKind kind);
float lardon3d_matcher_default_ratio(Lardon3DMatcherKind kind);
void lardon3d_matcher_fingerprint(const Lardon3DMatcherParams *params,
unsigned char fingerprint[32]);
Lardon3DMatcherResult lardon3d_matcher_run(
const char *project_path,
const Lardon3DProjectDbFeatureSet *feature_set_a,
const Lardon3DProjectDbFeatureSet *feature_set_b,
const Lardon3DMatcherParams *params,
const char *match_file_path,
Lardon3DMatcherStats *stats);
Lardon3DMatcherResult lardon3d_matcher_match_and_publish(
const char *project_path,
Lardon3DProjectDb *database,
const Lardon3DProjectDbCandidatePair *pair,
const Lardon3DProjectDbFeatureSet *feature_set_a,
const Lardon3DProjectDbFeatureSet *feature_set_b,
const Lardon3DMatcherParams *params,
Lardon3DProjectDbMatchResult *result);
Lardon3DMatcherResult lardon3d_matcher_match_and_publish_profiled(
const char *project_path,
Lardon3DProjectDb *database,
const Lardon3DProjectDbCandidatePair *pair,
const Lardon3DProjectDbFeatureSet *feature_set_a,
const Lardon3DProjectDbFeatureSet *feature_set_b,
const Lardon3DMatcherParams *params,
Lardon3DProjectDbMatchResult *result,
Lardon3DMatcherStats *stats);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -9,7 +9,7 @@
#include <lardon3d/task.h> #include <lardon3d/task.h>
enum { enum {
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 9, LARDON3D_PROJECT_DB_SCHEMA_VERSION = 10,
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,
@ -17,6 +17,7 @@ enum {
LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX = 256, LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX = 256,
LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX = 256, LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX = 256,
LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX = 256, LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX = 256,
LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX = 256,
LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY = 256, LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY = 256,
LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY = 256, LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY = 256,
LARDON3D_PROJECT_DB_SHA256_SIZE = 32, LARDON3D_PROJECT_DB_SHA256_SIZE = 32,
@ -130,11 +131,33 @@ typedef struct {
int64_t created_at; int64_t created_at;
} Lardon3DProjectDbCandidatePair; } Lardon3DProjectDbCandidatePair;
typedef struct {
uint64_t match_result_id;
uint64_t candidate_pair_id;
uint64_t feature_set_id_a;
uint64_t feature_set_id_b;
char matcher_kind[LARDON3D_PROJECT_DB_KIND_CAPACITY];
uint32_t matcher_version;
unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE];
int result_status;
uint32_t match_count;
bool has_match_asset;
unsigned char match_asset_sha256[LARDON3D_PROJECT_DB_SHA256_SIZE];
char match_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
uint64_t match_asset_size_bytes;
int64_t created_at;
} Lardon3DProjectDbMatchResult;
typedef enum { typedef enum {
LARDON3D_PROJECT_DB_IMAGE_REGISTERED = 0, LARDON3D_PROJECT_DB_IMAGE_REGISTERED = 0,
LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT
} Lardon3DProjectDbImageRegisterStatus; } Lardon3DProjectDbImageRegisterStatus;
enum {
LARDON3D_MATCH_RESULT_STATUS_NO_MATCH = 0,
LARDON3D_MATCH_RESULT_STATUS_MATCHED = 1,
};
typedef enum { typedef enum {
LARDON3D_DB_FEATURE_ASSET_DURABLE = 0, LARDON3D_DB_FEATURE_ASSET_DURABLE = 0,
LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE = 1 LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE = 1
@ -441,4 +464,29 @@ Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task(
Lardon3DProjectDb *database, uint64_t task_id, Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DProjectDbCandidatePairGenerateTask *parameters); Lardon3DProjectDbCandidatePairGenerateTask *parameters);
Lardon3DProjectDbResult lardon3d_project_db_create_match_result(
Lardon3DProjectDb *database, uint64_t candidate_pair_id, uint64_t feature_set_id_a,
uint64_t feature_set_id_b, const char *matcher_kind, uint32_t matcher_version,
const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE], int result_status,
uint32_t match_count,
const unsigned char *match_asset_sha256, const char *match_asset_path,
uint64_t match_asset_size_bytes, int64_t created_at,
Lardon3DProjectDbMatchResult *result);
Lardon3DProjectDbResult lardon3d_project_db_load_match_result(
Lardon3DProjectDb *database, uint64_t match_result_id, Lardon3DProjectDbMatchResult *result);
Lardon3DProjectDbResult lardon3d_project_db_repair_match_result(
Lardon3DProjectDb *database, uint64_t match_result_id, int result_status,
uint32_t match_count, const unsigned char *match_asset_sha256,
const char *match_asset_path, uint64_t match_asset_size_bytes,
Lardon3DProjectDbMatchResult *result);
Lardon3DProjectDbResult lardon3d_project_db_find_match_result(
Lardon3DProjectDb *database, uint64_t candidate_pair_id,
uint64_t feature_set_id_a, uint64_t feature_set_id_b,
const char *matcher_kind, uint32_t matcher_version,
const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE],
Lardon3DProjectDbMatchResult *result);
Lardon3DProjectDbResult lardon3d_project_db_list_match_results(
Lardon3DProjectDb *database, uint64_t after_match_result_id,
Lardon3DProjectDbMatchResult *results, size_t capacity, size_t *count);
#endif #endif

View file

@ -59,6 +59,7 @@ executable(
'src/image_catalog_persistent.c', 'src/image_catalog_persistent.c',
'src/feature_extractor_opencv.cpp', 'src/feature_extractor_opencv.cpp',
'src/feature_store.c', 'src/feature_store.c',
'src/feature_extractor_opencv.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/visual_index.c', 'src/visual_index.c',
'src/visual_index_task.c', 'src/visual_index_task.c',
@ -75,6 +76,8 @@ executable(
'src/resource_governor.c', 'src/resource_governor.c',
'src/resource_snapshot.c', 'src/resource_snapshot.c',
'src/hardware_profile.c', 'src/hardware_profile.c',
'src/match_file.c',
'src/matcher.cpp',
], ],
include_directories: include_directories('include'), include_directories: include_directories('include'),
dependencies: [ncursesw, threads, sqlite3, openssl, opencv], dependencies: [ncursesw, threads, sqlite3, openssl, opencv],
@ -389,6 +392,22 @@ project_db_test = executable(
test('project-db', project_db_test, timeout: 30) test('project-db', project_db_test, timeout: 30)
match_result_test = executable(
'test-match-result',
sources: [
'tests/test_match_result.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('match-result', match_result_test, timeout: 30)
project_test = executable( project_test = executable(
'test-project', 'test-project',
sources: [ sources: [
@ -517,3 +536,65 @@ resource_reservation_test = executable(
) )
test('resource-reservation', resource_reservation_test, timeout: 60) test('resource-reservation', resource_reservation_test, timeout: 60)
matcher_test = executable(
'test-matcher',
sources: [
'tests/test_matcher.c',
'src/match_file.c',
'src/matcher.cpp',
'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',
],
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv],
)
test('matcher', matcher_test, timeout: 60)
matcher_e2e_test = executable(
'test-matcher-e2e',
sources: [
'tests/test_matcher_e2e.c',
'src/match_file.c',
'src/matcher.cpp',
'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',
],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv],
)
test('matcher-e2e', matcher_e2e_test, timeout: 60)
executable(
'benchmark-matcher',
sources: [
'tests/benchmark_matcher.cpp',
'src/match_file.c',
'src/matcher.cpp',
'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',
],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv],
)

280
src/match_file.c Normal file
View file

@ -0,0 +1,280 @@
#include <errno.h>
#include <fcntl.h>
#include <math.h>
#include <openssl/evp.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <lardon3d/match_file.h>
enum { FT_U8 = 1, FT_F32 = 2 };
static void put_u32(unsigned char *p, uint32_t v) {
p[0] = (unsigned char)v;
p[1] = (unsigned char)(v >> 8);
p[2] = (unsigned char)(v >> 16);
p[3] = (unsigned char)(v >> 24);
}
static uint32_t get_u32(const unsigned char *p) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) |
((uint32_t)p[3] << 24);
}
static void put_u64(unsigned char *p, uint64_t v) {
put_u32(p, (uint32_t)v);
put_u32(p + 4, (uint32_t)(v >> 32));
}
static uint64_t get_u64(const unsigned char *p) {
return (uint64_t)get_u32(p) | ((uint64_t)get_u32(p + 4) << 32);
}
static void put_float(unsigned char *p, float value) {
uint32_t bits = 0;
memcpy(&bits, &value, sizeof(bits));
put_u32(p, bits);
}
static float get_float(const unsigned char *p) {
uint32_t bits = get_u32(p);
float value = 0;
memcpy(&value, &bits, sizeof(value));
return value;
}
static bool write_exact(int fd, const void *data, size_t size) {
const unsigned char *bytes = data;
size_t used = 0;
while (used < size) {
ssize_t n = write(fd, bytes + used, size - used);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) return false;
used += (size_t)n;
}
return true;
}
static bool read_exact(int fd, void *data, size_t size) {
unsigned char *bytes = data;
size_t used = 0;
while (used < size) {
ssize_t n = read(fd, bytes + used, size - used);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) return false;
used += (size_t)n;
}
return true;
}
static bool valid_dimension(uint8_t type, uint32_t dimension) {
return (type == FT_U8 && dimension == 32) || (type == FT_F32 && dimension == 128);
}
static void decode_header(const unsigned char raw[32], Lardon3DMatchFileHeader *header) {
memcpy(header->magic, raw, 4);
header->format_version = raw[4];
header->descriptor_type = raw[5];
header->reserved = (uint16_t)(raw[6] | ((uint16_t)raw[7] << 8));
header->match_count = get_u32(raw + 8);
header->descriptor_dimension = get_u32(raw + 12);
header->feature_set_id_a = get_u64(raw + 16);
header->feature_set_id_b = get_u64(raw + 24);
}
static Lardon3DMatchFileResult validate_header(const Lardon3DMatchFileHeader *header,
uint64_t file_size,
uint64_t expected_a, uint64_t expected_b) {
if (memcmp(header->magic, LARDON3D_MATCH_FILE_MAGIC, 4) != 0)
return LARDON3D_MATCH_FILE_BAD_MAGIC;
if (header->format_version != LARDON3D_MATCH_FILE_VERSION)
return LARDON3D_MATCH_FILE_BAD_VERSION;
if (header->reserved != 0) return LARDON3D_MATCH_FILE_CORRUPT;
if (!valid_dimension(header->descriptor_type, header->descriptor_dimension))
return LARDON3D_MATCH_FILE_BAD_TYPE;
if (header->match_count > LARDON3D_MATCH_FILE_MAX_MATCHES)
return LARDON3D_MATCH_FILE_BAD_COUNT;
if (header->feature_set_id_a == 0 || header->feature_set_id_b == 0 ||
header->feature_set_id_a == header->feature_set_id_b)
return LARDON3D_MATCH_FILE_CORRUPT;
if ((expected_a != 0 && header->feature_set_id_a != expected_a) ||
(expected_b != 0 && header->feature_set_id_b != expected_b))
return LARDON3D_MATCH_FILE_CORRUPT;
uint64_t expected_size = LARDON3D_MATCH_FILE_HEADER_SIZE +
(uint64_t)header->match_count * LARDON3D_MATCH_FILE_ENTRY_SIZE;
return file_size == expected_size ? LARDON3D_MATCH_FILE_OK : LARDON3D_MATCH_FILE_BAD_SIZE;
}
static bool valid_entry(const Lardon3DMatchFileEntry *entry, uint32_t count_a,
uint32_t count_b) {
uint32_t limit_a = count_a == 0 ? 8192 : count_a;
uint32_t limit_b = count_b == 0 ? 8192 : count_b;
return entry->feature_index_a < limit_a && entry->feature_index_b < limit_b &&
isfinite(entry->distance) && entry->distance >= 0.0F;
}
Lardon3DMatchFileResult lardon3d_match_file_write(
int fd, uint8_t descriptor_type, uint32_t descriptor_dimension,
uint64_t feature_set_id_a, uint64_t feature_set_id_b,
const Lardon3DMatchFileEntry *entries, uint32_t match_count) {
if (fd < 0 || !valid_dimension(descriptor_type, descriptor_dimension) ||
feature_set_id_a == 0 || feature_set_id_b == 0 || feature_set_id_a == feature_set_id_b ||
match_count > LARDON3D_MATCH_FILE_MAX_MATCHES || (match_count > 0 && !entries))
return LARDON3D_MATCH_FILE_INVALID_ARGUMENT;
size_t file_size = LARDON3D_MATCH_FILE_HEADER_SIZE +
(size_t)match_count * LARDON3D_MATCH_FILE_ENTRY_SIZE;
unsigned char *raw = calloc(1, file_size);
if (!raw) return LARDON3D_MATCH_FILE_IO_ERROR;
memcpy(raw, LARDON3D_MATCH_FILE_MAGIC, 4);
raw[4] = LARDON3D_MATCH_FILE_VERSION;
raw[5] = descriptor_type;
put_u32(raw + 8, match_count);
put_u32(raw + 12, descriptor_dimension);
put_u64(raw + 16, feature_set_id_a);
put_u64(raw + 24, feature_set_id_b);
for (uint32_t i = 0; i < match_count; ++i) {
if (!valid_entry(&entries[i], 0, 0) ||
(i > 0 && entries[i].feature_index_a <= entries[i - 1].feature_index_a)) {
free(raw);
return LARDON3D_MATCH_FILE_BAD_ENTRY;
}
unsigned char *item = raw + LARDON3D_MATCH_FILE_HEADER_SIZE +
(size_t)i * LARDON3D_MATCH_FILE_ENTRY_SIZE;
put_u32(item, entries[i].feature_index_a);
put_u32(item + 4, entries[i].feature_index_b);
put_float(item + 8, entries[i].distance);
}
bool written = write_exact(fd, raw, file_size);
free(raw);
return written ? LARDON3D_MATCH_FILE_OK : LARDON3D_MATCH_FILE_IO_ERROR;
}
Lardon3DMatchFileResult lardon3d_match_file_read(
int fd, Lardon3DMatchFileHeader *header, Lardon3DMatchFileEntry *entries,
size_t entry_capacity, uint32_t *match_count, uint64_t expected_a,
uint64_t expected_b, uint32_t count_a, uint32_t count_b) {
if (fd < 0 || !header || !match_count || (!entries && entry_capacity != 0) ||
count_a > 8192 || count_b > 8192)
return LARDON3D_MATCH_FILE_INVALID_ARGUMENT;
*match_count = 0;
memset(header, 0, sizeof(*header));
struct stat info;
if (fstat(fd, &info) != 0 || !S_ISREG(info.st_mode) || info.st_size < 0)
return LARDON3D_MATCH_FILE_IO_ERROR;
unsigned char raw[32];
if (!read_exact(fd, raw, sizeof(raw))) return LARDON3D_MATCH_FILE_BAD_SIZE;
decode_header(raw, header);
Lardon3DMatchFileResult result = validate_header(header, (uint64_t)info.st_size,
expected_a, expected_b);
if (result != LARDON3D_MATCH_FILE_OK) return result;
if (header->match_count > entry_capacity) return LARDON3D_MATCH_FILE_BAD_COUNT;
for (uint32_t i = 0; i < header->match_count; ++i) {
unsigned char item[12];
if (!read_exact(fd, item, sizeof(item))) return LARDON3D_MATCH_FILE_BAD_SIZE;
entries[i].feature_index_a = get_u32(item);
entries[i].feature_index_b = get_u32(item + 4);
entries[i].distance = get_float(item + 8);
if (!valid_entry(&entries[i], count_a, count_b) ||
(i > 0 && entries[i].feature_index_a <= entries[i - 1].feature_index_a))
return LARDON3D_MATCH_FILE_BAD_ENTRY;
}
*match_count = header->match_count;
return LARDON3D_MATCH_FILE_OK;
}
Lardon3DMatchFileResult lardon3d_match_file_validate(
const char *path, Lardon3DMatchFileHeader *header, uint64_t expected_a,
uint64_t expected_b, uint32_t count_a, uint32_t count_b) {
if (!path || !header || count_a > 8192 || count_b > 8192)
return LARDON3D_MATCH_FILE_INVALID_ARGUMENT;
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) return LARDON3D_MATCH_FILE_IO_ERROR;
struct stat info;
unsigned char raw[32];
Lardon3DMatchFileResult result = LARDON3D_MATCH_FILE_IO_ERROR;
if (fstat(fd, &info) == 0 && S_ISREG(info.st_mode) && info.st_size >= 0) {
if (!read_exact(fd, raw, sizeof(raw))) {
result = LARDON3D_MATCH_FILE_BAD_SIZE;
} else {
decode_header(raw, header);
result = validate_header(header, (uint64_t)info.st_size, expected_a, expected_b);
if (result == LARDON3D_MATCH_FILE_OK) {
Lardon3DMatchFileEntry entry;
uint32_t previous_a = 0;
for (uint32_t i = 0; i < header->match_count; ++i) {
unsigned char item[12];
if (!read_exact(fd, item, sizeof(item))) {
result = LARDON3D_MATCH_FILE_BAD_SIZE;
break;
}
entry.feature_index_a = get_u32(item);
entry.feature_index_b = get_u32(item + 4);
entry.distance = get_float(item + 8);
if (!valid_entry(&entry, count_a, count_b) ||
(i > 0 && entry.feature_index_a <= previous_a)) {
result = LARDON3D_MATCH_FILE_BAD_ENTRY;
break;
}
previous_a = entry.feature_index_a;
}
}
}
}
(void)close(fd);
return result;
}
Lardon3DMatchFileResult lardon3d_match_file_validate_asset(
const char *path, const unsigned char expected_sha256[32], uint64_t expected_size,
Lardon3DMatchFileHeader *header, uint64_t expected_a, uint64_t expected_b,
uint32_t count_a, uint32_t count_b) {
if (!path || !expected_sha256 || !header || expected_size < LARDON3D_MATCH_FILE_HEADER_SIZE ||
expected_size > LARDON3D_MATCH_FILE_MAX_SIZE || count_a > 8192 || count_b > 8192)
return LARDON3D_MATCH_FILE_INVALID_ARGUMENT;
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) return LARDON3D_MATCH_FILE_IO_ERROR;
struct stat info;
if (fstat(fd, &info) != 0 || !S_ISREG(info.st_mode) || info.st_size < 0 ||
(uint64_t)info.st_size != expected_size) {
(void)close(fd);
return LARDON3D_MATCH_FILE_BAD_SIZE;
}
unsigned char *bytes = malloc((size_t)expected_size);
if (!bytes) {
(void)close(fd);
return LARDON3D_MATCH_FILE_IO_ERROR;
}
bool read_ok = read_exact(fd, bytes, (size_t)expected_size);
(void)close(fd);
if (!read_ok) {
free(bytes);
return LARDON3D_MATCH_FILE_BAD_SIZE;
}
unsigned char actual_sha256[32];
unsigned int digest_size = 0;
bool hash_ok = EVP_Digest(bytes, (size_t)expected_size, actual_sha256, &digest_size,
EVP_sha256(), NULL) == 1 && digest_size == 32;
if (!hash_ok || memcmp(actual_sha256, expected_sha256, 32) != 0) {
free(bytes);
return LARDON3D_MATCH_FILE_CORRUPT;
}
decode_header(bytes, header);
Lardon3DMatchFileResult result = validate_header(header, expected_size, expected_a, expected_b);
uint32_t previous_a = 0;
for (uint32_t i = 0; result == LARDON3D_MATCH_FILE_OK && i < header->match_count; ++i) {
const unsigned char *item = bytes + LARDON3D_MATCH_FILE_HEADER_SIZE +
(size_t)i * LARDON3D_MATCH_FILE_ENTRY_SIZE;
Lardon3DMatchFileEntry entry = {get_u32(item), get_u32(item + 4), get_float(item + 8)};
if (!valid_entry(&entry, count_a, count_b) ||
(i > 0 && entry.feature_index_a <= previous_a))
result = LARDON3D_MATCH_FILE_BAD_ENTRY;
previous_a = entry.feature_index_a;
}
free(bytes);
return result;
}

695
src/matcher.cpp Normal file
View file

@ -0,0 +1,695 @@
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstring>
#include <new>
#include <vector>
#include <fcntl.h>
#include <math.h>
#include <openssl/evp.h>
#include <sys/stat.h>
#include <unistd.h>
#include <opencv2/core.hpp>
#include <opencv2/features2d.hpp>
extern "C" {
#include <lardon3d/feature_extractor.h>
#include <lardon3d/match_file.h>
#include <lardon3d/matcher.h>
}
static const char matcher_orb_str[] = "orb_bf";
static const char matcher_sift_str[] = "sift_bf";
static const char matcher_rootsift_str[] = "rootsift_bf";
static uint64_t elapsed_ns(std::chrono::steady_clock::time_point start) {
auto elapsed = std::chrono::steady_clock::now() - start;
return (uint64_t)std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count();
}
static bool join_path(char output[4096], const char *a, const char *b) {
int n = snprintf(output, 4096, "%s/%s", a, b);
return n > 0 && (size_t)n < 4096;
}
static bool ensure_directory(const char *path) {
if (mkdir(path, 0755) == 0) {
return true;
}
if (errno != EEXIST) {
return false;
}
struct stat info;
return lstat(path, &info) == 0 && S_ISDIR(info.st_mode) && !S_ISLNK(info.st_mode);
}
static bool sync_directory(const char *path) {
int fd = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0) {
return false;
}
bool ok = fsync(fd) == 0;
if (close(fd) != 0) {
ok = false;
}
return ok;
}
static void hex_sha(const unsigned char hash[32], char text[65]) {
static const char digits[] = "0123456789abcdef";
for (size_t i = 0; i < 32; ++i) {
text[2 * i] = digits[hash[i] >> 4];
text[2 * i + 1] = digits[hash[i] & 15];
}
text[64] = '\0';
}
static bool sha256_fd(int fd, unsigned char output[32]) {
EVP_MD_CTX *context = EVP_MD_CTX_new();
if (!context) {
return false;
}
bool ok = EVP_DigestInit_ex(context, EVP_sha256(), NULL) == 1;
unsigned char buffer[65536];
off_t offset = 0;
while (ok) {
ssize_t n = pread(fd, buffer, sizeof(buffer), offset);
if (n < 0 && errno == EINTR) {
continue;
}
if (n < 0) {
ok = false;
break;
}
if (n == 0) {
break;
}
ok = EVP_DigestUpdate(context, buffer, (size_t)n) == 1;
offset += (off_t)n;
}
unsigned int length = 0;
ok = ok && EVP_DigestFinal_ex(context, output, &length) == 1 && length == 32;
EVP_MD_CTX_free(context);
return ok;
}
static bool sha256_file(const char *path, unsigned char output[32]) {
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) {
return false;
}
bool ok = sha256_fd(fd, output);
(void)close(fd);
return ok;
}
/* --- matcher_kind helpers --- */
extern "C" const char *lardon3d_matcher_kind_string(Lardon3DMatcherKind kind) {
switch (kind) {
case LARDON3D_MATCHER_ORB_BF:
return matcher_orb_str;
case LARDON3D_MATCHER_SIFT_BF:
return matcher_sift_str;
case LARDON3D_MATCHER_ROOTSIFT_BF:
return matcher_rootsift_str;
default:
return "unknown";
}
}
extern "C" float lardon3d_matcher_default_ratio(Lardon3DMatcherKind kind) {
switch (kind) {
case LARDON3D_MATCHER_ORB_BF:
return 0.75F;
case LARDON3D_MATCHER_SIFT_BF:
case LARDON3D_MATCHER_ROOTSIFT_BF:
return 0.7F;
default:
return 0.75F;
}
}
extern "C" void lardon3d_matcher_fingerprint(const Lardon3DMatcherParams *params,
unsigned char fingerprint[32]) {
memset(fingerprint, 0, 32);
if (!params) {
return;
}
const char *kind_str = lardon3d_matcher_kind_string(params->kind);
size_t kind_len = strlen(kind_str);
size_t total = kind_len + 4 + 4 + 4 + 4; /* kind + version + knn_k + ratio + cross_check */
unsigned char buffer[128]; /* enough for any kind string */
if (total > sizeof(buffer)) {
return;
}
memcpy(buffer, kind_str, kind_len);
uint32_t ratio_bits = 0;
memcpy(&ratio_bits, &params->ratio_threshold, sizeof(ratio_bits));
auto put_le32 = [](unsigned char *p, uint32_t value) {
p[0] = (unsigned char)value;
p[1] = (unsigned char)(value >> 8);
p[2] = (unsigned char)(value >> 16);
p[3] = (unsigned char)(value >> 24);
};
put_le32(buffer + kind_len, LARDON3D_MATCHER_VERSION);
put_le32(buffer + kind_len + 4, LARDON3D_MATCHER_KNN_K);
put_le32(buffer + kind_len + 8, ratio_bits);
put_le32(buffer + kind_len + 12, 0);
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (ctx) {
if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) &&
EVP_DigestUpdate(ctx, buffer, total) &&
EVP_DigestFinal_ex(ctx, fingerprint, NULL)) {
/* success */
}
EVP_MD_CTX_free(ctx);
}
}
/* --- Matching logic --- */
struct MatchEntry {
int query_idx;
int train_idx;
float distance;
};
struct FeatureReaderPair {
Lardon3DFeatureReader *a = nullptr;
Lardon3DFeatureReader *b = nullptr;
~FeatureReaderPair() {
lardon3d_feature_reader_close(a);
lardon3d_feature_reader_close(b);
}
};
static bool accept_knn_match(const std::vector<cv::DMatch> &knn, float threshold,
MatchEntry *entry) {
if (!entry || knn.empty() || !std::isfinite(threshold) || threshold <= 0.0F ||
threshold >= 1.0F) {
return false;
}
const cv::DMatch &first = knn[0];
if (first.queryIdx < 0 || first.trainIdx < 0 || !std::isfinite(first.distance) ||
first.distance < 0.0F) {
return false;
}
if (knn.size() >= 2) {
const float second = knn[1].distance;
if (!std::isfinite(second) || second < 0.0F || second == 0.0F ||
!(first.distance < threshold * second)) {
return false;
}
}
*entry = {first.queryIdx, first.trainIdx, first.distance};
return true;
}
static bool operator<(const MatchEntry &a, const MatchEntry &b) {
if (a.query_idx != b.query_idx) {
return a.query_idx < b.query_idx;
}
if (a.distance != b.distance) {
return a.distance < b.distance;
}
return a.train_idx < b.train_idx;
}
static bool fill_descriptors_u8(std::vector<unsigned char> &buffer,
Lardon3DFeatureReader *reader,
uint32_t feature_count,
uint32_t descriptor_dimension) {
size_t total = (size_t)feature_count * descriptor_dimension;
buffer.resize(total);
size_t offset = 0;
for (uint32_t start = 0; start < feature_count; start += 256) {
uint32_t batch = feature_count - start;
if (batch > 256) {
batch = 256;
}
size_t batch_bytes = (size_t)batch * descriptor_dimension;
Lardon3DFeatureStoreResult r = lardon3d_feature_reader_descriptors_u8(
reader, start, buffer.data() + offset, batch, batch_bytes);
if (r != LARDON3D_FEATURE_STORE_OK) {
return false;
}
offset += batch_bytes;
}
return true;
}
static bool fill_descriptors_f32(std::vector<float> &buffer,
Lardon3DFeatureReader *reader,
uint32_t feature_count,
uint32_t descriptor_dimension) {
size_t total = (size_t)feature_count * descriptor_dimension;
buffer.resize(total);
size_t offset = 0;
for (uint32_t start = 0; start < feature_count; start += 256) {
uint32_t batch = feature_count - start;
if (batch > 256) {
batch = 256;
}
size_t batch_bytes = (size_t)batch * descriptor_dimension * sizeof(float);
Lardon3DFeatureStoreResult r = lardon3d_feature_reader_descriptors_f32(
reader, start, buffer.data() + offset, batch, batch_bytes);
if (r != LARDON3D_FEATURE_STORE_OK) {
return false;
}
offset += (size_t)batch * descriptor_dimension;
}
return true;
}
static Lardon3DMatcherResult matcher_run_impl(
const char *project_path,
const Lardon3DProjectDbFeatureSet *feature_set_a,
const Lardon3DProjectDbFeatureSet *feature_set_b,
const Lardon3DMatcherParams *params,
const char *match_file_path,
Lardon3DMatcherStats *stats) {
if (stats) {
memset(stats, 0, sizeof(*stats));
}
if (!project_path || !feature_set_a || !feature_set_b || !params || !match_file_path ||
!stats || params->kind < LARDON3D_MATCHER_ORB_BF ||
params->kind > LARDON3D_MATCHER_ROOTSIFT_BF ||
!std::isfinite(params->ratio_threshold) || params->ratio_threshold <= 0.0F ||
params->ratio_threshold >= 1.0F) {
return LARDON3D_MATCHER_INVALID_ARGUMENT;
}
if (feature_set_a->feature_count > LARDON3D_FEATURE_MAX_FEATURES ||
feature_set_b->feature_count > LARDON3D_FEATURE_MAX_FEATURES) {
return LARDON3D_MATCHER_INVALID_ARGUMENT;
}
if (feature_set_a->feature_set_id == feature_set_b->feature_set_id) {
return LARDON3D_MATCHER_INVALID_ARGUMENT;
}
/* Verify same descriptor type and dimension */
if (feature_set_a->descriptor_type != feature_set_b->descriptor_type ||
feature_set_a->descriptor_dimension != feature_set_b->descriptor_dimension) {
return LARDON3D_MATCHER_TYPE_MISMATCH;
}
/* Verify descriptor type matches matcher kind */
if (params->kind == LARDON3D_MATCHER_ORB_BF) {
if (strcmp(feature_set_a->extractor_kind, "orb") != 0 ||
strcmp(feature_set_b->extractor_kind, "orb") != 0 ||
feature_set_a->descriptor_type != LARDON3D_FEATURE_DESCRIPTOR_U8 ||
feature_set_a->descriptor_dimension != 32) {
return LARDON3D_MATCHER_TYPE_MISMATCH;
}
} else if (params->kind == LARDON3D_MATCHER_SIFT_BF) {
if (strcmp(feature_set_a->extractor_kind, "sift") != 0 ||
strcmp(feature_set_b->extractor_kind, "sift") != 0 ||
feature_set_a->descriptor_type != LARDON3D_FEATURE_DESCRIPTOR_F32 ||
feature_set_a->descriptor_dimension != 128) {
return LARDON3D_MATCHER_TYPE_MISMATCH;
}
} else if (strcmp(feature_set_a->extractor_kind, "rootsift") != 0 ||
strcmp(feature_set_b->extractor_kind, "rootsift") != 0 ||
feature_set_a->descriptor_type != LARDON3D_FEATURE_DESCRIPTOR_F32 ||
feature_set_a->descriptor_dimension != 128) {
return LARDON3D_MATCHER_TYPE_MISMATCH;
}
stats->feature_count_a = feature_set_a->feature_count;
stats->feature_count_b = feature_set_b->feature_count;
/* Open feature readers */
FeatureReaderPair readers;
Lardon3DFeatureFileMetadata meta_a, meta_b;
auto phase_start = std::chrono::steady_clock::now();
Lardon3DFeatureStoreResult open_a = lardon3d_feature_reader_open(
project_path, feature_set_a, &readers.a, &meta_a);
Lardon3DFeatureStoreResult open_b = lardon3d_feature_reader_open(
project_path, feature_set_b, &readers.b, &meta_b);
if (open_a != LARDON3D_FEATURE_STORE_OK || open_b != LARDON3D_FEATURE_STORE_OK) {
return LARDON3D_MATCHER_IO_ERROR;
}
stats->feature_open_ns = elapsed_ns(phase_start);
Lardon3DMatcherResult result = LARDON3D_MATCHER_OK;
std::vector<MatchEntry> filtered_matches;
filtered_matches.reserve(feature_set_a->feature_count);
if (params->kind == LARDON3D_MATCHER_ORB_BF) {
std::vector<unsigned char> desc_a, desc_b;
phase_start = std::chrono::steady_clock::now();
if (!fill_descriptors_u8(desc_a, readers.a, feature_set_a->feature_count, 32) ||
!fill_descriptors_u8(desc_b, readers.b, feature_set_b->feature_count, 32)) {
result = LARDON3D_MATCHER_IO_ERROR;
} else {
stats->descriptor_read_ns = elapsed_ns(phase_start);
cv::Mat mat_a((int)feature_set_a->feature_count, 32, CV_8UC1, desc_a.data());
cv::Mat mat_b((int)feature_set_b->feature_count, 32, CV_8UC1, desc_b.data());
cv::BFMatcher matcher(cv::NORM_HAMMING, false);
std::vector<std::vector<cv::DMatch>> matches;
phase_start = std::chrono::steady_clock::now();
matcher.knnMatch(mat_a, mat_b, matches, LARDON3D_MATCHER_KNN_K);
stats->knn_ns = elapsed_ns(phase_start);
stats->knn_query_count = (uint32_t)matches.size();
phase_start = std::chrono::steady_clock::now();
for (size_t i = 0; i < matches.size(); ++i) {
const auto &knn = matches[i];
MatchEntry entry;
if (accept_knn_match(knn, params->ratio_threshold, &entry))
filtered_matches.push_back(entry);
}
stats->filter_ns = elapsed_ns(phase_start);
}
} else {
std::vector<float> desc_a, desc_b;
phase_start = std::chrono::steady_clock::now();
if (!fill_descriptors_f32(desc_a, readers.a, feature_set_a->feature_count, 128) ||
!fill_descriptors_f32(desc_b, readers.b, feature_set_b->feature_count, 128)) {
result = LARDON3D_MATCHER_IO_ERROR;
} else {
stats->descriptor_read_ns = elapsed_ns(phase_start);
cv::Mat mat_a((int)feature_set_a->feature_count, 128, CV_32FC1, desc_a.data());
cv::Mat mat_b((int)feature_set_b->feature_count, 128, CV_32FC1, desc_b.data());
cv::BFMatcher matcher(cv::NORM_L2, false);
std::vector<std::vector<cv::DMatch>> matches;
phase_start = std::chrono::steady_clock::now();
matcher.knnMatch(mat_a, mat_b, matches, LARDON3D_MATCHER_KNN_K);
stats->knn_ns = elapsed_ns(phase_start);
stats->knn_query_count = (uint32_t)matches.size();
phase_start = std::chrono::steady_clock::now();
for (size_t i = 0; i < matches.size(); ++i) {
const auto &knn = matches[i];
MatchEntry entry;
if (accept_knn_match(knn, params->ratio_threshold, &entry))
filtered_matches.push_back(entry);
}
stats->filter_ns = elapsed_ns(phase_start);
}
}
if (result != LARDON3D_MATCHER_OK) {
return result;
}
phase_start = std::chrono::steady_clock::now();
/* Sort deterministically */
std::sort(filtered_matches.begin(), filtered_matches.end());
for (size_t i = 1; i < filtered_matches.size(); ++i) {
if (filtered_matches[i].query_idx == filtered_matches[i - 1].query_idx) {
return LARDON3D_MATCHER_FAILED;
}
}
stats->match_count = (uint32_t)filtered_matches.size();
stats->canonicalize_ns = elapsed_ns(phase_start);
if (filtered_matches.size() > LARDON3D_MATCH_FILE_MAX_MATCHES) {
return LARDON3D_MATCHER_FAILED;
}
/* Build entry array and write */
std::vector<Lardon3DMatchFileEntry> entries(filtered_matches.size());
for (size_t i = 0; i < filtered_matches.size(); ++i) {
entries[i].feature_index_a = (uint32_t)filtered_matches[i].query_idx;
entries[i].feature_index_b = (uint32_t)filtered_matches[i].train_idx;
entries[i].distance = filtered_matches[i].distance;
}
uint8_t descriptor_type = feature_set_a->descriptor_type == LARDON3D_FEATURE_DESCRIPTOR_U8
? (uint8_t)1
: (uint8_t)2;
phase_start = std::chrono::steady_clock::now();
int fd = open(match_file_path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (fd < 0) {
return LARDON3D_MATCHER_IO_ERROR;
}
Lardon3DMatchFileResult write_result = lardon3d_match_file_write(
fd, descriptor_type, feature_set_a->descriptor_dimension,
feature_set_a->feature_set_id, feature_set_b->feature_set_id,
entries.data(), (uint32_t)entries.size());
if (write_result == LARDON3D_MATCH_FILE_OK) {
if (fsync(fd) != 0) {
write_result = LARDON3D_MATCH_FILE_IO_ERROR;
}
}
(void)close(fd);
if (write_result != LARDON3D_MATCH_FILE_OK) {
unlink(match_file_path);
return LARDON3D_MATCHER_IO_ERROR;
}
stats->serialize_ns = elapsed_ns(phase_start);
return LARDON3D_MATCHER_OK;
}
extern "C" Lardon3DMatcherResult lardon3d_matcher_run(
const char *project_path,
const Lardon3DProjectDbFeatureSet *feature_set_a,
const Lardon3DProjectDbFeatureSet *feature_set_b,
const Lardon3DMatcherParams *params,
const char *match_file_path,
Lardon3DMatcherStats *stats) {
try {
return matcher_run_impl(project_path, feature_set_a, feature_set_b, params,
match_file_path, stats);
} catch (const cv::Exception &) {
return LARDON3D_MATCHER_FAILED;
} catch (const std::bad_alloc &) {
return LARDON3D_MATCHER_FAILED;
}
}
extern "C" Lardon3DMatcherResult lardon3d_matcher_match_and_publish_profiled(
const char *project_path,
Lardon3DProjectDb *database,
const Lardon3DProjectDbCandidatePair *pair,
const Lardon3DProjectDbFeatureSet *feature_set_a,
const Lardon3DProjectDbFeatureSet *feature_set_b,
const Lardon3DMatcherParams *params,
Lardon3DProjectDbMatchResult *result,
Lardon3DMatcherStats *profile) {
auto total_start = std::chrono::steady_clock::now();
if (profile) memset(profile, 0, sizeof(*profile));
if (result) {
memset(result, 0, sizeof(*result));
}
if (!project_path || !database || !pair || !feature_set_a || !feature_set_b || !params ||
!result) {
return LARDON3D_MATCHER_INVALID_ARGUMENT;
}
const char *matcher_kind = lardon3d_matcher_kind_string(params->kind);
unsigned char fp[32];
lardon3d_matcher_fingerprint(params, fp);
uint64_t repair_match_result_id = 0;
/* Check if Match Result already exists */
Lardon3DProjectDbResult found = lardon3d_project_db_find_match_result(
database, pair->candidate_pair_id, feature_set_a->feature_set_id,
feature_set_b->feature_set_id, matcher_kind, LARDON3D_MATCHER_VERSION, fp, result);
if (found == LARDON3D_PROJECT_DB_OK) {
if (result->result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH &&
result->match_count == 0 && !result->has_match_asset &&
result->match_asset_path[0] == '\0' && result->match_asset_size_bytes == 0) {
if (profile) profile->total_ns = elapsed_ns(total_start);
return LARDON3D_MATCHER_OK;
}
if (result->result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED &&
result->match_count > 0 && result->has_match_asset &&
result->match_asset_path[0] != '\0' && result->match_asset_size_bytes > 0) {
char asset_full[4096];
if (join_path(asset_full, project_path, result->match_asset_path)) {
Lardon3DMatchFileHeader header;
if (lardon3d_match_file_validate_asset(
asset_full, result->match_asset_sha256,
result->match_asset_size_bytes, &header, feature_set_a->feature_set_id,
feature_set_b->feature_set_id, feature_set_a->feature_count,
feature_set_b->feature_count) == LARDON3D_MATCH_FILE_OK &&
header.match_count == result->match_count &&
header.descriptor_type == feature_set_a->descriptor_type &&
header.descriptor_dimension == feature_set_a->descriptor_dimension) {
if (profile) profile->total_ns = elapsed_ns(total_start);
return LARDON3D_MATCHER_OK;
}
}
}
repair_match_result_id = result->match_result_id;
} else if (found != LARDON3D_PROJECT_DB_NOT_FOUND) {
return LARDON3D_MATCHER_IO_ERROR;
}
/* Ensure assets/matches directory */
char assets[4096], matches_dir[4096];
if (!join_path(assets, project_path, "assets") || !ensure_directory(assets) ||
!join_path(matches_dir, assets, "matches") || !ensure_directory(matches_dir)) {
return LARDON3D_MATCHER_IO_ERROR;
}
/* Write to temp file */
char tmp_path[4096];
int n = snprintf(tmp_path, sizeof(tmp_path), "%s/.match-XXXXXX", matches_dir);
if (n <= 0 || (size_t)n >= sizeof(tmp_path)) {
return LARDON3D_MATCHER_IO_ERROR;
}
int tmp_fd = mkstemp(tmp_path);
if (tmp_fd < 0) {
return LARDON3D_MATCHER_IO_ERROR;
}
/* matcher_run reopens the named temp with O_TRUNC. */
if (close(tmp_fd) != 0) {
(void)unlink(tmp_path);
return LARDON3D_MATCHER_IO_ERROR;
}
Lardon3DMatcherStats stats;
Lardon3DMatcherResult run_result = lardon3d_matcher_run(
project_path, feature_set_a, feature_set_b, params, tmp_path, &stats);
if (run_result != LARDON3D_MATCHER_OK) {
unlink(tmp_path);
return run_result;
}
if (profile) *profile = stats;
int64_t now = (int64_t)time(NULL);
if (now < 0) {
unlink(tmp_path);
return LARDON3D_MATCHER_IO_ERROR;
}
if (stats.match_count == 0) {
if (unlink(tmp_path) != 0) return LARDON3D_MATCHER_IO_ERROR;
auto database_start = std::chrono::steady_clock::now();
Lardon3DProjectDbResult db_result = repair_match_result_id != 0
? lardon3d_project_db_repair_match_result(
database, repair_match_result_id, LARDON3D_MATCH_RESULT_STATUS_NO_MATCH,
0, NULL, NULL, 0, result)
: lardon3d_project_db_create_match_result(
database, pair->candidate_pair_id, feature_set_a->feature_set_id,
feature_set_b->feature_set_id, matcher_kind, LARDON3D_MATCHER_VERSION, fp,
LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, NULL, NULL, 0, now, result);
if (db_result == LARDON3D_PROJECT_DB_CONSTRAINT) {
return lardon3d_matcher_match_and_publish_profiled(
project_path, database, pair, feature_set_a, feature_set_b, params, result,
profile);
}
if (profile) {
profile->database_ns = elapsed_ns(database_start);
profile->total_ns = elapsed_ns(total_start);
}
return db_result == LARDON3D_PROJECT_DB_OK ? LARDON3D_MATCHER_OK
: LARDON3D_MATCHER_FAILED;
}
/* Compute SHA-256 of temp file */
auto sha_start = std::chrono::steady_clock::now();
unsigned char file_hash[32];
if (!sha256_file(tmp_path, file_hash)) {
unlink(tmp_path);
return LARDON3D_MATCHER_IO_ERROR;
}
if (profile) profile->sha256_ns = elapsed_ns(sha_start);
/* Create content-addressed directory */
auto publication_start = std::chrono::steady_clock::now();
char hex[65];
hex_sha(file_hash, hex);
char prefix[4096];
n = snprintf(prefix, sizeof(prefix), "%s/%.2s", matches_dir, hex);
if (n <= 0 || (size_t)n >= sizeof(prefix) || !ensure_directory(prefix)) {
unlink(tmp_path);
return LARDON3D_MATCHER_IO_ERROR;
}
/* Build relative and final paths */
char relative[LARDON3D_PROJECT_DB_PATH_CAPACITY];
n = snprintf(relative, sizeof(relative), "assets/matches/%.2s/%s", hex, hex);
if (n <= 0 || (size_t)n >= (int)sizeof(relative)) {
unlink(tmp_path);
return LARDON3D_MATCHER_IO_ERROR;
}
char final_path[4096];
if (!join_path(final_path, project_path, relative)) {
unlink(tmp_path);
return LARDON3D_MATCHER_IO_ERROR;
}
/* Atomic content-addressed publication via link. */
if (link(tmp_path, final_path) != 0) {
if (errno != EEXIST) {
unlink(tmp_path);
return LARDON3D_MATCHER_IO_ERROR;
}
/* EEXIST: verify content match */
int existing = open(final_path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
struct stat info;
unsigned char existing_hash[32];
bool same = existing >= 0 && fstat(existing, &info) == 0 && S_ISREG(info.st_mode) &&
sha256_fd(existing, existing_hash) &&
memcmp(existing_hash, file_hash, 32) == 0;
if (existing >= 0) {
(void)close(existing);
}
if (!same) {
if (repair_match_result_id == 0 || rename(tmp_path, final_path) != 0) {
unlink(tmp_path);
return LARDON3D_MATCHER_IO_ERROR;
}
tmp_path[0] = '\0';
}
}
if (tmp_path[0] != '\0' && unlink(tmp_path) != 0) {
return LARDON3D_MATCHER_IO_ERROR;
}
if (!sync_directory(prefix)) {
return LARDON3D_MATCHER_IO_ERROR;
}
if (profile) profile->publication_ns = elapsed_ns(publication_start);
/* Get match file size */
struct stat st;
uint64_t file_size = 0;
if (stat(final_path, &st) == 0 && S_ISREG(st.st_mode)) {
file_size = (uint64_t)st.st_size;
}
auto database_start = std::chrono::steady_clock::now();
Lardon3DProjectDbResult db_result = repair_match_result_id != 0
? lardon3d_project_db_repair_match_result(
database, repair_match_result_id, LARDON3D_MATCH_RESULT_STATUS_MATCHED,
stats.match_count, file_hash, relative, (uint64_t)file_size, result)
: lardon3d_project_db_create_match_result(
database, pair->candidate_pair_id, feature_set_a->feature_set_id,
feature_set_b->feature_set_id, matcher_kind, LARDON3D_MATCHER_VERSION, fp,
LARDON3D_MATCH_RESULT_STATUS_MATCHED, stats.match_count, file_hash, relative,
(uint64_t)file_size, now, result);
if (db_result == LARDON3D_PROJECT_DB_CONSTRAINT) {
return lardon3d_matcher_match_and_publish_profiled(
project_path, database, pair, feature_set_a, feature_set_b, params, result, profile);
}
if (db_result != LARDON3D_PROJECT_DB_OK) {
return LARDON3D_MATCHER_FAILED;
}
if (profile) {
profile->database_ns = elapsed_ns(database_start);
profile->total_ns = elapsed_ns(total_start);
}
return LARDON3D_MATCHER_OK;
}
extern "C" Lardon3DMatcherResult lardon3d_matcher_match_and_publish(
const char *project_path,
Lardon3DProjectDb *database,
const Lardon3DProjectDbCandidatePair *pair,
const Lardon3DProjectDbFeatureSet *feature_set_a,
const Lardon3DProjectDbFeatureSet *feature_set_b,
const Lardon3DMatcherParams *params,
Lardon3DProjectDbMatchResult *result) {
return lardon3d_matcher_match_and_publish_profiled(
project_path, database, pair, feature_set_a, feature_set_b, params, result, NULL);
}

View file

@ -199,6 +199,33 @@ static const char schema_candidate_generate_v9[] =
"scanset_filter INTEGER NOT NULL CHECK(scanset_filter>=0 AND scanset_filter<=2)," "scanset_filter INTEGER NOT NULL CHECK(scanset_filter>=0 AND scanset_filter<=2),"
"exclude_same_asset INTEGER NOT NULL CHECK(exclude_same_asset IN (0,1)));"; "exclude_same_asset INTEGER NOT NULL CHECK(exclude_same_asset IN (0,1)));";
static const char schema_match_result_v10[] =
"CREATE TABLE match_results("
"match_result_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(match_result_id>0),"
"candidate_pair_id INTEGER NOT NULL REFERENCES candidate_pairs(candidate_pair_id),"
"feature_set_id_a INTEGER NOT NULL REFERENCES feature_sets(feature_set_id),"
"feature_set_id_b INTEGER NOT NULL REFERENCES feature_sets(feature_set_id),"
"matcher_kind TEXT NOT NULL CHECK(length(matcher_kind)>0 AND length(matcher_kind)<=64),"
"matcher_version INTEGER NOT NULL CHECK(matcher_version>0),"
"parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32),"
"result_status INTEGER NOT NULL CHECK(result_status IN (0,1)),"
"match_count INTEGER NOT NULL CHECK(match_count>=0 AND match_count<=8192),"
"match_asset_sha256 BLOB CHECK(match_asset_sha256 IS NULL OR"
" length(match_asset_sha256)=32),"
"match_asset_path TEXT CHECK(match_asset_path IS NULL OR length(match_asset_path)>0),"
"match_asset_size_bytes INTEGER CHECK(match_asset_size_bytes IS NULL OR"
" match_asset_size_bytes>0),"
"created_at INTEGER NOT NULL CHECK(created_at>=0),"
"CHECK((result_status=0 AND match_count=0 AND match_asset_sha256 IS NULL AND "
"match_asset_path IS NULL AND match_asset_size_bytes IS NULL) OR "
"(result_status=1 AND match_count>0 AND match_asset_sha256 IS NOT NULL AND "
"match_asset_path IS NOT NULL AND match_asset_size_bytes IS NOT NULL)),"
"UNIQUE(candidate_pair_id,feature_set_id_a,feature_set_id_b,"
"matcher_kind,matcher_version,parameter_fingerprint));"
"CREATE INDEX match_results_candidate_pair_idx ON match_results(candidate_pair_id);"
"CREATE INDEX match_results_feature_set_a_idx ON match_results(feature_set_id_a);"
"CREATE INDEX match_results_feature_set_b_idx ON match_results(feature_set_id_b);";
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 : "");
@ -288,7 +315,7 @@ 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 != 8 && from_version != 9) {
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");
@ -499,6 +526,21 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
"finish schema v9 migration"); "finish schema v9 migration");
} }
} }
if (result == LARDON3D_PROJECT_DB_OK && from_version < 10) {
result = execute(database, schema_match_result_v10, "migrate schema v9 to v10");
#ifdef LARDON3D_PROJECT_DB_TESTING
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V10");
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 v10 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=10 WHERE key='schema_version' AND value=9",
"finish schema v10 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");
} }
@ -611,8 +653,9 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje
"feature_support_sets", "feature_support_sets",
"feature_support_groups", "feature_support_groups",
"feature_support_members", "feature_support_members",
"candidate_pairs"}; "candidate_pairs",
for (size_t index = 0; index < 16 && result == LARDON3D_PROJECT_DB_OK; ++index) { "match_results"};
for (size_t index = 0; index < 17 && result == LARDON3D_PROJECT_DB_OK; ++index) {
if (!table_exists(database->connection, required[index])) { if (!table_exists(database->connection, required[index])) {
copy_error(database->error, "Schéma v1 incomplet."); copy_error(database->error, "Schéma v1 incomplet.");
result = LARDON3D_PROJECT_DB_CORRUPT; result = LARDON3D_PROJECT_DB_CORRUPT;
@ -3180,6 +3223,409 @@ Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task(
return result; return result;
} }
static bool read_match_result(sqlite3_stmt *statement,
Lardon3DProjectDbMatchResult *result) {
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
sqlite3_int64 cp_id = sqlite3_column_int64(statement, 1);
sqlite3_int64 fs_a = sqlite3_column_int64(statement, 2);
sqlite3_int64 fs_b = sqlite3_column_int64(statement, 3);
const char *kind = (const char *)sqlite3_column_text(statement, 4);
sqlite3_int64 version = sqlite3_column_int64(statement, 5);
const void *fp = sqlite3_column_blob(statement, 6);
sqlite3_int64 status = sqlite3_column_int64(statement, 7);
sqlite3_int64 match_count = sqlite3_column_int64(statement, 8);
const void *asset_sha256 = sqlite3_column_blob(statement, 9);
int asset_sha256_bytes = sqlite3_column_bytes(statement, 9);
const char *asset_path = (const char *)sqlite3_column_text(statement, 10);
sqlite3_int64 asset_size = sqlite3_column_int64(statement, 11);
sqlite3_int64 created = sqlite3_column_int64(statement, 12);
bool has_sha = sqlite3_column_type(statement, 9) != SQLITE_NULL;
bool has_path = sqlite3_column_type(statement, 10) != SQLITE_NULL;
bool has_size = sqlite3_column_type(statement, 11) != SQLITE_NULL;
if (id <= 0 || cp_id <= 0 || fs_a <= 0 || fs_b <= 0 ||
!kind || version <= 0 || !fp || sqlite3_column_bytes(statement, 6) != 32 ||
status < 0 || status > 1 || match_count < 0 || match_count > 8192 || created < 0 ||
(status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH &&
(match_count != 0 || has_sha || has_path || has_size)) ||
(status == LARDON3D_MATCH_RESULT_STATUS_MATCHED &&
(match_count == 0 || !has_sha || !has_path || !has_size || asset_size <= 0))) {
return false;
}
result->match_result_id = (uint64_t)id;
result->candidate_pair_id = (uint64_t)cp_id;
result->feature_set_id_a = (uint64_t)fs_a;
result->feature_set_id_b = (uint64_t)fs_b;
size_t len = strlen(kind);
if (len == 0 || len >= LARDON3D_PROJECT_DB_KIND_CAPACITY) return false;
memcpy(result->matcher_kind, kind, len + 1);
result->matcher_version = (uint32_t)version;
memcpy(result->parameter_fingerprint, fp, 32);
result->result_status = (int)status;
result->match_count = (uint32_t)match_count;
/* Asset fields */
if (asset_sha256 && asset_sha256_bytes == LARDON3D_PROJECT_DB_SHA256_SIZE) {
result->has_match_asset = true;
memcpy(result->match_asset_sha256, asset_sha256, LARDON3D_PROJECT_DB_SHA256_SIZE);
} else {
result->has_match_asset = false;
memset(result->match_asset_sha256, 0, LARDON3D_PROJECT_DB_SHA256_SIZE);
}
if (asset_path) {
size_t plen = strlen(asset_path);
if (plen < LARDON3D_PROJECT_DB_PATH_CAPACITY) {
memcpy(result->match_asset_path, asset_path, plen + 1);
} else {
memcpy(result->match_asset_path, asset_path, LARDON3D_PROJECT_DB_PATH_CAPACITY - 1);
result->match_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY - 1] = '\0';
}
} else {
result->match_asset_path[0] = '\0';
}
result->match_asset_size_bytes = asset_size >= 0 ? (uint64_t)asset_size : 0;
result->created_at = created;
return true;
}
Lardon3DProjectDbResult lardon3d_project_db_create_match_result(
Lardon3DProjectDb *database, uint64_t candidate_pair_id, uint64_t feature_set_id_a,
uint64_t feature_set_id_b, const char *matcher_kind, uint32_t matcher_version,
const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE], int result_status,
uint32_t match_count,
const unsigned char *match_asset_sha256, const char *match_asset_path,
uint64_t match_asset_size_bytes, int64_t created_at,
Lardon3DProjectDbMatchResult *result) {
if (!database || !valid_catalog_id(candidate_pair_id) ||
!valid_catalog_id(feature_set_id_a) || !valid_catalog_id(feature_set_id_b) ||
!bounded_text(matcher_kind, LARDON3D_PROJECT_DB_KIND_CAPACITY, false) ||
matcher_version == 0 || !parameter_fingerprint || result_status < 0 || result_status > 1 ||
match_count > 8192 || created_at < 0 || !result ||
(result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH &&
(match_count != 0 || match_asset_sha256 || match_asset_path || match_asset_size_bytes != 0)) ||
(result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED &&
(match_count == 0 || !match_asset_sha256 || !match_asset_path ||
match_asset_size_bytes == 0))) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
/* Validate asset path if provided */
if (match_asset_path && match_asset_path[0] != '\0' &&
!bounded_text(match_asset_path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false)) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(result, 0, sizeof(*result));
(void)pthread_mutex_lock(&database->mutex);
/* Validate Feature Set → image ownership before INSERT */
sqlite3_stmt *check = NULL;
Lardon3DProjectDbResult db_result = LARDON3D_PROJECT_DB_OK;
sqlite3_int64 cp_image_a = 0, cp_image_b = 0;
sqlite3_int64 fs_a_image = 0, fs_b_image = 0;
/* Load candidate pair image_ids */
db_result = prepare(database,
"SELECT image_id_a,image_id_b FROM candidate_pairs WHERE "
"candidate_pair_id=?1",
&check);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(check, 1, (sqlite3_int64)candidate_pair_id);
int code = sqlite3_step(check);
if (code == SQLITE_DONE) {
db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} else if (code != SQLITE_ROW) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
cp_image_a = sqlite3_column_int64(check, 0);
cp_image_b = sqlite3_column_int64(check, 1);
}
(void)sqlite3_finalize(check);
check = NULL;
}
/* Verify feature_set_id_a belongs to image_id_a */
if (db_result == LARDON3D_PROJECT_DB_OK) {
db_result = prepare(database,
"SELECT image_id FROM feature_sets WHERE feature_set_id=?1", &check);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(check, 1, (sqlite3_int64)feature_set_id_a);
int code = sqlite3_step(check);
if (code == SQLITE_DONE) {
db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} else if (code != SQLITE_ROW) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
fs_a_image = sqlite3_column_int64(check, 0);
if (fs_a_image != cp_image_a) {
db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
}
(void)sqlite3_finalize(check);
check = NULL;
}
}
/* Verify feature_set_id_b belongs to image_id_b */
if (db_result == LARDON3D_PROJECT_DB_OK) {
db_result = prepare(database,
"SELECT image_id FROM feature_sets WHERE feature_set_id=?1", &check);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(check, 1, (sqlite3_int64)feature_set_id_b);
int code = sqlite3_step(check);
if (code == SQLITE_DONE) {
db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} else if (code != SQLITE_ROW) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
fs_b_image = sqlite3_column_int64(check, 0);
if (fs_b_image != cp_image_b) {
db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
}
(void)sqlite3_finalize(check);
check = NULL;
}
}
/* INSERT */
sqlite3_stmt *statement = NULL;
if (db_result == LARDON3D_PROJECT_DB_OK) {
db_result = prepare(
database,
"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,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
&statement);
}
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)candidate_pair_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)feature_set_id_a);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)feature_set_id_b);
(void)sqlite3_bind_text(statement, 4, matcher_kind, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 5, matcher_version);
(void)sqlite3_bind_blob(statement, 6, parameter_fingerprint, 32, SQLITE_TRANSIENT);
(void)sqlite3_bind_int(statement, 7, result_status);
(void)sqlite3_bind_int64(statement, 8, match_count);
if (match_asset_sha256) {
(void)sqlite3_bind_blob(statement, 9, match_asset_sha256,
LARDON3D_PROJECT_DB_SHA256_SIZE, SQLITE_TRANSIENT);
} else {
(void)sqlite3_bind_null(statement, 9);
}
if (match_asset_path && match_asset_path[0] != '\0') {
(void)sqlite3_bind_text(statement, 10, match_asset_path, -1, SQLITE_TRANSIENT);
} else {
(void)sqlite3_bind_null(statement, 10);
}
if (result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED) {
(void)sqlite3_bind_int64(statement, 11, (sqlite3_int64)match_asset_size_bytes);
} else {
(void)sqlite3_bind_null(statement, 11);
}
(void)sqlite3_bind_int64(statement, 12, created_at);
db_result = step_done(database, statement, "create match result");
}
sqlite3_int64 id = sqlite3_last_insert_rowid(database->connection);
if (db_result == LARDON3D_PROJECT_DB_OK && id <= 0) {
db_result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
if (db_result == LARDON3D_PROJECT_DB_OK) {
result->match_result_id = (uint64_t)id;
result->candidate_pair_id = candidate_pair_id;
result->feature_set_id_a = feature_set_id_a;
result->feature_set_id_b = feature_set_id_b;
(void)snprintf(result->matcher_kind, sizeof(result->matcher_kind), "%s", matcher_kind);
result->matcher_version = matcher_version;
memcpy(result->parameter_fingerprint, parameter_fingerprint, 32);
result->result_status = result_status;
result->match_count = match_count;
if (match_asset_sha256) {
result->has_match_asset = true;
memcpy(result->match_asset_sha256, match_asset_sha256, LARDON3D_PROJECT_DB_SHA256_SIZE);
} else {
result->has_match_asset = false;
memset(result->match_asset_sha256, 0, LARDON3D_PROJECT_DB_SHA256_SIZE);
}
if (match_asset_path && match_asset_path[0] != '\0') {
size_t plen = strlen(match_asset_path);
if (plen < LARDON3D_PROJECT_DB_PATH_CAPACITY) {
memcpy(result->match_asset_path, match_asset_path, plen + 1);
} else {
memcpy(result->match_asset_path, match_asset_path,
LARDON3D_PROJECT_DB_PATH_CAPACITY - 1);
result->match_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY - 1] = '\0';
}
} else {
result->match_asset_path[0] = '\0';
}
result->match_asset_size_bytes = match_asset_size_bytes;
result->created_at = created_at;
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_match_result(
Lardon3DProjectDb *database, uint64_t match_result_id,
Lardon3DProjectDbMatchResult *result) {
if (!database || !valid_catalog_id(match_result_id) || !result) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(result, 0, sizeof(*result));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult db_result = prepare(
database,
"SELECT "
"match_result_id,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 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 || !read_match_result(statement, result)) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
Lardon3DProjectDbResult lardon3d_project_db_repair_match_result(
Lardon3DProjectDb *database, uint64_t match_result_id, int result_status,
uint32_t match_count, const unsigned char *match_asset_sha256,
const char *match_asset_path, uint64_t match_asset_size_bytes,
Lardon3DProjectDbMatchResult *result) {
if (!database || !valid_catalog_id(match_result_id) || result_status < 0 ||
result_status > 1 || match_count > 8192 || !result ||
(result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH &&
(match_count != 0 || match_asset_sha256 || match_asset_path || match_asset_size_bytes != 0)) ||
(result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED &&
(match_count == 0 || !match_asset_sha256 || !match_asset_path ||
match_asset_size_bytes == 0)) ||
(match_asset_path &&
!bounded_text(match_asset_path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false))) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(result, 0, sizeof(*result));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult db_result = prepare(
database,
"UPDATE match_results SET result_status=?2,match_count=?3,match_asset_sha256=?4,"
"match_asset_path=?5,match_asset_size_bytes=?6 WHERE match_result_id=?1",
&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, result_status);
(void)sqlite3_bind_int64(statement, 3, match_count);
if (match_asset_sha256) {
(void)sqlite3_bind_blob(statement, 4, match_asset_sha256, 32, SQLITE_TRANSIENT);
(void)sqlite3_bind_text(statement, 5, match_asset_path, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 6, (sqlite3_int64)match_asset_size_bytes);
} else {
(void)sqlite3_bind_null(statement, 4);
(void)sqlite3_bind_null(statement, 5);
(void)sqlite3_bind_null(statement, 6);
}
db_result = step_done(database, statement, "repair match result");
}
if (db_result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
db_result = LARDON3D_PROJECT_DB_NOT_FOUND;
}
(void)pthread_mutex_unlock(&database->mutex);
if (db_result != LARDON3D_PROJECT_DB_OK) return db_result;
return lardon3d_project_db_load_match_result(database, match_result_id, result);
}
Lardon3DProjectDbResult lardon3d_project_db_find_match_result(
Lardon3DProjectDb *database, uint64_t candidate_pair_id,
uint64_t feature_set_id_a, uint64_t feature_set_id_b,
const char *matcher_kind, uint32_t matcher_version,
const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE],
Lardon3DProjectDbMatchResult *result) {
if (!database || !valid_catalog_id(candidate_pair_id) ||
!valid_catalog_id(feature_set_id_a) || !valid_catalog_id(feature_set_id_b) ||
!bounded_text(matcher_kind, LARDON3D_PROJECT_DB_KIND_CAPACITY, false) ||
matcher_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;
Lardon3DProjectDbResult db_result = prepare(
database,
"SELECT "
"match_result_id,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 FROM match_results WHERE candidate_pair_id=?1 AND feature_set_id_a=?2 AND "
"feature_set_id_b=?3 AND matcher_kind=?4 AND matcher_version=?5 AND "
"parameter_fingerprint=?6",
&statement);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)candidate_pair_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)feature_set_id_a);
(void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)feature_set_id_b);
(void)sqlite3_bind_text(statement, 4, matcher_kind, -1, SQLITE_TRANSIENT);
(void)sqlite3_bind_int64(statement, 5, matcher_version);
(void)sqlite3_bind_blob(statement, 6, parameter_fingerprint, 32, SQLITE_TRANSIENT);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) {
db_result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (code != SQLITE_ROW || !read_match_result(statement, result)) {
db_result = LARDON3D_PROJECT_DB_CORRUPT;
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
Lardon3DProjectDbResult lardon3d_project_db_list_match_results(
Lardon3DProjectDb *database, uint64_t after_match_result_id,
Lardon3DProjectDbMatchResult *results, size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!database || !results || !count || after_match_result_id > INT64_MAX || capacity == 0 ||
capacity > LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult db_result = prepare(
database,
"SELECT "
"match_result_id,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 FROM match_results WHERE match_result_id>?1 ORDER BY match_result_id LIMIT ?2",
&statement);
if (db_result == LARDON3D_PROJECT_DB_OK) {
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)after_match_result_id);
(void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)capacity);
int code = SQLITE_DONE;
while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) {
if (!read_match_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 match results");
}
(void)sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return db_result;
}
#ifdef LARDON3D_PROJECT_DB_TESTING #ifdef LARDON3D_PROJECT_DB_TESTING
Lardon3DProjectDbResult lardon3d_project_db_test_delete_feature_identity( Lardon3DProjectDbResult lardon3d_project_db_test_delete_feature_identity(
Lardon3DProjectDb *database, uint64_t feature_set_id, uint64_t feature_asset_id) { Lardon3DProjectDb *database, uint64_t feature_set_id, uint64_t feature_asset_id) {

201
tests/benchmark_matcher.cpp Normal file
View file

@ -0,0 +1,201 @@
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <string>
#include <vector>
extern "C" {
#include <lardon3d/app_state.h>
#include <lardon3d/feature_store.h>
#include <lardon3d/matcher.h>
#include <lardon3d/project_db.h>
}
struct Sample {
uint64_t open_ns;
uint64_t read_ns;
uint64_t knn_ns;
uint64_t filter_ns;
uint64_t canonicalize_ns;
uint64_t serialize_ns;
uint64_t sha_ns;
uint64_t publish_ns;
uint64_t database_ns;
uint64_t total_ns;
};
static uint64_t median(std::vector<uint64_t> values) {
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
static void image_path(const unsigned char hash[32], char path[4096]) {
static const char digits[] = "0123456789abcdef";
char hex[65];
for (size_t i = 0; i < 32; ++i) {
hex[2 * i] = digits[hash[i] >> 4];
hex[2 * i + 1] = digits[hash[i] & 15U];
}
hex[64] = '\0';
(void)snprintf(path, 4096, "assets/images/%c%c/%s", hex[0], hex[1], hex);
}
static bool register_image(Lardon3DProjectDb *db, uint64_t scanset_id, unsigned char seed,
Lardon3DProjectDbImage *image) {
unsigned char hash[32];
memset(hash, seed, sizeof(hash));
char path[4096];
image_path(hash, path);
Lardon3DProjectDbImageRegisterStatus status;
return lardon3d_project_db_register_image(db, scanset_id, hash, path, 1, "fixture.bin",
"/synthetic/fixture.bin", 0, seed, &status,
image) == LARDON3D_PROJECT_DB_OK;
}
static void fill_features(Lardon3DExtractedFeatures *features, uint32_t count,
Lardon3DFeatureDescriptorType type, unsigned char salt) {
(void)salt;
memset(features, 0, sizeof(*features));
features->image_width = 4096;
features->image_height = 4096;
features->feature_count = count;
features->keypoints = static_cast<Lardon3DFeatureKeypoint *>(
calloc(count == 0 ? 1 : count, sizeof(*features->keypoints)));
for (uint32_t i = 0; i < count; ++i) {
features->keypoints[i].x = (float)(i % 4096U);
features->keypoints[i].y = (float)(i % 4096U);
features->keypoints[i].size = 1.0F;
}
size_t scalar = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 1 : sizeof(float);
features->descriptor_bytes = (size_t)count * 128 * scalar;
features->descriptors = static_cast<unsigned char *>(malloc(features->descriptor_bytes));
if (type == LARDON3D_FEATURE_DESCRIPTOR_U8) {
features->descriptor_bytes = (size_t)count * 32;
features->descriptors = static_cast<unsigned char *>(
realloc(features->descriptors, features->descriptor_bytes));
for (uint32_t row = 0; row < count; ++row) {
uint32_t value = row + 1U;
for (uint32_t column = 0; column < 32; ++column) {
value ^= value << 13;
value ^= value >> 17;
value ^= value << 5;
features->descriptors[(size_t)row * 32 + column] = (unsigned char)value;
}
}
} else {
float *values = reinterpret_cast<float *>(features->descriptors);
size_t value_count = (size_t)count * 128;
for (size_t i = 0; i < value_count; ++i)
values[i] = (float)(((i / 128U) * 37U + (i % 128U) * 11U) % 65521U) / 65521.0F;
}
}
static bool publish(Lardon3DAppState *state, uint64_t image_id, const char *kind,
Lardon3DFeatureDescriptorType type, uint32_t count, unsigned char salt,
Lardon3DProjectDbFeatureSet *set) {
Lardon3DExtractedFeatures features;
fill_features(&features, count, type, salt);
unsigned char fingerprint[32];
memset(fingerprint, salt, sizeof(fingerprint));
uint32_t dimension = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 32 : 128;
Lardon3DFeatureStoreResult result = lardon3d_feature_store_publish_v2(
state, image_id, 0, kind, 1, fingerprint, type, dimension, 0, &features, set);
free(features.keypoints);
free(features.descriptors);
return result == LARDON3D_FEATURE_STORE_OK;
}
static bool benchmark_case(const char *kind, Lardon3DMatcherKind matcher_kind,
Lardon3DFeatureDescriptorType type, uint32_t count) {
char root_template[] = "/tmp/lardon3d-matcher-bench-XXXXXX";
char *root = mkdtemp(root_template);
if (!root) return false;
std::string db_path = std::string(root) + "/project.db";
Lardon3DProjectDb *db = nullptr;
char error[256];
if (lardon3d_project_db_open(db_path.c_str(), &db, error) != LARDON3D_PROJECT_DB_OK)
return false;
Lardon3DProjectDbScanSet scanset;
Lardon3DProjectDbImage image_a, image_b;
Lardon3DAppState state;
lardon3d_app_state_init(&state);
state.project_loaded = true;
state.project_db = db;
(void)snprintf(state.project_path, sizeof(state.project_path), "%s", root);
bool ok = lardon3d_project_db_create_scanset(db, "benchmark", &scanset) ==
LARDON3D_PROJECT_DB_OK &&
register_image(db, scanset.scanset_id, 1, &image_a) &&
register_image(db, scanset.scanset_id, 2, &image_b);
Lardon3DProjectDbFeatureSet set_a, set_b;
ok = ok && publish(&state, image_a.image_id, kind, type, count, 3, &set_a) &&
publish(&state, image_b.image_id, kind, type, count, 4, &set_b);
Lardon3DProjectDbCandidatePair pair;
ok = ok && lardon3d_project_db_create_candidate_pair(
db, image_a.image_id, image_b.image_id, 1, &pair) == LARDON3D_PROJECT_DB_OK;
std::vector<Sample> samples;
Lardon3DMatcherParams params = {matcher_kind, lardon3d_matcher_default_ratio(matcher_kind)};
for (int repetition = 0; ok && repetition < 3; ++repetition) {
Lardon3DMatcherStats stats;
params.ratio_threshold = lardon3d_matcher_default_ratio(matcher_kind) -
(float)repetition * 0.01F;
Lardon3DProjectDbMatchResult result;
ok = lardon3d_matcher_match_and_publish_profiled(
root, db, &pair, &set_a, &set_b, &params, &result, &stats) == LARDON3D_MATCHER_OK;
samples.push_back({stats.feature_open_ns, stats.descriptor_read_ns, stats.knn_ns,
stats.filter_ns, stats.canonicalize_ns, stats.serialize_ns,
stats.sha256_ns, stats.publication_ns, stats.database_ns,
stats.total_ns});
}
if (ok) {
std::vector<uint64_t> values;
auto field = [&](auto getter) {
values.clear();
for (const Sample &sample : samples) values.push_back(getter(sample));
return (double)median(values) / 1000000.0;
};
printf("%s,%u,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f\n", kind, count,
field([](const Sample &s) { return s.open_ns; }),
field([](const Sample &s) { return s.read_ns; }),
field([](const Sample &s) { return s.knn_ns; }),
field([](const Sample &s) { return s.filter_ns; }),
field([](const Sample &s) { return s.canonicalize_ns; }),
field([](const Sample &s) { return s.serialize_ns; }),
field([](const Sample &s) { return s.sha_ns; }),
field([](const Sample &s) { return s.publish_ns; }),
field([](const Sample &s) { return s.database_ns; }),
field([](const Sample &s) { return s.total_ns; }));
std::vector<uint64_t> reuse_totals;
for (int repetition = 0; repetition < 5; ++repetition) {
Lardon3DMatcherStats reuse_stats;
Lardon3DProjectDbMatchResult reused;
ok = ok && lardon3d_matcher_match_and_publish_profiled(
root, db, &pair, &set_a, &set_b, &params, &reused, &reuse_stats) ==
LARDON3D_MATCHER_OK;
reuse_totals.push_back(reuse_stats.total_ns);
}
printf("%s-reuse,%u,0,0,0,0,0,0,0,0,0,%.3f\n", kind, count,
(double)median(reuse_totals) / 1000000.0);
}
lardon3d_project_db_close(db);
std::filesystem::remove_all(root);
return ok;
}
int main(void) {
printf("kind,count,open_ms,read_ms,knn_ms,filter_ms,canonicalize_ms,serialize_ms,"
"sha_ms,publish_ms,db_ms,total_ms\n");
const uint32_t sizes[] = {64, 256, 1024, 4096, 8192};
for (uint32_t count : sizes) {
if (!benchmark_case("orb", LARDON3D_MATCHER_ORB_BF,
LARDON3D_FEATURE_DESCRIPTOR_U8, count) ||
!benchmark_case("sift", LARDON3D_MATCHER_SIFT_BF,
LARDON3D_FEATURE_DESCRIPTOR_F32, count) ||
!benchmark_case("rootsift", LARDON3D_MATCHER_ROOTSIFT_BF,
LARDON3D_FEATURE_DESCRIPTOR_F32, count))
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

526
tests/test_match_result.c Normal file
View file

@ -0,0 +1,526 @@
#include <errno.h>
#include <limits.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/project_db.h>
#define CHECK(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \
return false; \
} \
} while (0)
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 bool create_v9_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);
sqlite3 *connection = NULL;
if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
static const char sql[] =
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE match_results;"
"UPDATE metadata SET value=9 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;";
bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK;
return sqlite3_close(connection) == SQLITE_OK && ok;
}
static void asset_path_for_hash(const unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE],
char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]) {
static const char digits[] = "0123456789abcdef";
char hex[65];
for (size_t i = 0; i < LARDON3D_PROJECT_DB_SHA256_SIZE; ++i) {
hex[i * 2] = digits[hash[i] >> 4];
hex[i * 2 + 1] = digits[hash[i] & 15U];
}
hex[64] = '\0';
(void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, "assets/images/%c%c/%s", hex[0], hex[1],
hex);
}
static void feature_asset_path_for_hash(
const unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE],
char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]) {
static const char digits[] = "0123456789abcdef";
char hex[65];
for (size_t i = 0; i < LARDON3D_PROJECT_DB_SHA256_SIZE; ++i) {
hex[i * 2] = digits[hash[i] >> 4];
hex[i * 2 + 1] = digits[hash[i] & 15U];
}
hex[64] = '\0';
(void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, "assets/features/%c%c/%s", hex[0], hex[1],
hex);
}
static bool run_test(void) {
char directory[] = "/tmp/lardon3d-match-result-XXXXXX";
CHECK(mkdtemp(directory));
char database_path[512], v9_path[512], failed_v10_path[512];
CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0);
CHECK(snprintf(v9_path, sizeof(v9_path), "%s/v9.db", directory) > 0);
CHECK(snprintf(failed_v10_path, sizeof(failed_v10_path), "%s/failed-v10.db", directory) > 0);
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(database && lardon3d_project_db_schema_version(database) == 10);
Lardon3DProjectDbScanSet scanset;
CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) ==
LARDON3D_PROJECT_DB_OK);
unsigned char hash_a[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0xAA};
char path_a[LARDON3D_PROJECT_DB_PATH_CAPACITY];
asset_path_for_hash(hash_a, path_a);
Lardon3DProjectDbImageRegisterStatus identity_status;
Lardon3DProjectDbImage image_a;
CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_a, path_a, 1,
"a.jpg", "/src/a.jpg", 0, 1, &identity_status,
&image_a) == LARDON3D_PROJECT_DB_OK);
unsigned char hash_b[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0xBB};
char path_b[LARDON3D_PROJECT_DB_PATH_CAPACITY];
asset_path_for_hash(hash_b, path_b);
Lardon3DProjectDbImage image_b;
CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_b, path_b, 1,
"b.jpg", "/src/b.jpg", 0, 2, &identity_status,
&image_b) == LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbCandidatePair pair;
CHECK(lardon3d_project_db_create_candidate_pair(database, image_a.image_id, image_b.image_id, 10,
&pair) == LARDON3D_PROJECT_DB_OK);
CHECK(pair.candidate_pair_id > 0);
/* Create feature sets for foreign key references */
unsigned char feature_hash_a[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x11};
char feature_path_a[LARDON3D_PROJECT_DB_PATH_CAPACITY];
feature_asset_path_for_hash(feature_hash_a, feature_path_a);
Lardon3DProjectDbFeatureSet fs_a;
CHECK(lardon3d_project_db_register_feature_set(
database, image_a.image_id, "orb", 1, feature_hash_a, hash_a, 100, 1, 32,
feature_hash_a, feature_path_a, 128, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 10,
&fs_a) == LARDON3D_PROJECT_DB_OK);
CHECK(fs_a.feature_set_id > 0 && fs_a.image_id == image_a.image_id);
unsigned char feature_hash_b[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x22};
char feature_path_b[LARDON3D_PROJECT_DB_PATH_CAPACITY];
feature_asset_path_for_hash(feature_hash_b, feature_path_b);
Lardon3DProjectDbFeatureSet fs_b;
CHECK(lardon3d_project_db_register_feature_set(
database, image_b.image_id, "sift", 1, feature_hash_b, hash_b, 100, 2, 128,
feature_hash_b, feature_path_b, 256,
LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE, 0, 20, &fs_b) ==
LARDON3D_PROJECT_DB_OK);
CHECK(fs_b.feature_set_id > 0 && fs_b.image_id == image_b.image_id);
/* Create MATCHED result with inliers */
unsigned char fp1[32];
memset(fp1, 0x01, 32);
Lardon3DProjectDbMatchResult result;
unsigned char match_hash[32];
memset(match_hash, 0x5A, sizeof(match_hash));
const char *match_path = "assets/matches/5a/asset";
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id,
"orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30,
match_hash, match_path, 392, 100,
&result) == LARDON3D_PROJECT_DB_OK);
CHECK(result.match_result_id > 0 && result.candidate_pair_id == pair.candidate_pair_id &&
result.feature_set_id_a == fs_a.feature_set_id &&
result.feature_set_id_b == fs_b.feature_set_id &&
strcmp(result.matcher_kind, "orb_bf") == 0 && result.matcher_version == 1 &&
memcmp(result.parameter_fingerprint, fp1, 32) == 0 &&
result.result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED &&
result.match_count == 30 && result.created_at == 100);
uint64_t first_id = result.match_result_id;
/* Create NO_MATCH result (0 inliers) */
unsigned char fp2[32];
memset(fp2, 0x02, 32);
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id,
"orb_bf", 2, fp2, LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0,
NULL, NULL, 0, 200,
&result) == LARDON3D_PROJECT_DB_OK);
CHECK(result.result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH &&
result.match_count == 0 && !result.has_match_asset);
uint64_t second_id = result.match_result_id;
/* Duplicate: same CP + same matcher + same params + same FS → CONSTRAINT */
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id,
"orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 10,
match_hash, match_path, 152, 500,
&result) == LARDON3D_PROJECT_DB_CONSTRAINT);
/* Create additional Feature Sets for distinct-FS and inverted-ID tests */
unsigned char feature_hash_a2[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x33};
char feature_path_a2[LARDON3D_PROJECT_DB_PATH_CAPACITY];
feature_asset_path_for_hash(feature_hash_a2, feature_path_a2);
Lardon3DProjectDbFeatureSet fs_a2;
unsigned char fs_a2_fp[32];
memset(fs_a2_fp, 0xAA, 32);
CHECK(lardon3d_project_db_register_feature_set(
database, image_a.image_id, "orb", 2, fs_a2_fp, hash_a, 200, 1, 32,
feature_hash_a2, feature_path_a2, 256, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 30,
&fs_a2) == LARDON3D_PROJECT_DB_OK);
CHECK(fs_a2.feature_set_id > 0 && fs_a2.image_id == image_a.image_id);
unsigned char feature_hash_b2[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x44};
char feature_path_b2[LARDON3D_PROJECT_DB_PATH_CAPACITY];
feature_asset_path_for_hash(feature_hash_b2, feature_path_b2);
Lardon3DProjectDbFeatureSet fs_b2;
unsigned char fs_b2_fp[32];
memset(fs_b2_fp, 0xBB, 32);
CHECK(lardon3d_project_db_register_feature_set(
database, image_b.image_id, "sift", 2, fs_b2_fp, hash_b, 200, 2, 128,
feature_hash_b2, feature_path_b2, 512,
LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE, 0, 40, &fs_b2) ==
LARDON3D_PROJECT_DB_OK);
CHECK(fs_b2.feature_set_id > 0 && fs_b2.image_id == image_b.image_id);
/* Same CP + same matcher + same params + different FS → distinct */
unsigned char fp5[32];
memset(fp5, 0x05, 32);
Lardon3DProjectDbMatchResult distinct_fs_result;
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a2.feature_set_id, fs_b2.feature_set_id,
"orb_bf", 1, fp5, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 20,
match_hash, match_path, 272, 400,
&distinct_fs_result) == LARDON3D_PROJECT_DB_OK);
CHECK(distinct_fs_result.feature_set_id_a == fs_a2.feature_set_id &&
distinct_fs_result.feature_set_id_b == fs_b2.feature_set_id);
uint64_t distinct_id = distinct_fs_result.match_result_id;
CHECK(distinct_id != first_id);
/* feature_set_id_a > feature_set_id_b but correct ownership → valid */
/* Register image_c, image_d; create FS for d first, then c → fs_c.id > fs_d.id */
unsigned char hash_c[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0xCC};
char path_c[LARDON3D_PROJECT_DB_PATH_CAPACITY];
asset_path_for_hash(hash_c, path_c);
Lardon3DProjectDbImage image_c;
CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_c, path_c, 1,
"c.jpg", "/src/c.jpg", 0, 3, &identity_status,
&image_c) == LARDON3D_PROJECT_DB_OK);
unsigned char hash_d[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0xDD};
char path_d[LARDON3D_PROJECT_DB_PATH_CAPACITY];
asset_path_for_hash(hash_d, path_d);
Lardon3DProjectDbImage image_d;
CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_d, path_d, 1,
"d.jpg", "/src/d.jpg", 0, 4, &identity_status,
&image_d) == LARDON3D_PROJECT_DB_OK);
/* Create feature set for image_d first (lower ID), then image_c (higher ID) */
unsigned char feature_hash_d[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x55};
char feature_path_d[LARDON3D_PROJECT_DB_PATH_CAPACITY];
feature_asset_path_for_hash(feature_hash_d, feature_path_d);
Lardon3DProjectDbFeatureSet fs_d;
CHECK(lardon3d_project_db_register_feature_set(
database, image_d.image_id, "orb", 1, feature_hash_d, hash_d, 100, 1, 32,
feature_hash_d, feature_path_d, 128, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 50,
&fs_d) == LARDON3D_PROJECT_DB_OK);
unsigned char feature_hash_c[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x66};
char feature_path_c[LARDON3D_PROJECT_DB_PATH_CAPACITY];
feature_asset_path_for_hash(feature_hash_c, feature_path_c);
Lardon3DProjectDbFeatureSet fs_c;
CHECK(lardon3d_project_db_register_feature_set(
database, image_c.image_id, "sift", 1, feature_hash_c, hash_c, 100, 2, 128,
feature_hash_c, feature_path_c, 256,
LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE, 0, 60, &fs_c) ==
LARDON3D_PROJECT_DB_OK);
/* fs_c was created after fs_d → fs_c.feature_set_id > fs_d.feature_set_id */
CHECK(fs_c.feature_set_id > fs_d.feature_set_id);
/* CP (image_c, image_d): image_c should come first since registered first */
Lardon3DProjectDbCandidatePair pair_cd;
CHECK(lardon3d_project_db_create_candidate_pair(database, image_c.image_id, image_d.image_id, 20,
&pair_cd) == LARDON3D_PROJECT_DB_OK);
unsigned char fp6[32];
memset(fp6, 0x06, 32);
Lardon3DProjectDbMatchResult inverted_fs_result;
CHECK(lardon3d_project_db_create_match_result(
database, pair_cd.candidate_pair_id, fs_c.feature_set_id, fs_d.feature_set_id,
"orb_bf", 1, fp6, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 15,
match_hash, match_path, 212, 500,
&inverted_fs_result) == LARDON3D_PROJECT_DB_OK);
CHECK(inverted_fs_result.feature_set_id_a == fs_c.feature_set_id &&
inverted_fs_result.feature_set_id_b == fs_d.feature_set_id);
CHECK(fs_c.feature_set_id > fs_d.feature_set_id);
/* FeatureSet A belonging to image B → rejected (INVALID_ARGUMENT) */
unsigned char fp7[32];
memset(fp7, 0x07, 32);
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_b.feature_set_id, fs_a.feature_set_id,
"orb_bf", 1, fp7, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 5,
match_hash, match_path, 92, 600,
&result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
/* FeatureSet B belonging to image A (not image B) → rejected */
unsigned char fp8[32];
memset(fp8, 0x08, 32);
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_a2.feature_set_id,
"orb_bf", 1, fp8, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 5,
match_hash, match_path, 92, 700,
&result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
/* Load by ID */
Lardon3DProjectDbMatchResult loaded;
CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) ==
LARDON3D_PROJECT_DB_OK);
CHECK(loaded.match_result_id == first_id && loaded.result_status ==
LARDON3D_MATCH_RESULT_STATUS_MATCHED && loaded.match_count == 30);
/* Load not found */
CHECK(lardon3d_project_db_load_match_result(database, 999999, &loaded) ==
LARDON3D_PROJECT_DB_NOT_FOUND);
/* Find by full 6-part identity */
Lardon3DProjectDbMatchResult found;
CHECK(lardon3d_project_db_find_match_result(database, pair.candidate_pair_id,
fs_a.feature_set_id, fs_b.feature_set_id,
"orb_bf", 1, fp1,
&found) == LARDON3D_PROJECT_DB_OK);
CHECK(found.match_result_id == first_id && found.match_count == 30);
/* Find with wrong feature_set_id → NOT_FOUND */
CHECK(lardon3d_project_db_find_match_result(database, pair.candidate_pair_id,
fs_a2.feature_set_id, fs_b.feature_set_id,
"orb_bf", 1, fp1,
&found) == LARDON3D_PROJECT_DB_NOT_FOUND);
/* Find not found */
unsigned char unknown_fp[32];
memset(unknown_fp, 0xFF, 32);
CHECK(lardon3d_project_db_find_match_result(database, pair.candidate_pair_id,
fs_a.feature_set_id, fs_b.feature_set_id,
"orb_bf", 1, unknown_fp,
&found) == LARDON3D_PROJECT_DB_NOT_FOUND);
/* Find with wrong candidate_pair */
CHECK(lardon3d_project_db_find_match_result(database, 999999,
fs_a.feature_set_id, fs_b.feature_set_id,
"orb_bf", 1, fp1,
&found) == LARDON3D_PROJECT_DB_NOT_FOUND);
/* Pagination */
Lardon3DProjectDbMatchResult page[LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX];
size_t page_count = 0;
CHECK(lardon3d_project_db_list_match_results(database, 0, page, 4, &page_count) ==
LARDON3D_PROJECT_DB_OK);
CHECK(page_count == 4 && page[0].match_result_id == first_id &&
page[1].match_result_id == second_id && page[2].match_result_id == distinct_id);
/* Cursor pagination */
page_count = 0;
CHECK(lardon3d_project_db_list_match_results(database, first_id, page, 2, &page_count) ==
LARDON3D_PROJECT_DB_OK);
CHECK(page_count == 2 && page[0].match_result_id == second_id &&
page[1].match_result_id == distinct_id);
/* End of list */
page_count = 0;
CHECK(lardon3d_project_db_list_match_results(database, distinct_id, page, 1, &page_count) ==
LARDON3D_PROJECT_DB_OK);
CHECK(page_count == 1 && page[0].match_result_id > distinct_id);
/* Capacity bound (PAGE_MAX) */
CHECK(lardon3d_project_db_list_match_results(
database, 0, page, LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX + 1,
&page_count) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
/* Invalid arguments: NULL database */
CHECK(lardon3d_project_db_create_match_result(NULL, pair.candidate_pair_id,
fs_a.feature_set_id, fs_b.feature_set_id, "orb_bf", 1, fp1,
LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30,
match_hash, match_path, 392, 100,
&result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
/* Invalid arguments: 0 ids */
CHECK(lardon3d_project_db_create_match_result(
database, 0, fs_a.feature_set_id, fs_b.feature_set_id, "orb_bf", 1, fp1,
LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30,
match_hash, match_path, 392, 100, &result) ==
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, 0, fs_b.feature_set_id, "orb_bf", 1, fp1,
LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30,
match_hash, match_path, 392, 100, &result) ==
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
/* Invalid arguments: NULL output */
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id,
"orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30,
match_hash, match_path, 392, 100, NULL) ==
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
/* Invalid arguments: empty matcher_kind */
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id,
"", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30,
match_hash, match_path, 392, 100, &result) ==
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
/* Invalid arguments: 0 matcher_version */
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id,
"orb_bf", 0, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30,
match_hash, match_path, 392, 100, &result) ==
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
/* Invalid arguments: NULL fingerprint */
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id,
"orb_bf", 1, NULL, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30,
match_hash, match_path, 392, 100, &result) ==
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
/* Invalid arguments: bad status */
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id,
"orb_bf", 1, fp1, 2, 30,
match_hash, match_path, 392, 100, &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
/* Invalid result/asset combinations */
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id,
"orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 10,
NULL, NULL, 0, 100, &result) ==
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id,
"bad-no-match", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0,
match_hash, match_path, 32, 100, &result) ==
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id,
"too-many", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 8193,
match_hash, match_path, 98348, 100, &result) ==
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
/* Invalid arguments: negative created_at */
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id,
"orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30,
match_hash, match_path, 392, -1, &result) ==
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
/* Status boundary values */
CHECK(lardon3d_project_db_create_match_result(
database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id,
"boundary", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0,
NULL, NULL, 0, 500, &result) ==
LARDON3D_PROJECT_DB_OK);
/* Close/reopen persistence */
lardon3d_project_db_close(database);
database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 10);
/* Verify persistence: load previously created results */
CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) ==
LARDON3D_PROJECT_DB_OK);
CHECK(loaded.match_result_id == first_id && loaded.result_status ==
LARDON3D_MATCH_RESULT_STATUS_MATCHED &&
strcmp(loaded.matcher_kind, "orb_bf") == 0 && loaded.match_count == 30);
/* Verify persistence: find by key */
CHECK(lardon3d_project_db_find_match_result(database, pair.candidate_pair_id,
fs_a.feature_set_id, fs_b.feature_set_id,
"orb_bf", 1, fp1,
&found) == LARDON3D_PROJECT_DB_OK);
CHECK(found.match_result_id == first_id);
/* Verify persistence of distinct result */
Lardon3DProjectDbMatchResult distinct_loaded;
CHECK(lardon3d_project_db_load_match_result(database, distinct_id, &distinct_loaded) ==
LARDON3D_PROJECT_DB_OK);
CHECK(distinct_loaded.match_result_id == distinct_id &&
distinct_loaded.feature_set_id_a == fs_a2.feature_set_id &&
distinct_loaded.feature_set_id_b == fs_b2.feature_set_id);
CHECK(distinct_id != first_id);
/* Verify inverted result persisted */
Lardon3DProjectDbMatchResult inverted_loaded;
CHECK(lardon3d_project_db_load_match_result(database, inverted_fs_result.match_result_id,
&inverted_loaded) == LARDON3D_PROJECT_DB_OK);
CHECK(inverted_loaded.feature_set_id_a == fs_c.feature_set_id &&
inverted_loaded.feature_set_id_b == fs_d.feature_set_id);
CHECK(inverted_loaded.feature_set_id_a > inverted_loaded.feature_set_id_b);
lardon3d_project_db_close(database);
database = NULL;
/* Migration from v9 */
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) == 10);
lardon3d_project_db_close(database);
database = NULL;
CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 10));
CHECK(query_integer(v9_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='match_results'", 1));
/* Forced migration failure v10 */
CHECK(create_v9_database(failed_v10_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V10", "1", 1) == 0);
CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) ==
LARDON3D_PROJECT_DB_IO_ERROR);
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V10") == 0);
CHECK(query_integer(failed_v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 9));
CHECK(query_integer(failed_v10_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='match_results'", 0));
CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) == LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 10);
lardon3d_project_db_close(database);
database = NULL;
CHECK(unlink(database_path) == 0);
CHECK(unlink(v9_path) == 0);
CHECK(unlink(failed_v10_path) == 0);
CHECK(rmdir(directory) == 0);
return true;
}
int main(void) {
return run_test() ? EXIT_SUCCESS : EXIT_FAILURE;
}

518
tests/test_matcher.c Normal file
View file

@ -0,0 +1,518 @@
#include <errno.h>
#include <fcntl.h>
#include <math.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/match_file.h>
#include <lardon3d/matcher.h>
#define CHECK(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \
return false; \
} \
} while (0)
/* --- Match File tests --- */
static bool test_match_file_write_read(void) {
char dir[] = "/tmp/lardon3d-mf-XXXXXX";
CHECK(mkdtemp(dir));
char path[512];
CHECK(snprintf(path, sizeof(path), "%s/test.mf", dir) > 0);
Lardon3DMatchFileEntry entries[3];
entries[0].feature_index_a = 0;
entries[0].feature_index_b = 0;
entries[0].distance = 0.0F;
entries[1].feature_index_a = 5;
entries[1].feature_index_b = 3;
entries[1].distance = 3.0F;
entries[2].feature_index_a = 10;
entries[2].feature_index_b = 20;
entries[2].distance = 12.5F;
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
CHECK(fd >= 0);
CHECK(lardon3d_match_file_write(fd, 1, 32, 1, 2, entries, 3) ==
LARDON3D_MATCH_FILE_OK);
(void)close(fd);
Lardon3DMatchFileHeader header;
CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 8192, 8192) ==
LARDON3D_MATCH_FILE_OK);
CHECK(memcmp(header.magic, LARDON3D_MATCH_FILE_MAGIC, 4) == 0);
CHECK(header.format_version == 1);
CHECK(header.descriptor_type == 1);
CHECK(header.descriptor_dimension == 32);
CHECK(header.match_count == 3);
CHECK(header.feature_set_id_a == 1 && header.feature_set_id_b == 2);
fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
CHECK(fd >= 0);
unsigned char physical_magic[4];
CHECK(read(fd, physical_magic, sizeof(physical_magic)) == (ssize_t)sizeof(physical_magic));
CHECK(memcmp(physical_magic, "L3DM", 4) == 0);
CHECK(lseek(fd, 0, SEEK_SET) == 0);
Lardon3DMatchFileEntry read_entries[10];
uint32_t count = 0;
CHECK(lardon3d_match_file_read(fd, &header, read_entries, 10, &count, 1, 2, 8192, 8192) ==
LARDON3D_MATCH_FILE_OK);
(void)close(fd);
CHECK(count == 3);
CHECK(read_entries[0].feature_index_a == 0);
CHECK(read_entries[2].feature_index_a == 10);
CHECK(read_entries[2].feature_index_b == 20);
CHECK(fabsf(read_entries[2].distance - 12.5F) < 0.001F);
CHECK(lardon3d_match_file_validate(path, &header, 9, 2, 8192, 8192) ==
LARDON3D_MATCH_FILE_CORRUPT);
CHECK(lardon3d_match_file_validate(path, &header, 1, 9, 8192, 8192) ==
LARDON3D_MATCH_FILE_CORRUPT);
CHECK(lardon3d_match_file_validate(path, &header, 2, 1, 8192, 8192) ==
LARDON3D_MATCH_FILE_CORRUPT);
CHECK(unlink(path) == 0);
CHECK(rmdir(dir) == 0);
return true;
}
static bool test_match_file_empty(void) {
char dir[] = "/tmp/lardon3d-me-XXXXXX";
CHECK(mkdtemp(dir));
char path[512];
CHECK(snprintf(path, sizeof(path), "%s/empty.mf", dir) > 0);
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
CHECK(fd >= 0);
CHECK(lardon3d_match_file_write(fd, 1, 32, 1, 2, NULL, 0) ==
LARDON3D_MATCH_FILE_OK);
(void)close(fd);
Lardon3DMatchFileHeader header;
CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 0, 0) ==
LARDON3D_MATCH_FILE_OK);
CHECK(header.match_count == 0);
fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
CHECK(fd >= 0);
Lardon3DMatchFileEntry entries[1];
uint32_t count = 0;
CHECK(lardon3d_match_file_read(fd, &header, entries, 1, &count, 1, 2, 0, 0) ==
LARDON3D_MATCH_FILE_OK);
(void)close(fd);
CHECK(count == 0);
CHECK(unlink(path) == 0);
CHECK(rmdir(dir) == 0);
return true;
}
static bool test_match_file_bad_magic(void) {
char dir[] = "/tmp/lardon3d-mbm-XXXXXX";
CHECK(mkdtemp(dir));
char path[512];
CHECK(snprintf(path, sizeof(path), "%s/bad.mf", dir) > 0);
unsigned char buf[32] = {0};
memcpy(buf, "MD3L", 4);
buf[4] = 1;
buf[5] = 1;
buf[12] = 32;
buf[16] = 1;
buf[24] = 2;
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
CHECK(fd >= 0);
CHECK(write(fd, buf, 32) == 32);
(void)close(fd);
Lardon3DMatchFileHeader header;
CHECK(lardon3d_match_file_validate(path, &header, 0, 0, 0, 0) ==
LARDON3D_MATCH_FILE_BAD_MAGIC);
CHECK(unlink(path) == 0);
CHECK(rmdir(dir) == 0);
return true;
}
static bool test_match_file_bad_version(void) {
unsigned char buf[32] = {0};
memcpy(buf, "L3DM", 4);
buf[4] = 42;
char dir[] = "/tmp/lardon3d-mbv-XXXXXX";
CHECK(mkdtemp(dir));
char path[512];
CHECK(snprintf(path, sizeof(path), "%s/badv.mf", dir) > 0);
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
CHECK(fd >= 0);
CHECK(write(fd, buf, 32) == 32);
(void)close(fd);
Lardon3DMatchFileHeader header;
CHECK(lardon3d_match_file_validate(path, &header, 0, 0, 0, 0) ==
LARDON3D_MATCH_FILE_BAD_VERSION);
CHECK(unlink(path) == 0);
CHECK(rmdir(dir) == 0);
return true;
}
static bool test_match_file_truncated(void) {
unsigned char buf[32] = {0};
memcpy(buf, "L3DM", 4);
buf[4] = 1;
buf[5] = 1;
buf[8] = 1;
buf[12] = 32;
buf[16] = 1;
buf[24] = 2;
char dir[] = "/tmp/lardon3d-mt-XXXXXX";
CHECK(mkdtemp(dir));
char path[512];
CHECK(snprintf(path, sizeof(path), "%s/trunc.mf", dir) > 0);
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
CHECK(fd >= 0);
CHECK(write(fd, buf, 32) == 32);
(void)close(fd);
Lardon3DMatchFileHeader header;
CHECK(lardon3d_match_file_validate(path, &header, 0, 0, 0, 0) ==
LARDON3D_MATCH_FILE_BAD_SIZE);
CHECK(unlink(path) == 0);
CHECK(rmdir(dir) == 0);
return true;
}
static bool test_match_file_invalid_entry(void) {
char dir[] = "/tmp/lardon3d-mie-XXXXXX";
CHECK(mkdtemp(dir));
char path[512];
CHECK(snprintf(path, sizeof(path), "%s/bade.mf", dir) > 0);
unsigned char buf[44];
memset(buf, 0, 44);
memcpy(buf, "L3DM", 4);
buf[4] = 1;
buf[5] = 1;
buf[8] = 1;
buf[12] = 32;
buf[16] = 1;
buf[24] = 2;
buf[32] = 0x0F;
buf[33] = 0x27;
buf[34] = 0;
buf[35] = 0;
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
CHECK(fd >= 0);
CHECK(write(fd, buf, 44) == 44);
(void)close(fd);
Lardon3DMatchFileHeader header;
CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 8192, 8192) ==
LARDON3D_MATCH_FILE_BAD_ENTRY);
fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
CHECK(fd >= 0);
Lardon3DMatchFileEntry entries[1];
uint32_t count = 0;
CHECK(lardon3d_match_file_read(fd, &header, entries, 1, &count, 1, 2, 8192, 8192) ==
LARDON3D_MATCH_FILE_BAD_ENTRY);
(void)close(fd);
CHECK(unlink(path) == 0);
CHECK(rmdir(dir) == 0);
return true;
}
static bool test_match_file_nan_distance(void) {
char dir[] = "/tmp/lardon3d-mnd-XXXXXX";
CHECK(mkdtemp(dir));
char path[512];
CHECK(snprintf(path, sizeof(path), "%s/nan.mf", dir) > 0);
unsigned char buf[44];
memset(buf, 0, 44);
memcpy(buf, "L3DM", 4);
buf[4] = 1;
buf[5] = 1;
buf[8] = 1;
buf[12] = 32;
buf[16] = 1;
buf[24] = 2;
buf[40] = 0;
buf[41] = 0;
buf[42] = 0xC0;
buf[43] = 0x7F;
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
CHECK(fd >= 0);
CHECK(write(fd, buf, 44) == 44);
(void)close(fd);
fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
CHECK(fd >= 0);
Lardon3DMatchFileHeader header;
Lardon3DMatchFileEntry entries[1];
uint32_t count = 0;
CHECK(lardon3d_match_file_read(fd, &header, entries, 1, &count, 1, 2, 8192, 8192) ==
LARDON3D_MATCH_FILE_BAD_ENTRY);
(void)close(fd);
CHECK(unlink(path) == 0);
CHECK(rmdir(dir) == 0);
return true;
}
static bool test_match_file_f32(void) {
char dir[] = "/tmp/lardon3d-mf32-XXXXXX";
CHECK(mkdtemp(dir));
char path[512];
CHECK(snprintf(path, sizeof(path), "%s/sift.mf", dir) > 0);
Lardon3DMatchFileEntry entries[1];
entries[0].feature_index_a = 0;
entries[0].feature_index_b = 0;
entries[0].distance = 100.0F;
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
CHECK(fd >= 0);
CHECK(lardon3d_match_file_write(fd, 2, 128, 1, 2, entries, 1) ==
LARDON3D_MATCH_FILE_OK);
(void)close(fd);
Lardon3DMatchFileHeader header;
CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 8192, 8192) ==
LARDON3D_MATCH_FILE_OK);
CHECK(header.descriptor_type == 2);
CHECK(header.descriptor_dimension == 128);
CHECK(unlink(path) == 0);
CHECK(rmdir(dir) == 0);
return true;
}
static bool test_match_file_bad_type(void) {
unsigned char buf[32] = {0};
memcpy(buf, "L3DM", 4);
buf[4] = 1;
buf[5] = 3;
char dir[] = "/tmp/lardon3d-mbt-XXXXXX";
CHECK(mkdtemp(dir));
char path[512];
CHECK(snprintf(path, sizeof(path), "%s/badtype.mf", dir) > 0);
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
CHECK(fd >= 0);
CHECK(write(fd, buf, 32) == 32);
(void)close(fd);
Lardon3DMatchFileHeader header;
CHECK(lardon3d_match_file_validate(path, &header, 0, 0, 0, 0) ==
LARDON3D_MATCH_FILE_BAD_TYPE);
CHECK(unlink(path) == 0);
CHECK(rmdir(dir) == 0);
return true;
}
static bool test_match_file_maximum_and_multiplicity(void) {
char dir[] = "/tmp/lardon3d-mmax-XXXXXX";
CHECK(mkdtemp(dir));
char path[512];
CHECK(snprintf(path, sizeof(path), "%s/max.mf", dir) > 0);
Lardon3DMatchFileEntry *entries = calloc(LARDON3D_MATCH_FILE_MAX_MATCHES,
sizeof(*entries));
CHECK(entries);
for (uint32_t i = 0; i < LARDON3D_MATCH_FILE_MAX_MATCHES; ++i) {
entries[i].feature_index_a = i;
entries[i].feature_index_b = 0;
entries[i].distance = (float)i;
}
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
CHECK(fd >= 0);
CHECK(lardon3d_match_file_write(fd, 1, 32, 11, 12, entries,
LARDON3D_MATCH_FILE_MAX_MATCHES) ==
LARDON3D_MATCH_FILE_OK);
CHECK(lardon3d_match_file_write(fd, 1, 32, 11, 12, entries,
LARDON3D_MATCH_FILE_MAX_MATCHES + 1) ==
LARDON3D_MATCH_FILE_INVALID_ARGUMENT);
CHECK(close(fd) == 0);
struct stat info;
CHECK(stat(path, &info) == 0 && info.st_size == LARDON3D_MATCH_FILE_MAX_SIZE);
Lardon3DMatchFileHeader header;
CHECK(lardon3d_match_file_validate(path, &header, 11, 12, 8192, 1) ==
LARDON3D_MATCH_FILE_OK);
free(entries);
CHECK(unlink(path) == 0);
CHECK(rmdir(dir) == 0);
return true;
}
static bool write_minimal_match_file(const char *path) {
Lardon3DMatchFileEntry entry = {0, 0, 1.0F};
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (fd < 0) return false;
bool ok = lardon3d_match_file_write(fd, 1, 32, 1, 2, &entry, 1) ==
LARDON3D_MATCH_FILE_OK;
return close(fd) == 0 && ok;
}
static bool mutate_bytes(const char *path, off_t offset, const void *bytes, size_t size) {
int fd = open(path, O_WRONLY | O_CLOEXEC);
if (fd < 0) return false;
bool ok = pwrite(fd, bytes, size, offset) == (ssize_t)size;
return close(fd) == 0 && ok;
}
static bool expect_mutation(const char *path, off_t offset, const void *bytes, size_t size,
Lardon3DMatchFileResult expected) {
if (!write_minimal_match_file(path) || !mutate_bytes(path, offset, bytes, size)) return false;
Lardon3DMatchFileHeader header;
return lardon3d_match_file_validate(path, &header, 1, 2, 1, 1) == expected;
}
static bool test_match_file_corruption_matrix(void) {
char dir[] = "/tmp/lardon3d-matrix-XXXXXX";
CHECK(mkdtemp(dir));
char path[512];
CHECK(snprintf(path, sizeof(path), "%s/match", dir) > 0);
Lardon3DMatchFileHeader header;
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
CHECK(fd >= 0 && close(fd) == 0);
CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 1, 1) ==
LARDON3D_MATCH_FILE_BAD_SIZE);
unsigned char one = 0;
CHECK(mutate_bytes(path, 0, &one, 1));
CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 1, 1) ==
LARDON3D_MATCH_FILE_BAD_SIZE);
unsigned char version_zero = 0;
unsigned char version_future = 2;
unsigned char type_zero = 0;
unsigned char type_unknown = 99;
unsigned char reserved[2] = {1, 0};
unsigned char dimension[4] = {31, 0, 0, 0};
unsigned char zero_id[8] = {0};
unsigned char count_8193[4] = {1, 32, 0, 0};
unsigned char count_max[4] = {255, 255, 255, 255};
unsigned char bad_magic[4] = {'B', 'A', 'D', '!'};
CHECK(expect_mutation(path, 0, bad_magic, 4, LARDON3D_MATCH_FILE_BAD_MAGIC));
CHECK(expect_mutation(path, 4, &version_zero, 1, LARDON3D_MATCH_FILE_BAD_VERSION));
CHECK(expect_mutation(path, 4, &version_future, 1, LARDON3D_MATCH_FILE_BAD_VERSION));
CHECK(expect_mutation(path, 5, &type_zero, 1, LARDON3D_MATCH_FILE_BAD_TYPE));
CHECK(expect_mutation(path, 5, &type_unknown, 1, LARDON3D_MATCH_FILE_BAD_TYPE));
CHECK(expect_mutation(path, 6, reserved, 2, LARDON3D_MATCH_FILE_CORRUPT));
CHECK(expect_mutation(path, 12, dimension, 4, LARDON3D_MATCH_FILE_BAD_TYPE));
CHECK(expect_mutation(path, 16, zero_id, 8, LARDON3D_MATCH_FILE_CORRUPT));
CHECK(expect_mutation(path, 24, zero_id, 8, LARDON3D_MATCH_FILE_CORRUPT));
CHECK(expect_mutation(path, 8, count_8193, 4, LARDON3D_MATCH_FILE_BAD_COUNT));
CHECK(expect_mutation(path, 8, count_max, 4, LARDON3D_MATCH_FILE_BAD_COUNT));
unsigned char index_one[4] = {1, 0, 0, 0};
CHECK(expect_mutation(path, 32, index_one, 4, LARDON3D_MATCH_FILE_BAD_ENTRY));
CHECK(expect_mutation(path, 36, index_one, 4, LARDON3D_MATCH_FILE_BAD_ENTRY));
unsigned char negative[4] = {0, 0, 128, 191};
unsigned char negative_zero[4] = {0, 0, 0, 128};
unsigned char positive_inf[4] = {0, 0, 128, 127};
unsigned char negative_inf[4] = {0, 0, 128, 255};
unsigned char nan[4] = {0, 0, 192, 127};
CHECK(expect_mutation(path, 40, negative, 4, LARDON3D_MATCH_FILE_BAD_ENTRY));
CHECK(expect_mutation(path, 40, positive_inf, 4, LARDON3D_MATCH_FILE_BAD_ENTRY));
CHECK(expect_mutation(path, 40, negative_inf, 4, LARDON3D_MATCH_FILE_BAD_ENTRY));
CHECK(expect_mutation(path, 40, nan, 4, LARDON3D_MATCH_FILE_BAD_ENTRY));
CHECK(expect_mutation(path, 40, negative_zero, 4, LARDON3D_MATCH_FILE_OK));
CHECK(write_minimal_match_file(path));
fd = open(path, O_WRONLY | O_APPEND | O_CLOEXEC);
CHECK(fd >= 0 && write(fd, "x", 1) == 1 && close(fd) == 0);
CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 1, 1) ==
LARDON3D_MATCH_FILE_BAD_SIZE);
Lardon3DMatchFileEntry ordered[2] = {{0, 0, 0.0F}, {1, 0, 1.0F}};
fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
CHECK(fd >= 0 && lardon3d_match_file_write(fd, 1, 32, 1, 2, ordered, 2) ==
LARDON3D_MATCH_FILE_OK &&
close(fd) == 0);
unsigned char duplicate_a[4] = {0, 0, 0, 0};
CHECK(mutate_bytes(path, 44, duplicate_a, sizeof(duplicate_a)));
CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 2, 1) ==
LARDON3D_MATCH_FILE_BAD_ENTRY);
ordered[1].feature_index_a = 0;
fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
CHECK(fd >= 0 && lardon3d_match_file_write(fd, 1, 32, 1, 2, ordered, 2) ==
LARDON3D_MATCH_FILE_BAD_ENTRY &&
close(fd) == 0);
CHECK(unlink(path) == 0 && rmdir(dir) == 0);
return true;
}
/* --- Matcher utility function tests --- */
static bool test_matcher_kind_string(void) {
CHECK(strcmp(lardon3d_matcher_kind_string(LARDON3D_MATCHER_ORB_BF), "orb_bf") == 0);
CHECK(strcmp(lardon3d_matcher_kind_string(LARDON3D_MATCHER_SIFT_BF), "sift_bf") == 0);
CHECK(strcmp(lardon3d_matcher_kind_string(LARDON3D_MATCHER_ROOTSIFT_BF), "rootsift_bf") == 0);
return true;
}
static bool test_matcher_fingerprint(void) {
unsigned char fp1[32], fp2[32];
Lardon3DMatcherParams p1 = {LARDON3D_MATCHER_ORB_BF, 0.75F};
Lardon3DMatcherParams p2 = {LARDON3D_MATCHER_ORB_BF, 0.80F};
lardon3d_matcher_fingerprint(&p1, fp1);
lardon3d_matcher_fingerprint(&p2, fp2);
CHECK(memcmp(fp1, fp2, 32) != 0);
unsigned char fp3[32];
lardon3d_matcher_fingerprint(&p1, fp3);
CHECK(memcmp(fp1, fp3, 32) == 0);
Lardon3DMatcherParams p3 = {LARDON3D_MATCHER_SIFT_BF, 0.75F};
unsigned char fp4[32];
lardon3d_matcher_fingerprint(&p3, fp4);
CHECK(memcmp(fp1, fp4, 32) != 0);
return true;
}
static bool test_matcher_default_ratio(void) {
float r = lardon3d_matcher_default_ratio(LARDON3D_MATCHER_ORB_BF);
CHECK(r == 0.75F);
r = lardon3d_matcher_default_ratio(LARDON3D_MATCHER_SIFT_BF);
CHECK(r == 0.7F);
r = lardon3d_matcher_default_ratio(LARDON3D_MATCHER_ROOTSIFT_BF);
CHECK(r == 0.7F);
return true;
}
static bool run_tests(void) {
CHECK(test_match_file_write_read());
CHECK(test_match_file_empty());
CHECK(test_match_file_bad_magic());
CHECK(test_match_file_bad_version());
CHECK(test_match_file_truncated());
CHECK(test_match_file_invalid_entry());
CHECK(test_match_file_nan_distance());
CHECK(test_match_file_f32());
CHECK(test_match_file_bad_type());
CHECK(test_match_file_maximum_and_multiplicity());
CHECK(test_match_file_corruption_matrix());
CHECK(test_matcher_kind_string());
CHECK(test_matcher_fingerprint());
CHECK(test_matcher_default_ratio());
return true;
}
int main(void) {
return run_tests() ? EXIT_SUCCESS : EXIT_FAILURE;
}

385
tests/test_matcher_e2e.c Normal file
View file

@ -0,0 +1,385 @@
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <math.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/app_state.h>
#include <lardon3d/feature_store.h>
#include <lardon3d/matcher.h>
#include <lardon3d/project_db.h>
#define CHECK(x) \
do { \
if (!(x)) { \
fprintf(stderr, "Échec ligne %d: %s\n", __LINE__, #x); \
return false; \
} \
} while (0)
typedef struct {
char root[PATH_MAX];
char database_path[PATH_MAX];
Lardon3DProjectDb *database;
Lardon3DAppState state;
Lardon3DProjectDbScanSet scanset;
Lardon3DProjectDbImage image_a;
Lardon3DProjectDbImage image_b;
Lardon3DProjectDbCandidatePair pair;
} Fixture;
static bool join_path(char output[PATH_MAX], const char *a, const char *b) {
int n = snprintf(output, PATH_MAX, "%s/%s", a, b);
return n > 0 && (size_t)n < PATH_MAX;
}
static bool remove_tree(const char *path) {
struct stat info;
if (lstat(path, &info) != 0) return errno == ENOENT;
if (!S_ISDIR(info.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[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 void image_asset_path(const unsigned char hash[32], char path[4096]) {
static const char digits[] = "0123456789abcdef";
char hex[65];
for (size_t i = 0; i < 32; ++i) {
hex[2 * i] = digits[hash[i] >> 4];
hex[2 * i + 1] = digits[hash[i] & 15U];
}
hex[64] = '\0';
(void)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];
memset(hash, seed, sizeof(hash));
char path[4096];
image_asset_path(hash, path);
Lardon3DProjectDbImageRegisterStatus status;
return lardon3d_project_db_register_image(
fixture->database, fixture->scanset.scanset_id, hash, path, 1, "fixture.bin",
"/synthetic/fixture.bin", 0, seed, &status, image) == LARDON3D_PROJECT_DB_OK;
}
static bool fixture_open(Fixture *fixture) {
memset(fixture, 0, sizeof(*fixture));
char root[] = "/tmp/lardon3d-matcher-e2e-XXXXXX";
char *created = mkdtemp(root);
if (!created || snprintf(fixture->root, sizeof(fixture->root), "%s", created) <= 0 ||
!join_path(fixture->database_path, fixture->root, "project.db"))
return false;
char error[256];
if (lardon3d_project_db_open(fixture->database_path, &fixture->database, error) !=
LARDON3D_PROJECT_DB_OK)
return false;
lardon3d_app_state_init(&fixture->state);
fixture->state.project_loaded = true;
fixture->state.project_db = fixture->database;
(void)snprintf(fixture->state.project_path, sizeof(fixture->state.project_path), "%s",
fixture->root);
return lardon3d_project_db_create_scanset(fixture->database, "matcher-e2e",
&fixture->scanset) == LARDON3D_PROJECT_DB_OK &&
register_image(fixture, 1, &fixture->image_a) &&
register_image(fixture, 2, &fixture->image_b) &&
lardon3d_project_db_create_candidate_pair(
fixture->database, fixture->image_a.image_id, fixture->image_b.image_id, 1,
&fixture->pair) == LARDON3D_PROJECT_DB_OK;
}
static bool fixture_close(Fixture *fixture) {
char matches_path[PATH_MAX];
bool no_temps = true;
if (join_path(matches_path, fixture->root, "assets/matches")) {
DIR *directory = opendir(matches_path);
if (directory) {
for (struct dirent *entry = readdir(directory); entry; entry = readdir(directory)) {
if (strncmp(entry->d_name, ".match-", 7) == 0) no_temps = false;
}
if (closedir(directory) != 0) no_temps = false;
} else if (errno != ENOENT) {
no_temps = false;
}
} else {
no_temps = false;
}
if (fixture->database) lardon3d_project_db_close(fixture->database);
fixture->database = NULL;
fixture->state.project_db = NULL;
bool removed = remove_tree(fixture->root);
return no_temps && removed;
}
static bool publish_features(Fixture *fixture, uint64_t image_id, const char *kind,
Lardon3DFeatureDescriptorType type, const void *descriptors,
uint32_t count, unsigned char fingerprint_seed,
Lardon3DProjectDbFeatureSet *set) {
Lardon3DExtractedFeatures features;
memset(&features, 0, sizeof(features));
features.image_width = 64;
features.image_height = 64;
features.feature_count = count;
if (count > 0) {
features.keypoints = calloc(count, sizeof(*features.keypoints));
if (!features.keypoints) return false;
for (uint32_t i = 0; i < count; ++i) features.keypoints[i].size = 1.0F;
size_t scalar = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 1 : sizeof(float);
size_t dimension = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 32 : 128;
features.descriptor_bytes = (size_t)count * dimension * scalar;
features.descriptors = malloc(features.descriptor_bytes);
if (!features.descriptors) {
free(features.keypoints);
return false;
}
memcpy(features.descriptors, descriptors, features.descriptor_bytes);
}
unsigned char fingerprint[32];
memset(fingerprint, fingerprint_seed, sizeof(fingerprint));
uint32_t dimension = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 32 : 128;
Lardon3DFeatureStoreResult result = lardon3d_feature_store_publish_v2(
&fixture->state, image_id, 0, kind, 1, fingerprint, type, dimension, 0, &features, set);
free(features.keypoints);
free(features.descriptors);
return result == LARDON3D_FEATURE_STORE_OK;
}
static bool read_result_asset(const Fixture *fixture, const Lardon3DProjectDbFeatureSet *set_a,
const Lardon3DProjectDbFeatureSet *set_b,
const Lardon3DProjectDbMatchResult *result,
Lardon3DMatchFileEntry *entries, size_t capacity,
uint32_t *count) {
char path[PATH_MAX];
if (!join_path(path, fixture->root, result->match_asset_path)) return false;
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) return false;
Lardon3DMatchFileHeader header;
Lardon3DMatchFileResult read_result = lardon3d_match_file_read(
fd, &header, entries, capacity, count, set_a->feature_set_id, set_b->feature_set_id,
set_a->feature_count, set_b->feature_count);
(void)close(fd);
return read_result == LARDON3D_MATCH_FILE_OK;
}
static bool run_kind_e2e(const char *kind, Lardon3DMatcherKind matcher_kind,
Lardon3DFeatureDescriptorType type) {
Fixture fixture;
CHECK(fixture_open(&fixture));
uint32_t dimension = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 32 : 128;
size_t scalar = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 1 : sizeof(float);
size_t row_bytes = (size_t)dimension * scalar;
unsigned char *a = calloc(3, row_bytes);
unsigned char *b = calloc(3, row_bytes);
CHECK(a && b);
if (type == LARDON3D_FEATURE_DESCRIPTOR_U8) {
memset(a + row_bytes, 0xFF, row_bytes);
memset(b + row_bytes, 0xF0, row_bytes);
memset(b + 2 * row_bytes, 0xFF, row_bytes);
} else {
float *af = (float *)a;
float *bf = (float *)b;
for (uint32_t i = 0; i < dimension; ++i) {
af[dimension + i] = 2.0F;
bf[dimension + i] = 1.0F;
bf[2 * dimension + i] = 2.0F;
}
}
Lardon3DProjectDbFeatureSet set_a, set_b;
CHECK(publish_features(&fixture, fixture.image_a.image_id, kind, type, a, 3, 3, &set_a));
CHECK(publish_features(&fixture, fixture.image_b.image_id, kind, type, b, 3, 4, &set_b));
free(a);
free(b);
Lardon3DMatcherParams params = {matcher_kind, lardon3d_matcher_default_ratio(matcher_kind)};
Lardon3DProjectDbMatchResult result;
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&set_a, &set_b, &params, &result) ==
LARDON3D_MATCHER_OK);
CHECK(result.result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED &&
result.match_count == 3 && result.has_match_asset);
uint64_t result_id = result.match_result_id;
unsigned char asset_hash[32];
memcpy(asset_hash, result.match_asset_sha256, 32);
Lardon3DMatchFileEntry entries[3];
uint32_t count = 0;
CHECK(read_result_asset(&fixture, &set_a, &set_b, &result, entries, 3, &count));
CHECK(count == 3 && entries[0].feature_index_a == 0 && entries[0].feature_index_b == 0 &&
entries[1].feature_index_a == 1 && entries[1].feature_index_b == 2 &&
entries[2].feature_index_a == 2 && entries[2].feature_index_b == 0);
lardon3d_project_db_close(fixture.database);
fixture.database = NULL;
fixture.state.project_db = NULL;
char error[256];
CHECK(lardon3d_project_db_open(fixture.database_path, &fixture.database, error) ==
LARDON3D_PROJECT_DB_OK);
fixture.state.project_db = fixture.database;
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&set_a, &set_b, &params, &result) ==
LARDON3D_MATCHER_OK);
CHECK(result.match_result_id == result_id &&
memcmp(result.match_asset_sha256, asset_hash, 32) == 0);
char asset_path[PATH_MAX];
CHECK(join_path(asset_path, fixture.root, result.match_asset_path));
int fd = open(asset_path, O_WRONLY | O_TRUNC | O_CLOEXEC);
CHECK(fd >= 0 && write(fd, "corrupt", 7) == 7 && close(fd) == 0);
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&set_a, &set_b, &params, &result) ==
LARDON3D_MATCHER_OK);
CHECK(result.match_result_id == result_id && result.match_count == 3 &&
read_result_asset(&fixture, &set_a, &set_b, &result, entries, 3, &count));
fd = open(asset_path, O_WRONLY | O_CLOEXEC);
unsigned char wrong_feature_set[8] = {99, 0, 0, 0, 0, 0, 0, 0};
CHECK(fd >= 0 && pwrite(fd, wrong_feature_set, sizeof(wrong_feature_set), 16) ==
(ssize_t)sizeof(wrong_feature_set) &&
close(fd) == 0);
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&set_a, &set_b, &params, &result) ==
LARDON3D_MATCHER_OK);
CHECK(result.match_result_id == result_id &&
read_result_asset(&fixture, &set_a, &set_b, &result, entries, 3, &count));
CHECK(unlink(asset_path) == 0);
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&set_a, &set_b, &params, &result) ==
LARDON3D_MATCHER_OK);
CHECK(result.match_result_id == result_id &&
read_result_asset(&fixture, &set_a, &set_b, &result, entries, 3, &count));
Lardon3DMatcherParams changed = params;
changed.ratio_threshold -= 0.05F;
Lardon3DProjectDbMatchResult distinct;
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&set_a, &set_b, &changed, &distinct) ==
LARDON3D_MATCHER_OK);
CHECK(distinct.match_result_id != result_id &&
memcmp(distinct.match_asset_sha256, result.match_asset_sha256, 32) == 0);
for (int iteration = 0; iteration < 100; ++iteration) {
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&set_a, &set_b, &params, &result) ==
LARDON3D_MATCHER_OK);
CHECK(result.match_result_id == result_id && result.match_count == 3);
}
CHECK(fixture_close(&fixture));
return true;
}
static bool test_ratio_single_neighbor_and_no_match(void) {
Fixture fixture;
CHECK(fixture_open(&fixture));
unsigned char a[32], b[64];
memset(a, 0x0F, sizeof(a));
memset(b, 0, 32);
memset(b + 32, 0xFF, 32);
Lardon3DProjectDbFeatureSet set_a, set_b;
CHECK(publish_features(&fixture, fixture.image_a.image_id, "orb",
LARDON3D_FEATURE_DESCRIPTOR_U8, a, 1, 10, &set_a));
CHECK(publish_features(&fixture, fixture.image_b.image_id, "orb",
LARDON3D_FEATURE_DESCRIPTOR_U8, b, 2, 11, &set_b));
Lardon3DMatcherParams params = {LARDON3D_MATCHER_ORB_BF, 0.75F};
Lardon3DProjectDbMatchResult result;
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&set_a, &set_b, &params, &result) ==
LARDON3D_MATCHER_OK);
CHECK(result.result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH &&
result.match_count == 0 && !result.has_match_asset);
uint64_t no_match_id = result.match_result_id;
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&set_a, &set_b, &params, &result) ==
LARDON3D_MATCHER_OK && result.match_result_id == no_match_id);
for (int iteration = 0; iteration < 100; ++iteration) {
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&set_a, &set_b, &params, &result) ==
LARDON3D_MATCHER_OK);
CHECK(result.match_result_id == no_match_id && !result.has_match_asset);
}
CHECK(fixture_close(&fixture));
CHECK(fixture_open(&fixture));
memset(a, 0, sizeof(a));
memset(b, 0, 32);
CHECK(publish_features(&fixture, fixture.image_a.image_id, "orb",
LARDON3D_FEATURE_DESCRIPTOR_U8, a, 1, 12, &set_a));
CHECK(publish_features(&fixture, fixture.image_b.image_id, "orb",
LARDON3D_FEATURE_DESCRIPTOR_U8, b, 1, 13, &set_b));
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&set_a, &set_b, &params, &result) ==
LARDON3D_MATCHER_OK);
CHECK(result.result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED && result.match_count == 1);
CHECK(fixture_close(&fixture));
CHECK(fixture_open(&fixture));
CHECK(publish_features(&fixture, fixture.image_a.image_id, "orb",
LARDON3D_FEATURE_DESCRIPTOR_U8, a, 1, 14, &set_a));
CHECK(publish_features(&fixture, fixture.image_b.image_id, "orb",
LARDON3D_FEATURE_DESCRIPTOR_U8, NULL, 0, 15, &set_b));
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&set_a, &set_b, &params, &result) ==
LARDON3D_MATCHER_OK);
CHECK(result.result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH && result.match_count == 0);
CHECK(fixture_close(&fixture));
return true;
}
static bool test_kind_and_ownership_rejection(void) {
Fixture fixture;
CHECK(fixture_open(&fixture));
float descriptors[128] = {0};
Lardon3DProjectDbFeatureSet sift_a, sift_b;
CHECK(publish_features(&fixture, fixture.image_b.image_id, "sift",
LARDON3D_FEATURE_DESCRIPTOR_F32, descriptors, 1, 21, &sift_b));
CHECK(publish_features(&fixture, fixture.image_a.image_id, "sift",
LARDON3D_FEATURE_DESCRIPTOR_F32, descriptors, 1, 20, &sift_a));
CHECK(sift_a.feature_set_id > sift_b.feature_set_id);
Lardon3DMatcherParams rootsift = {LARDON3D_MATCHER_ROOTSIFT_BF, 0.7F};
Lardon3DMatcherStats stats;
char output[PATH_MAX];
CHECK(join_path(output, fixture.root, "wrong.match"));
CHECK(lardon3d_matcher_run(fixture.root, &sift_a, &sift_b, &rootsift, output, &stats) ==
LARDON3D_MATCHER_TYPE_MISMATCH);
Lardon3DMatcherParams sift = {LARDON3D_MATCHER_SIFT_BF, 0.7F};
Lardon3DProjectDbMatchResult result;
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&sift_a, &sift_b, &sift, &result) ==
LARDON3D_MATCHER_OK);
CHECK(result.result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED);
CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair,
&sift_b, &sift_a, &sift, &result) ==
LARDON3D_MATCHER_FAILED);
CHECK(fixture_close(&fixture));
return true;
}
static bool run_tests(void) {
CHECK(run_kind_e2e("orb", LARDON3D_MATCHER_ORB_BF,
LARDON3D_FEATURE_DESCRIPTOR_U8));
CHECK(run_kind_e2e("sift", LARDON3D_MATCHER_SIFT_BF,
LARDON3D_FEATURE_DESCRIPTOR_F32));
CHECK(run_kind_e2e("rootsift", LARDON3D_MATCHER_ROOTSIFT_BF,
LARDON3D_FEATURE_DESCRIPTOR_F32));
CHECK(test_ratio_single_neighbor_and_no_match());
CHECK(test_kind_and_ownership_rejection());
return true;
}
int main(void) {
return run_tests() ? EXIT_SUCCESS : EXIT_FAILURE;
}

View file

@ -89,7 +89,7 @@ static bool create_future_database(const char *path) {
} }
bool ok = sqlite3_exec(connection, bool ok = sqlite3_exec(connection,
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
"INSERT INTO metadata VALUES('schema_version',10);", "INSERT INTO metadata VALUES('schema_version',11);",
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 match_results;"
"DROP TABLE candidate_pair_generate_tasks;" "DROP TABLE candidate_pair_generate_tasks;"
"DROP TABLE candidate_pairs;" "DROP TABLE candidate_pairs;"
"UPDATE metadata SET value=7 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;"; "UPDATE metadata SET value=7 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;";
@ -119,6 +120,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 match_results;"
"DROP TABLE candidate_pair_generate_tasks;" "DROP TABLE candidate_pair_generate_tasks;"
"DROP TABLE candidate_pairs;" "DROP TABLE candidate_pairs;"
"DROP TABLE feature_support_members;DROP TABLE feature_support_groups;" "DROP TABLE feature_support_members;DROP TABLE feature_support_groups;"
@ -308,7 +310,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) == 9); CHECK(database && lardon3d_project_db_schema_version(database) == 10);
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 &&
@ -575,7 +577,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'", 9)); CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 10));
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);
@ -597,7 +599,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) == 9); CHECK(lardon3d_project_db_schema_version(database) == 10);
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);
@ -607,7 +609,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'", 9)); CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 10));
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);
@ -632,7 +634,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'", 9)); CHECK(query_integer(v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 10));
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);
@ -660,7 +662,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'", 9)); CHECK(query_integer(v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 10));
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);
@ -677,7 +679,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) == 9); CHECK(lardon3d_project_db_schema_version(database) == 10);
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);
@ -711,24 +713,24 @@ static bool run_test(void) {
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
"name='sift_extract_tasks'", 0)); "name='sift_extract_tasks'", 0));
CHECK(lardon3d_project_db_open(failed_v7_path, &database, error) == LARDON3D_PROJECT_DB_OK && CHECK(lardon3d_project_db_open(failed_v7_path, &database, error) == LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 9); lardon3d_project_db_schema_version(database) == 10);
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) == 9); lardon3d_project_db_schema_version(database) == 10);
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) == 9); CHECK(lardon3d_project_db_schema_version(database) == 10);
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'", 9)); CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 10));
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);
@ -739,7 +741,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) == 9); lardon3d_project_db_schema_version(database) == 10);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;